From 5f28800dbbb8f27daa60bdb5e49f79db44a8b731 Mon Sep 17 00:00:00 2001 From: Nemo D'ACREMONT Date: Wed, 7 May 2025 21:32:52 +0200 Subject: [PATCH] feat: add chess --- chess/.gitignore | 3 + chess/.gitkeep | 0 chess/README.md | 8 ++ chess/heuristics.py | 49 ++++++++++ chess/player.py | 216 +++++++++++++++++++++++++++++++++++++++++ chess/requirements.txt | 1 + chess/starter-chess.py | 80 +++++++++++++++ go_ia/.gitignore | 3 + go_ia/.gitkeep | 0 go_player/.gitignore | 3 + go_player/.gitkeep | 0 11 files changed, 363 insertions(+) create mode 100644 chess/.gitignore create mode 100644 chess/.gitkeep create mode 100644 chess/README.md create mode 100644 chess/heuristics.py create mode 100644 chess/player.py create mode 100644 chess/requirements.txt create mode 100755 chess/starter-chess.py create mode 100644 go_ia/.gitignore create mode 100644 go_ia/.gitkeep create mode 100644 go_player/.gitignore create mode 100644 go_player/.gitkeep diff --git a/chess/.gitignore b/chess/.gitignore new file mode 100644 index 0000000..48d6580 --- /dev/null +++ b/chess/.gitignore @@ -0,0 +1,3 @@ +__pycache__ +/venv +/.venv diff --git a/chess/.gitkeep b/chess/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/chess/README.md b/chess/README.md new file mode 100644 index 0000000..0d27180 --- /dev/null +++ b/chess/README.md @@ -0,0 +1,8 @@ +# Chess TP + +## VENV ACTIVATE + +``` +python -m venv venv +. ./venv/bin/activate +``` diff --git a/chess/heuristics.py b/chess/heuristics.py new file mode 100644 index 0000000..861c005 --- /dev/null +++ b/chess/heuristics.py @@ -0,0 +1,49 @@ +import chess +import math + +def b(board: chess.Board, color: chess.Color): + piece_values = { + chess.PAWN: 1, + chess.KNIGHT: 3, + chess.BISHOP: 3, + chess.ROOK: 5, + chess.QUEEN: 9, + chess.KING: 0 + } + + score = 0 + for square in chess.SQUARES: + piece = board.piece_at(square) + if piece is not None: + if piece.color == chess.WHITE: + score += piece_values[piece.piece_type] + else: + score -= piece_values[piece.piece_type] + return score + +def h_shannon(board: chess.Board, color: chess.Color): + piece_values = { + chess.PAWN: 1, + chess.KNIGHT: 3, + chess.BISHOP: 3, + chess.ROOK: 5, + chess.QUEEN: 9, + chess.KING: 0 + } + acc = 0 + + outcome = board.outcome() + if board.is_checkmate() and outcome is not None: + winner = outcome.winner + return math.inf if winner is not None and winner == color else -math.inf + + if board.is_game_over(): + return 0 + + for square in board.piece_map(): + piece = board.piece_at(square) + if piece is not None: + n = 1 if piece.color == color else -1 + acc += n * piece_values[piece.piece_type] + + return acc diff --git a/chess/player.py b/chess/player.py new file mode 100644 index 0000000..2a20e3f --- /dev/null +++ b/chess/player.py @@ -0,0 +1,216 @@ +from random import randint, choice +import math +from typing import Callable +import chess +from heuristics import h_shannon + + +class PlayerInterface: + board: chess.Board + color: chess.Color + + # Returns your player name , as to be displayed during the game + def getPlayerName(self) -> str: + return " Not Defined " + + # Returns your move. The move must be a valid string of coordinates ("A1", + # "D5", ...) on the grid or the special "PASS" move. A couple of two integers , + # which are the coordinates of where you want to put your piece on the board . + # Coordinates are the coordinates given by the Goban . py method legal_moves (). + def getPlayerMove(self) -> chess.Move: + return chess.Move.null() + + # Inform you that the oponent has played this move. You must play it with no + # search (just update your local variables to take it into account) + def playOpponentMove(self, move): + self.board.push(move) + + # Starts a new game , and give you your color . As defined in Goban . py : color =1 + # for BLACK , and color =2 for WHITE + def newGame(self, color): + self.board = chess.Board() + self.color = color + + # You can get a feedback on the winner + # This function gives you the color of the winner + def endGame(self, color): + pass + + +class RandomPlayer(PlayerInterface): + # Returns your player name , as to be displayed during the game + def getPlayerName(self) -> str: + return "Random Player" + + def playOpponentMove(self, move): + self.board.push(move) + + def getPlayerMove(self): + """ + Renvoie un mouvement au hasard sur la liste des mouvements possibles. Pour avoir un choix au hasard, il faut + construire explicitement tous les mouvements. Or, generate_legal_moves() nous donne un itérateur. + """ + move = choice([m for m in self.board.generate_legal_moves()]) + self.board.push(move) + return move + + +def minmax( + board: chess.Board, + heuristic: Callable[[chess.Board, chess.Color], float], + color: chess.Color, + depth: int = 3, +) -> chess.Move: + def aux( + board: chess.Board, + heuristic: Callable[[chess.Board, chess.Color], float], + color: chess.Color, + depth: int = 3, + ) -> tuple[float, chess.Move]: + wantMax = board.turn == color + + if depth == 0 or board.is_game_over(): + return heuristic(board, color), board.peek() + + if wantMax: + acc = -math.inf, chess.Move.null() + for move in board.generate_legal_moves(): + board.push(move) + acc = max( + acc, + ( + aux(board, heuristic=heuristic, color=color, depth=depth - 1)[ + 0 + ], + move, + ), + key=lambda t: t[0], + ) + board.pop() + + else: # minimizing player + acc = math.inf, chess.Move.null() + for move in board.generate_legal_moves(): + board.push(move) + acc = min( + acc, + ( + aux(board, heuristic=heuristic, color=color, depth=depth - 1)[ + 0 + ], + move, + ), + key=lambda t: t[0], + ) + board.pop() + + return acc + + _, move = aux(board, heuristic, color, depth) + return move + + +class MinMaxPlayer(PlayerInterface): + depth = 3 + + # Returns your player name , as to be displayed during the game + def getPlayerName(self) -> str: + return "MinMax Player" + + def getPlayerMove(self): + move = minmax( + self.board, heuristic=h_shannon, color=self.color, depth=self.depth + ) + self.board.push(move) + return move + + +def alphabeta( + board: chess.Board, + heuristic: Callable[[chess.Board, chess.Color], float], + color: chess.Color, + depth: int = 3, +) -> chess.Move: + def aux( + board: chess.Board, + heuristic: Callable[[chess.Board, chess.Color], float], + color: chess.Color, + alpha=-math.inf, + beta=math.inf, + depth: int = 3, + ) -> tuple[float, chess.Move]: + wantMax = board.turn == color + if depth == 0 or board.is_game_over(): + return heuristic(board, color), board.peek() + + if wantMax: + acc = -math.inf, chess.Move.null() + for move in board.generate_legal_moves(): + board.push(move) + value = ( + aux( + board, + alpha=alpha, + beta=beta, + heuristic=heuristic, + color=color, + depth=depth - 1, + )[0], + move, + ) + acc = max( + acc, + value, + key=lambda t: t[0], + ) + board.pop() + + if acc[0] >= beta: + break # beta cutoff + alpha = max(alpha, value[0]) + + else: + acc = math.inf, chess.Move.null() + for move in board.generate_legal_moves(): + board.push(move) + value = ( + aux( + board, + alpha=alpha, + beta=beta, + heuristic=heuristic, + color=color, + depth=depth - 1, + )[0], + move, + ) + acc = min( + acc, + value, + key=lambda t: t[0], + ) + board.pop() + + if acc[0] <= alpha: + break # alpha cutoff + beta = min(beta, value[0]) + + return acc + + _, move = aux(board, heuristic=heuristic, color=color, depth=depth) + return move + + +class AlphaBetaPlayer(PlayerInterface): + depth = 5 + + # Returns your player name , as to be displayed during the game + def getPlayerName(self) -> str: + return "AlphaBeta Player" + + def getPlayerMove(self): + move = alphabeta( + self.board, heuristic=h_shannon, color=self.color, depth=self.depth + ) + self.board.push(move) + return move diff --git a/chess/requirements.txt b/chess/requirements.txt new file mode 100644 index 0000000..7b05f41 --- /dev/null +++ b/chess/requirements.txt @@ -0,0 +1 @@ +chess diff --git a/chess/starter-chess.py b/chess/starter-chess.py new file mode 100755 index 0000000..b163276 --- /dev/null +++ b/chess/starter-chess.py @@ -0,0 +1,80 @@ +#!python +import sys +import time +import chess +import player + +def color2str(color: chess.Color): + return "WHITE" if color is chess.WHITE else "BLACK" + +def play(white: player.PlayerInterface, black: player.PlayerInterface): + """ + Déroulement d'une partie d'échecs au hasard des coups possibles. Cela va donner presque exclusivement + des parties très longues et sans gagnant. Cela illustre cependant comment on peut jouer avec la librairie + très simplement. + """ + def step(board: chess.Board, current: player.PlayerInterface, opponent: player.PlayerInterface): + move = current.getPlayerMove() + assert move is not chess.Move.null() + board.push(move) + opponent.playOpponentMove(move) + + return move + + board = chess.Board() + white.newGame(chess.WHITE) + black.newGame(chess.BLACK) + + i = 1 + while not board.is_game_over(): + try: + if board.turn == chess.WHITE: + move = step(board, white, black) + else: + move = step(board, black, white) + except: + print(board) + print(f"{color2str(board.turn)} cheated, end the game") + print(sys.exception()) + break; + + print("----------------") + print(board) + print(f"Move {move}") + print(f"Turn {i}") + i += 1 + + print(board.outcome()) + + return board + +def enumerate_moves(board: chess.Board, depth=1): + if depth == 0: + return [] + boards = [] + + for move in board.generate_legal_moves(): + board.push(move) + + boards.append(board) + boards += enumerate_moves(board, depth - 1) + + board.pop() + + return boards + + +def t_enum(board, depth): + st = time.time() + n = len(enumerate_moves(board, depth=depth)) + nd = time.time() + print(f"{nd-st:.6f} {n}") + + +if __name__ == "__main__": + black = player.RandomPlayer() + white = player.AlphaBetaPlayer() + + board = play(white, black) + print(board) + diff --git a/go_ia/.gitignore b/go_ia/.gitignore new file mode 100644 index 0000000..48d6580 --- /dev/null +++ b/go_ia/.gitignore @@ -0,0 +1,3 @@ +__pycache__ +/venv +/.venv diff --git a/go_ia/.gitkeep b/go_ia/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/go_player/.gitignore b/go_player/.gitignore new file mode 100644 index 0000000..48d6580 --- /dev/null +++ b/go_player/.gitignore @@ -0,0 +1,3 @@ +__pycache__ +/venv +/.venv diff --git a/go_player/.gitkeep b/go_player/.gitkeep new file mode 100644 index 0000000..e69de29