From cfc7ce86ad0aaaa72b617623023859f6dfac3d20 Mon Sep 17 00:00:00 2001 From: Ashish Acharya Date: Tue, 12 Jul 2022 23:01:44 -0500 Subject: [PATCH] add fastapi on top of pybaghchal --- Board.py | 147 ++++++++++++++++++++++++++++++++++------------- Engine.py | 11 ++-- Game.py | 2 +- api.py | 68 ++++++++++++++++++++++ requirements.txt | 2 + 5 files changed, 184 insertions(+), 46 deletions(-) create mode 100644 api.py create mode 100644 requirements.txt diff --git a/Board.py b/Board.py index 5034f4d..8cdb6fa 100644 --- a/Board.py +++ b/Board.py @@ -9,22 +9,59 @@ class Board(object): # possible connections from one point to another _move_connections = { - 0: [1, 5, 6], 1: [2, 0, 6], 2: [3, 1, 7, 6, 8], 3: [4, 2, 8], 4: [3, 9, 8], - 5: [6, 10, 0], 6: [7, 5, 11, 1, 10, 2, 12, 0], 7: [8, 6, 12, 2], 8: [9, 7, 13, 3, 12, 4, 14, 2], 9: [8, 14, 4], - 10: [11, 15, 5, 6, 16], 11: [12, 10, 16, 6], 12: [13, 11, 17, 7, 16, 8, 18, 6], 13: [14, 12, 18, 8], + 0: [1, 5, 6], + 1: [2, 0, 6], + 2: [3, 1, 7, 6, 8], + 3: [4, 2, 8], + 4: [3, 9, 8], + 5: [6, 10, 0], + 6: [7, 5, 11, 1, 10, 2, 12, 0], + 7: [8, 6, 12, 2], + 8: [9, 7, 13, 3, 12, 4, 14, 2], + 9: [8, 14, 4], + 10: [11, 15, 5, 6, 16], + 11: [12, 10, 16, 6], + 12: [13, 11, 17, 7, 16, 8, 18, 6], + 13: [14, 12, 18, 8], 14: [13, 19, 9, 18, 8], - 15: [16, 20, 10], 16: [17, 15, 21, 11, 20, 12, 22, 10], 17: [18, 16, 22, 12], - 18: [19, 17, 23, 13, 22, 14, 24, 12], 19: [18, 24, 14], - 20: [21, 15, 16], 21: [22, 20, 16], 22: [23, 21, 17, 18, 16], 23: [24, 22, 18], 24: [23, 19, 18] + 15: [16, 20, 10], + 16: [17, 15, 21, 11, 20, 12, 22, 10], + 17: [18, 16, 22, 12], + 18: [19, 17, 23, 13, 22, 14, 24, 12], + 19: [18, 24, 14], + 20: [21, 15, 16], + 21: [22, 20, 16], + 22: [23, 21, 17, 18, 16], + 23: [24, 22, 18], + 24: [23, 19, 18], } _capture_connections = { - 0: [2, 10, 12], 1: [3, 11], 2: [4, 0, 12, 10, 14], 3: [1, 13], 4: [2, 14, 12], - 5: [7, 15], 6: [8, 16, 18], 7: [9, 5, 17], 8: [6, 18, 16], 9: [7, 19], - 10: [12, 20, 0, 2, 22], 11: [13, 21, 1], 12: [14, 10, 22, 2, 20, 4, 24, 0], - 13: [11, 23, 3], 14: [12, 24, 4, 22, 2], - 15: [17, 5], 16: [18, 6, 8], 17: [19, 15, 7], 18: [16, 8, 6], 19: [17, 9], - 20: [22, 10, 12], 21: [23, 11], 22: [24, 20, 12, 14, 10], 23: [21, 13], 24: [22, 14, 12] + 0: [2, 10, 12], + 1: [3, 11], + 2: [4, 0, 12, 10, 14], + 3: [1, 13], + 4: [2, 14, 12], + 5: [7, 15], + 6: [8, 16, 18], + 7: [9, 5, 17], + 8: [6, 18, 16], + 9: [7, 19], + 10: [12, 20, 0, 2, 22], + 11: [13, 21, 1], + 12: [14, 10, 22, 2, 20, 4, 24, 0], + 13: [11, 23, 3], + 14: [12, 24, 4, 22, 2], + 15: [17, 5], + 16: [18, 6, 8], + 17: [19, 15, 7], + 18: [16, 8, 6], + 19: [17, 9], + 20: [22, 10, 12], + 21: [23, 11], + 22: [24, 20, 12, 14, 10], + 23: [21, 13], + 24: [22, 14, 12], } class Player(Enum): @@ -51,11 +88,15 @@ def __str__(self): return self.__repr__() # f = from, t = to, mt = MoveType - nt = namedtuple('Move', ['f', 't', 'mt']) + nt = namedtuple("Move", ["f", "t", "mt"]) class Move(nt): def __repr__(self): - return "%s-%s-%s" % (Point.get_coord(self.f), Point.get_coord(self.t), self.mt.name) + return "%s-%s-%s" % ( + Point.get_coord(self.f), + Point.get_coord(self.t), + self.mt.name, + ) # horizontal (1, -1) # vertical (5, -5) # diagonal (4, -4, 6, -6) directions = [1, -1, 5, -5, 4, -4, 6, -6] @@ -85,7 +126,8 @@ def reset(self): self.lastMove = "" def show(self): - print(""" a b c d e + print( + """ A B C D E 1 %s %s %s %s %s | \\ | / | \\ | / | 2 %s %s %s %s %s @@ -94,7 +136,9 @@ def show(self): | \\ | / | \\ | / | 4 %s %s %s %s %s | / | \\ | / | \\ | -5 %s %s %s %s %s\n""" % tuple(i.print_state() for i in self.points)) +5 %s %s %s %s %s\n""" + % tuple(i.print_state() for i in self.points) + ) print("Turn: %s" % ("Goat" if self.turn == self.Player.G else "Tiger")) print("Remaining Goats: %d" % self.goatsToBePlaced) print("Dead Goats: %d\n" % self.deadGoats) @@ -108,20 +152,23 @@ def _get_full_position(pos_string): """ full_pos = [] - pos_string = pos_string.upper().split('/') + pos_string = pos_string.upper().split("/") for row in pos_string: row_pos = [] for i in row: - if i == 'G': - row_pos.append('G') - elif i == 'T': - row_pos.append('T') + if i == "G": + row_pos.append("G") + elif i == "T": + row_pos.append("T") elif i.isdigit(): for j in range(int(i)): - row_pos.append('E') + row_pos.append("E") # check the validity of each row and print invalid rows here - assert len(row_pos) % 5 == 0, "Invaild row %s. row_pos: %s" % (row, ''.join(row_pos)) + assert len(row_pos) % 5 == 0, "Invaild row %s. row_pos: %s" % ( + row, + "".join(row_pos), + ) full_pos.extend(row_pos) return full_pos @@ -136,10 +183,12 @@ def parse_position(self, position): self.turn = self.Player[parts[1].upper()] self.goatsToBePlaced = int(parts[2][1:]) - assert (self.goatsToBePlaced in range(21)), "Invalid goatsToBePlaced: %d" % self.goatsToBePlaced + assert self.goatsToBePlaced in range(21), ( + "Invalid goatsToBePlaced: %d" % self.goatsToBePlaced + ) self.deadGoats = int(parts[3][1:]) - assert (self.deadGoats in range(6)), "Invalid deadGoats: %d" % self.deadGoats + assert self.deadGoats in range(6), "Invalid deadGoats: %d" % self.deadGoats self.lastMove = parts[4][1:] @@ -169,18 +218,23 @@ def position(self): """ # I'm sorry for this line, but I love Python! - pos_string = ''.join(['/' * (n % 5 == 0 and n != 0) + p.get_state().name for n, p in enumerate(self.points)]) + pos_string = "".join( + [ + "/" * (n % 5 == 0 and n != 0) + p.get_state().name + for n, p in enumerate(self.points) + ] + ) # replacement dict: {'EEEEE: 5, 'EEEE': 4, 'EEE: 3, 'EE': 2, 'E': 1} for i in reversed(range(1, 6)): - pos_string = pos_string.replace(''.join('E' * i), str(i)) + pos_string = pos_string.replace("".join("E" * i), str(i)) return "%s %s %s %s %s" % ( - ''.join(pos_string), + "".join(pos_string), self.turn.name.lower(), - 'g%d' % self.goatsToBePlaced, - 'c%d' % self.deadGoats, - 'm%s' % self.lastMove + "g%d" % self.goatsToBePlaced, + "c%d" % self.deadGoats, + "m%s" % self.lastMove, ) @staticmethod @@ -203,7 +257,8 @@ def is_movable(self, from_point, to_point): return ( # connection must exist - to_point in Board._move_connections[from_point] and + to_point in Board._move_connections[from_point] + and # to_point must be empty self.points[to_point].get_state() == Point.State.E ) @@ -226,11 +281,14 @@ def can_capture(self, from_point, to_point): return ( # connection must exist - to_point in Board._capture_connections[from_point] and + to_point in Board._capture_connections[from_point] + and # from_point must be a tiger - self.points[from_point].get_state() == Point.State.T and + self.points[from_point].get_state() == Point.State.T + and # mid_point must be a goat - self.points[mid_point].get_state() == Point.State.G and + self.points[mid_point].get_state() == Point.State.G + and # to_point must be empty self.points[to_point].get_state() == Point.State.E ) @@ -267,8 +325,7 @@ def winner(self): return None - -# move related + # move related def _placements(self): return [ Board.Move(point.index, point.index, Board.MoveType.P) @@ -429,7 +486,7 @@ def _get_empty_positions(self): """ Returns all the empty positions(points) in the board. """ - return [i.get_index(i.coord) for i in self.points if i.state.name == 'E'] + return [i.get_index(i.coord) for i in self.points if i.state.name == "E"] def _is_closed(self, position): """ @@ -440,9 +497,19 @@ def _is_closed(self, position): can access the empty position by capturing. """ - all_goat_neighbours = any([not self.points[i].state.name in {'T', 'E'} for i in self._move_connections[position]]) + all_goat_neighbours = any( + [ + not self.points[i].state.name in {"T", "E"} + for i in self._move_connections[position] + ] + ) - capture_tiger_present = any([self.points[i].state.name == 'T' for i in self._capture_connections[position]]) + capture_tiger_present = any( + [ + self.points[i].state.name == "T" + for i in self._capture_connections[position] + ] + ) return all_goat_neighbours and not capture_tiger_present diff --git a/Engine.py b/Engine.py index e9102a8..0314f24 100644 --- a/Engine.py +++ b/Engine.py @@ -21,8 +21,12 @@ def evaluate(self, depth=0): """ winner = self.board.winner if not winner: - return 300 * self.board.movable_tigers() + 700 * self.board.deadGoats\ - - 700 * self.board.no_of_closed_spaces - depth + return ( + 300 * self.board.movable_tigers() + + 700 * self.board.deadGoats + - 700 * self.board.no_of_closed_spaces + - depth + ) if winner == Board.Player.G: return -Engine.INF @@ -48,7 +52,6 @@ def minmax(self, is_max=True, depth=0, alpha=-INF, beta=INF): beta = min(beta, value_t) - if value_t < value: value = value_t beta = min(beta, value) @@ -79,8 +82,6 @@ def minmax(self, is_max=True, depth=0, alpha=-INF, beta=INF): if depth == 0: self.best_move = move - - # then revert the move self.board.revert_move(move) diff --git a/Game.py b/Game.py index 18ef646..a553d53 100644 --- a/Game.py +++ b/Game.py @@ -11,7 +11,7 @@ class Game(object): def __init__(self, position=None): super(Game, self).__init__() self.board = Board(position) - self.engine = Engine(self.board, depth=7) + self.engine = Engine(self.board, depth=5) def input_move(self): idx = None diff --git a/api.py b/api.py new file mode 100644 index 0000000..8cb8065 --- /dev/null +++ b/api.py @@ -0,0 +1,68 @@ +from Board import Board +from Engine import Engine + +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Position(BaseModel): + position: str + + +def get_board(position): + """ + Return True if the board position is valid. + """ + + # given a FEN create a board + try: + board = Board(position=position) + except: + return False + + return board + + +def is_winner(board): + """ + Return True if the board position already has a winner. + """ + + # check if the board position is a winner + if board.winner: + return board.winner.name + + return False + + +def board_position_after_best_move(board): + """ + Return the best move for the current player given the board position. + """ + + # find the best move + engine = Engine(board) + move = engine.get_best_move() + + # make the best move on the board + board.make_move(move) + + # return FEN from the board + return board.position + + +@app.post("/api/v1/position/") +def play(position: Position): + board = get_board(position.position) + if not board: + return {"error": "Invalid position"} + + if is_winner(board): + return {"winner": is_winner(board), "new_position": None} + + new_position = board_position_after_best_move(board) + return {"new_position": new_position} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a2ff37c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +fastapi==0.78.0 +uvicorn==0.18.2