diff --git a/src/ai/ai.py b/src/ai/ai.py new file mode 100644 index 0000000..ee2fc1a --- /dev/null +++ b/src/ai/ai.py @@ -0,0 +1,145 @@ +from collections import defaultdict +import time + +from tqdm import tqdm +from logic.board import INITIAL_BOARD, Board +from logic.move import Move +from logic.pieces.piece import Colour + +pos2expected = { + # -- Position 1 + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1": { + 1: 20, + 2: 400, + 3: 8_902, + 4: 197_281, + }, + + # -- Position 2 + "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1": { + 1: 48, + 2: 2_039, + 3: 97_862, + 4: 4_085_603, + }, + # pos 2 after f3f6 + "r3k2r/p1ppqpb1/bn2pQp1/3PN3/1p2P3/2N4p/PPPBBPPP/R3K2R b KQkq - 0 1": { + 2: 2_111, + 3: 77_838, + }, + # pos 2 after f3f6 and e7d8 + "r2qk2r/p1pp1pb1/bn2pQp1/3PN3/1p2P3/2N4p/PPPBBPPP/R3K2R w KQkq - 1 2": { + 2: 1843, + }, +} + +tick = "\033[92m✔️\033[0m" # Green Tick +cross = "\033[91m❌\033[0m" # Red Cross + +res = defaultdict(lambda : 0) + +def peft(pos: str): + global res + expected = pos2expected[pos] + board = Board.setup_FEN_position(pos) + for depth in expected: + with tqdm(total=expected[depth], desc=f"Depth: {depth}") as bar: + start = time.process_time() + moves = move_generation_test(bar, board, depth, depth) + bar.close() + elapsed = time.process_time() - start + elapsed *= 1_000 + + print("Depth:", depth, end=" ") + print("Result:", moves, end=" ") + if moves == expected[depth]: + print(f"{tick}", end=" ") + else: + print(f"{cross} (expected {expected[depth]})", end=" ") + print("positions Time:", int(elapsed), "milliseconds") + + if moves != expected[depth]: + print() + for key, value in res.items(): + print(f"{key}: {value}") + + + +def move_generation_test(bar, board: Board, depth: int, max_depth, first = None): + global res + if first is None: + res = defaultdict(lambda : 0) + + if board.is_terminal(): + # bar.update(1) + # res[first] += 1 + return 0 + + if depth == 0: + res[first] += 1 + bar.update(1) + return 1 + + moves = board.legal_moves() + if depth == 1: + res[first] += len(moves) + bar.update(len(moves)) + return len(moves) + + num_pos = 0 + for move in moves: + tmp_board = board.make_move(move) + if first is None: + first = move.piece.pos.to_algebraic() + move.pos.to_algebraic() + + if first == "f7h8": + print(tmp_board.legal_moves()) + num_pos += move_generation_test(bar, tmp_board, depth - 1, max_depth, first = first) + + if depth == max_depth: + first = None + + return num_pos + + + +def play_game(board: Board, strategies: dict, verbose: bool = False) -> Board: + """Play a turn-taking game. `strategies` is a {player_name: function} dict, + where function(state) is used to get the player's move.""" + state = board + move_counter = 1 + while not (board.is_checkmate_for(board._turn) or board.is_stalemate_for(board._turn)): + player = board._turn + move = strategies[player](state) + state = board.make_move(move) + if verbose: + if player == Colour.WHITE: + print(str(move_counter) + ".", move, end=" ") + else: + print(move) + + return state + +def minmax_search(state: Board) -> tuple[float, Move]: + """Search game tree to determine best move; return (value, move) pair.""" + return _max_value(state) if state._turn == Colour.WHITE else _min_value(state) + +def _max_value(state: Board) -> tuple[float, Move]: + if state.is_terminal(): + return state.utility(), None + v, move = -float("inf"), None + for a in state.legal_moves(): + v2, _ = _min_value(state.make_move(a)) + if v2 > v: + v, move = v2, a + return v, move + +def _min_value(state: Board) -> tuple[float, Move]: + if state.is_terminal(): + return state.utility(), None + v, move = float("inf"), None + for a in state.legal_moves(): + v2, _ = _min_value(state.make_move(a)) + if v2 < v: + v, move = v2, a + return v, move diff --git a/src/logic/board.py b/src/logic/board.py index 00f98f2..48cad17 100644 --- a/src/logic/board.py +++ b/src/logic/board.py @@ -248,6 +248,12 @@ class Board: ret._white_castling_rights.remove(CastleSide.Queen) elif piece.pos.x == 7 and CastleSide.King in ret._white_castling_rights: ret._white_castling_rights.remove(CastleSide.King) + + if move.is_capturing and move.pos.y == 7 and move.pos in self._black and type(self._black[move.pos]) == Rook: + if move.pos.x == 0 and CastleSide.Queen in ret._black_castling_rights: + ret._black_castling_rights.remove(CastleSide.Queen) + elif move.pos.x == 7 and CastleSide.King in ret._black_castling_rights: + ret._black_castling_rights.remove(CastleSide.King) else: if type(piece) == King: ret._black_castling_rights = set() @@ -258,6 +264,11 @@ class Board: elif piece.pos.x == 7 and CastleSide.King in ret._black_castling_rights: ret._black_castling_rights.remove(CastleSide.King) + if move.is_capturing and move.pos.y == 0 and move.pos in self._white and type(self._white[move.pos]) == Rook: + if move.pos.x == 0 and CastleSide.Queen in ret._white_castling_rights: + ret._white_castling_rights.remove(CastleSide.Queen) + elif move.pos.x == 7 and CastleSide.King in ret._white_castling_rights: + ret._white_castling_rights.remove(CastleSide.King) return ret @@ -309,7 +320,7 @@ class Board: ret += " " if self._en_passant_target is not None: - pos = self._en_passant_target.pos + pos = Position(self._en_passant_target.pos.x, self._en_passant_target.pos.y) pos.y += -1 if self._en_passant_target.colour == Colour.WHITE else 1 ret += pos.to_algebraic() else: @@ -322,6 +333,28 @@ class Board: return ret -_fen_pos = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" + + def legal_moves(self) -> list[Move]: + ret = [] + pieces = self._white if self._turn == Colour.WHITE else self._black + for piece in pieces.values(): + ret += piece.legal_moves(self) + return ret + + def is_terminal(self) -> bool: + return self.is_stalemate_for(Colour.WHITE) or self.is_stalemate_for(Colour.BLACK) or self.is_checkmate_for(Colour.WHITE) or self.is_checkmate_for(Colour.BLACK) + + def utility(self) -> int: + if self.is_stalemate_for(Colour.WHITE) or self.is_stalemate_for(Colour.BLACK): + return 0 + + if self.is_checkmate_for(Colour.WHITE): + return 1 + + if self.is_checkmate_for(Colour.BLACK): + return -1 + + raise ValueError("Cannot determine the utility of board become it neither checkmate nor stalemate for either players") + _fen_pos = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" INITIAL_BOARD = Board.setup_FEN_position(_fen_pos) diff --git a/src/logic/move.py b/src/logic/move.py index 8de98e1..10dbf3d 100644 --- a/src/logic/move.py +++ b/src/logic/move.py @@ -32,10 +32,19 @@ class Move: return "O-O-O" ret = "" - if type(self.piece).__name__ != "Pawn": + if type(self.piece).__name__ == "Pawn": + if self.is_capturing: + ret += self.piece.pos.to_algebraic()[0] + ret += "x" + ret += self.pos.to_algebraic() + else: + ret += self.pos.to_algebraic() + else: ret += self.piece.letter().upper() - - ret += str(self.pos) + if self.is_capturing: + ret += "x" + ret += str(self.pos) + return ret def __repr__(self) -> str: diff --git a/src/logic/pieces/bishop.py b/src/logic/pieces/bishop.py index 4099fc1..9bf680c 100644 --- a/src/logic/pieces/bishop.py +++ b/src/logic/pieces/bishop.py @@ -17,7 +17,7 @@ class Bishop(Piece): # looking north west ret.extend(self._look_direction(board, -1, 1)) - if not looking_for_check and board.is_check_for(self.colour): + if not looking_for_check:# and board.is_check_for(self.colour): return self.keep_only_blocking(ret, board) return ret diff --git a/src/logic/pieces/king.py b/src/logic/pieces/king.py index be478e3..ebb2258 100644 --- a/src/logic/pieces/king.py +++ b/src/logic/pieces/king.py @@ -47,7 +47,7 @@ class King(Piece): if CastleSide.Queen in castling_rights: clear = True - for dx in range(1, 3): + for dx in range(1, 4): x = self.pos.x - dx y = self.pos.y @@ -57,7 +57,7 @@ class King(Piece): move = self._move_for_position(board, x, y) board_after_move = board.make_move(move) - if board_after_move.is_check_for(self.colour): + if dx < 3 and board_after_move.is_check_for(self.colour): clear = False break diff --git a/src/logic/pieces/knight.py b/src/logic/pieces/knight.py index 4294ad9..9586618 100644 --- a/src/logic/pieces/knight.py +++ b/src/logic/pieces/knight.py @@ -16,7 +16,7 @@ class Knight(Piece): if move is not None: ret.append(move) - if not looking_for_check and board.is_check_for(self.colour): + if not looking_for_check:# and board.is_check_for(self.colour): return self.keep_only_blocking(ret, board) return ret diff --git a/src/logic/pieces/pawn.py b/src/logic/pieces/pawn.py index 2556607..4634f75 100644 --- a/src/logic/pieces/pawn.py +++ b/src/logic/pieces/pawn.py @@ -71,7 +71,7 @@ class Pawn(Piece): else: ret.append(Move(self, pos, becomes_en_passant_target=dy==2)) - if not looking_for_check and board.is_check_for(self.colour): + if not looking_for_check:# and board.is_check_for(self.colour): return self.keep_only_blocking(ret, board) return ret diff --git a/src/logic/pieces/queen.py b/src/logic/pieces/queen.py index 0626e1d..9abeb33 100644 --- a/src/logic/pieces/queen.py +++ b/src/logic/pieces/queen.py @@ -29,7 +29,7 @@ class Queen(Piece): # looking north ret.extend(self._look_direction(board, 0, 1)) - if not looking_for_check and board.is_check_for(self.colour): + if not looking_for_check:# and board.is_check_for(self.colour): return self.keep_only_blocking(ret, board) return ret diff --git a/src/logic/pieces/rook.py b/src/logic/pieces/rook.py index ce6d6f4..a80936e 100644 --- a/src/logic/pieces/rook.py +++ b/src/logic/pieces/rook.py @@ -17,7 +17,7 @@ class Rook(Piece): # looking north ret.extend(self._look_direction(board, 0, 1)) - if not looking_for_check and board.is_check_for(self.colour): + if not looking_for_check:# and board.is_check_for(self.colour): return self.keep_only_blocking(ret, board) return ret diff --git a/src/main.py b/src/main.py index 4a8bc46..7eb4ca4 100644 --- a/src/main.py +++ b/src/main.py @@ -1,13 +1,27 @@ +import time +from pprint import pprint +from tqdm import tqdm + +from ai.ai import move_generation_test from controller.controller import Controller -from logic.board import INITIAL_BOARD +from logic.board import INITIAL_BOARD, Board +from logic.position import Position from view.gui import GUI from view.tui import TUI +from ai.ai import peft + if __name__ == "__main__": board = INITIAL_BOARD + pos = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1" + board = Board.setup_FEN_position(pos) + view = GUI() controller = Controller(board, view) - view.show() + # view.show() + # exit() + + peft(pos)