diff --git a/chesspp/engine.py b/chesspp/engine.py index 4a7242a..c7f0ace 100644 --- a/chesspp/engine.py +++ b/chesspp/engine.py @@ -15,13 +15,13 @@ from typing import Dict class Limit: """ Class to determine when to stop searching for moves """ - time: float|None + time: float | None """ Search for `time` seconds """ - nodes: int|None + nodes: int | None """ Search for a limited number of `nodes`""" - def __init__(self, time: float|None = None, nodes: int|None = None): + def __init__(self, time: float | None = None, nodes: int | None = None): self.time = time self.nodes = nodes @@ -44,9 +44,15 @@ class Limit: def _run_time(self, func, *args, **kwargs): start = time.perf_counter_ns() - while (time.perf_counter_ns()-start)/1e9 < self.time: + while (time.perf_counter_ns() - start) / 1e9 < self.time: func(*args, **kwargs) + def translate_to_engine_limit(self) -> chess.engine.Limit: + if self.nodes: + return chess.engine.Limit(nodes=self.nodes) + elif self.time: + return chess.engine.Limit(time=self.time) + class Engine(ABC): board: chess.Board @@ -56,7 +62,7 @@ class Engine(ABC): strategy: IStrategy """The strategy used to pick moves when simulating games.""" - def __init__(self, board: chess.Board, color: chess.Color, strategy: IStrategy): + def __init__(self, board: chess.Board, color: chess.Color, strategy: IStrategy | None): self.board = board self.color = color self.strategy = strategy @@ -138,3 +144,29 @@ class RandomEngine(Engine): def play(self, board: chess.Board, limit: Limit) -> chess.engine.PlayResult: move = random.choice(list(board.legal_moves)) return chess.engine.PlayResult(move=move, ponder=None) + + +class StockFishEngine(Engine): + def __init__(self, board: chess.Board, color: chess, path="../stockfish/stockfish-ubuntu-x86-64-avx2"): + super().__init__(board, color, None) + self.stockfish = chess.engine.SimpleEngine.popen_uci(path) + + def play(self, board: chess.Board, limit: Limit) -> chess.engine.PlayResult: + return self.stockfish.play(board, limit.translate_to_engine_limit()) + + @staticmethod + def get_name() -> str: + return "Stockfish" + + +class Lc0Engine(Engine): + def __init__(self, board: chess.Board, color: chess, path="../lc0/lc0"): + super().__init__(board, color, None) + self.lc0 = chess.engine.SimpleEngine.popen_uci(path) + + def play(self, board: chess.Board, limit: Limit) -> chess.engine.PlayResult: + return self.lc0.play(board, limit.translate_to_engine_limit()) + + @staticmethod + def get_name() -> str: + return "Lc0" diff --git a/chesspp/engine_factory.py b/chesspp/engine_factory.py new file mode 100644 index 0000000..53d9ec6 --- /dev/null +++ b/chesspp/engine_factory.py @@ -0,0 +1,93 @@ +from enum import Enum + +from chesspp.engine import * +from chesspp.lc0_strategy import Lc0Strategy +from chesspp.random_strategy import RandomStrategy +from chesspp.stockfish_strategy import StockFishStrategy +from chesspp.random_stockfish_strategy import RandomStockfishStrategy +from chesspp.pesto_strategy import PestoStrategy +from chesspp.i_strategy import IStrategy +import chess + + +class EngineEnum(Enum): + ClassicMcts = 0 + BayesianMcts = 1 + Stockfish = 2 + Lc0 = 3 + Random = 4 + + +class StrategyEnum(Enum): + Stockfish = 0 + Lc0 = 1 + Random = 2 + RandomStockfish = 3 + Pestos = 4 + + +class EngineFactory: + + @staticmethod + def create_engine(engine_name: EngineEnum, strategy_name: StrategyEnum, color: chess.Color, stockfish_path: str, lc0_path: str, rollout_depth: int = 4) -> Engine: + match strategy_name: + case StrategyEnum.Stockfish: + strategy = EngineFactory._get_stockfish_strategy(stockfish_path, rollout_depth) + case StrategyEnum.Lc0: + strategy = EngineFactory._get_lc0_strategy(lc0_path, rollout_depth) + case StrategyEnum.Random: + strategy = EngineFactory._get_random_strategy(rollout_depth) + case StrategyEnum.RandomStockfish: + strategy = EngineFactory._get_random_stockfish_strategy(stockfish_path, rollout_depth) + case StrategyEnum.Pestos: + strategy = EngineFactory._get_pesto_strategy(rollout_depth) + + match engine_name: + case EngineEnum.ClassicMcts: + return EngineFactory.classic_mcts(color, strategy) + + case EngineEnum.BayesianMcts: + return EngineFactory.bayesian_mcts(color, strategy) + + case EngineEnum.Stockfish: + return EngineFactory.stockfish_engine(color, stockfish_path) + + case EngineEnum.Lc0: + return EngineFactory.lc0_engine(color, lc0_path) + + @staticmethod + def stockfish_engine(color: chess.Color, engine_path: str, board: chess.Board | None = chess.Board()) -> Engine: + return StockFishEngine(board, color, engine_path) + + @staticmethod + def lc0_engine(color: chess.Color, engine_path: str, board: chess.Board | None = chess.Board()) -> Engine: + return Lc0Engine(board, color, engine_path) + + @staticmethod + def bayesian_mcts(color: chess.Color, strategy: IStrategy, board: chess.Board | None = chess.Board()) -> Engine: + return BayesMctsEngine(board, color, strategy) + + @staticmethod + def classic_mcts(color: chess.Color, strategy: IStrategy, board: chess.Board | None = chess.Board()) -> Engine: + return ClassicMctsEngine(board, color, strategy) + + @staticmethod + def _get_random_strategy(rollout_depth: int) -> IStrategy: + return RandomStrategy(random.Random(), rollout_depth) + + @staticmethod + def _get_stockfish_strategy(engine_path: str, rollout_depth: int) -> IStrategy: + return StockFishStrategy(engine_path, rollout_depth) + + @staticmethod + def _get_random_stockfish_strategy(engine_path: str, rollout_depth: int) -> IStrategy: + return RandomStockfishStrategy(rollout_depth, engine_path) + + @staticmethod + def _get_lc0_strategy(engine_path: str, rollout_depth: int) -> IStrategy: + return Lc0Strategy(engine_path, rollout_depth) + + @staticmethod + def _get_pesto_strategy(rollout_depth: int) -> IStrategy: + return PestoStrategy(rollout_depth) + diff --git a/chesspp/eval.py b/chesspp/eval.py index 375b1f5..ab171a7 100644 --- a/chesspp/eval.py +++ b/chesspp/eval.py @@ -76,12 +76,12 @@ king_eval = [ ] king_endgame_eval = [ 50, -30, -30, -30, -30, -30, -30, -50, - -30, -30, 0, 0, 0, 0, -30, -30, + -30, -30, 0, 0, 0, 0, -30, -30, -30, -10, 20, 30, 30, 20, -10, -30, -30, -10, 30, 40, 40, 30, -10, -30, -30, -10, 30, 40, 40, 30, -10, -30, -30, -10, 20, 30, 30, 20, -10, -30, - -30, -20, -10, 0, 0, -10, -20, -30, + -30, -20, -10, 0, 0, -10, -20, -30, -50, -40, -30, -20, -20, -30, -40, -50 ] @@ -124,18 +124,18 @@ def check_endgame(board: chess.Board) -> bool: if piece.piece_type == chess.QUEEN: if piece.color == chess.WHITE: - queens_white += 1 + queens_white += 1 else: queens_black += 1 if piece.piece_type == chess.BISHOP or piece.piece_type == chess.KNIGHT: if piece.color == chess.WHITE: - minors_white += 1 + minors_white += 1 else: minors_black += 1 return (queens_black == 0 and queens_white == 0) or ((queens_black >= 1 >= minors_black) or ( - queens_white >= 1 >= minors_white)) + queens_white >= 1 >= minors_white)) def score_manual(board: chess.Board) -> int: @@ -173,30 +173,38 @@ def score_manual(board: chess.Board) -> int: return score -def score_stockfish(board: chess.Board, stockfish: chess.engine.SimpleEngine | None = None) -> int: +def score_stockfish(board: chess.Board, stockfish: chess.engine.SimpleEngine | None = None, + limit: chess.engine.Limit = chess.engine.Limit(depth=0)) -> int: """ Calculate the score of the given board using stockfish :param board: + :param stockfish: + :param limit: :return: """ if stockfish is None: engine = chess.engine.SimpleEngine.popen_uci( "/home/luke/projects/pp-project/chess-engine-pp/stockfish/stockfish-ubuntu-x86-64-avx2") - info = engine.analyse(board, chess.engine.Limit(depth=0)) + info = engine.analyse(board, limit) engine.quit() return info['score'].white().score(mate_score=100_000) else: - info = stockfish.analyse(board, chess.engine.Limit(depth=0)) + info = stockfish.analyse(board, limit) return info['score'].white().score(mate_score=100_000) -def score_lc0(board: chess.Board) -> chess.engine.PovScore: +def score_lc0(board: chess.Board, lc0: chess.engine.SimpleEngine | None = None, + limit: chess.engine.Limit= chess.engine.Limit(depth=0)) -> int: """ Calculate the score of the given board using lc0 :param board: :return: """ - engine = chess.engine.SimpleEngine.popen_uci("/home/luke/projects/pp-project/chess-engine-pp/lc0/lc0") - info = engine.analyse(board, chess.engine.Limit(depth=4)) - engine.quit() - return info["score"] + if lc0 is None: + engine = chess.engine.SimpleEngine.popen_uci("/home/luke/projects/pp-project/chess-engine-pp/lc0/lc0") + info = engine.analyse(board, limit) + engine.quit() + return info["score"] + else: + info = lc0.analyse(board, limit) + return info['score'].white().score(mate_score=100_000) diff --git a/chesspp/i_strategy.py b/chesspp/i_strategy.py index d985b79..b37b0cb 100644 --- a/chesspp/i_strategy.py +++ b/chesspp/i_strategy.py @@ -3,8 +3,11 @@ from abc import ABC, abstractmethod import chess -# TODO extend class class IStrategy(ABC): + rollout_depth: int + + def __init__(self, rollout_depth: int = 4): + self.rollout_depth = rollout_depth @abstractmethod def pick_next_move(self, board: chess.Board) -> chess.Move: diff --git a/chesspp/lc0_strategy.py b/chesspp/lc0_strategy.py new file mode 100644 index 0000000..d12d746 --- /dev/null +++ b/chesspp/lc0_strategy.py @@ -0,0 +1,38 @@ +import chess +import chess.engine +import os + +from chesspp.i_strategy import IStrategy +from chesspp.eval import score_lc0 + +_DIR = os.path.abspath(os.path.dirname(__file__)) + + +class Lc0Strategy(IStrategy): + def __init__(self, path="lc0/lc0", rollout_depth: int = 4, + limit: chess.engine.Limit = chess.engine.Limit(depth=4)): + super().__init__(rollout_depth) + self._lc0 = None + self.path = path + self.limit = limit + + def __del__(self): + if self._lc0 is not None: + self._lc0.quit() + + @property + def lc0(self) -> chess.engine.SimpleEngine: + if self._lc0 is None: + self._lc0 = self.lc0 = chess.engine.SimpleEngine.popen_uci(self.path) + return self._lc0 + + @lc0.setter + def lc0(self, value): + self._lc0 = value + + def pick_next_move(self, board: chess.Board) -> chess.Move | None: + return self.lc0.play(board, self.limit).move + + def analyze_board(self, board: chess.Board) -> int: + score = score_lc0(board, self.lc0) + return score diff --git a/chesspp/pesto_strategy.py b/chesspp/pesto_strategy.py new file mode 100644 index 0000000..bedce6e --- /dev/null +++ b/chesspp/pesto_strategy.py @@ -0,0 +1,339 @@ +import chess.engine +import chess +from functools import cache + +from chesspp.i_strategy import IStrategy +import numba + +# Scoring based on PeSTO (Piece-Square Tables Only) Evaluation Functions +# https://www.chessprogramming.org/PeSTO%27s_Evaluation_Function + + + +PAWN = 0 +KNIGHT = 1 +BISHOP = 2 +ROOK = 3 +QUEEN = 4 +KING = 5 + +# board representation +WHITE = 0 +BLACK = 1 + +WHITE_PAWN = (2*PAWN + WHITE) +BLACK_PAWN = (2*PAWN + BLACK) +WHITE_KNIGHT = (2*KNIGHT + WHITE) +BLACK_KNIGHT = (2*KNIGHT + BLACK) +WHITE_BISHOP = (2*BISHOP + WHITE) +BLACK_BISHOP = (2*BISHOP + BLACK) +WHITE_ROOK = (2*ROOK + WHITE) +BLACK_ROOK = (2*ROOK + BLACK) +WHITE_QUEEN = (2*QUEEN + WHITE) +BLACK_QUEEN = (2*QUEEN + BLACK) +WHITE_KING = (2*KING + WHITE) +BLACK_KING = (2*KING + BLACK) +EMPTY = (BLACK_KING + 1) + +mg_value = [82, 337, 365, 477, 1025, 0] +eg_value = [94, 281, 297, 512, 936, 0] + + +FLIP = lambda sq: (sq^56) +OTHER = lambda side: (side^1) + + +mg_pawn_table = [ + 0, 0, 0, 0, 0, 0, 0, 0, + 98, 134, 61, 95, 68, 126, 34, -11, + -6, 7, 26, 31, 65, 56, 25, -20, + -14, 13, 6, 21, 23, 12, 17, -23, + -27, -2, -5, 12, 17, 6, 10, -25, + -26, -4, -4, -10, 3, 3, 33, -12, + -35, -1, -20, -23, -15, 24, 38, -22, + 0, 0, 0, 0, 0, 0, 0, 0, +] + +eg_pawn_table = [ + 0, 0, 0, 0, 0, 0, 0, 0, + 178, 173, 158, 134, 147, 132, 165, 187, + 94, 100, 85, 67, 56, 53, 82, 84, + 32, 24, 13, 5, -2, 4, 17, 17, + 13, 9, -3, -7, -7, -8, 3, -1, + 4, 7, -6, 1, 0, -5, -1, -8, + 13, 8, 8, 10, 13, 0, 2, -7, + 0, 0, 0, 0, 0, 0, 0, 0, +] + +mg_knight_table = [ + -167, -89, -34, -49, 61, -97, -15, -107, + -73, -41, 72, 36, 23, 62, 7, -17, + -47, 60, 37, 65, 84, 129, 73, 44, + -9, 17, 19, 53, 37, 69, 18, 22, + -13, 4, 16, 13, 28, 19, 21, -8, + -23, -9, 12, 10, 19, 17, 25, -16, + -29, -53, -12, -3, -1, 18, -14, -19, + -105, -21, -58, -33, -17, -28, -19, -23, +] + +eg_knight_table = [ + -58, -38, -13, -28, -31, -27, -63, -99, + -25, -8, -25, -2, -9, -25, -24, -52, + -24, -20, 10, 9, -1, -9, -19, -41, + -17, 3, 22, 22, 22, 11, 8, -18, + -18, -6, 16, 25, 16, 17, 4, -18, + -23, -3, -1, 15, 10, -3, -20, -22, + -42, -20, -10, -5, -2, -20, -23, -44, + -29, -51, -23, -15, -22, -18, -50, -64, +] + +mg_bishop_table = [ + -29, 4, -82, -37, -25, -42, 7, -8, + -26, 16, -18, -13, 30, 59, 18, -47, + -16, 37, 43, 40, 35, 50, 37, -2, + -4, 5, 19, 50, 37, 37, 7, -2, + -6, 13, 13, 26, 34, 12, 10, 4, + 0, 15, 15, 15, 14, 27, 18, 10, + 4, 15, 16, 0, 7, 21, 33, 1, + -33, -3, -14, -21, -13, -12, -39, -21, +] + +eg_bishop_table = [ + -14, -21, -11, -8, -7, -9, -17, -24, + -8, -4, 7, -12, -3, -13, -4, -14, + 2, -8, 0, -1, -2, 6, 0, 4, + -3, 9, 12, 9, 14, 10, 3, 2, + -6, 3, 13, 19, 7, 10, -3, -9, + -12, -3, 8, 10, 13, 3, -7, -15, + -14, -18, -7, -1, 4, -9, -15, -27, + -23, -9, -23, -5, -9, -16, -5, -17, +] + +mg_rook_table = [ + 32, 42, 32, 51, 63, 9, 31, 43, + 27, 32, 58, 62, 80, 67, 26, 44, + -5, 19, 26, 36, 17, 45, 61, 16, + -24, -11, 7, 26, 24, 35, -8, -20, + -36, -26, -12, -1, 9, -7, 6, -23, + -45, -25, -16, -17, 3, 0, -5, -33, + -44, -16, -20, -9, -1, 11, -6, -71, + -19, -13, 1, 17, 16, 7, -37, -26, +] + +eg_rook_table = [ + 13, 10, 18, 15, 12, 12, 8, 5, + 11, 13, 13, 11, -3, 3, 8, 3, + 7, 7, 7, 5, 4, -3, -5, -3, + 4, 3, 13, 1, 2, 1, -1, 2, + 3, 5, 8, 4, -5, -6, -8, -11, + -4, 0, -5, -1, -7, -12, -8, -16, + -6, -6, 0, 2, -9, -9, -11, -3, + -9, 2, 3, -1, -5, -13, 4, -20, +] + +mg_queen_table = [ + -28, 0, 29, 12, 59, 44, 43, 45, + -24, -39, -5, 1, -16, 57, 28, 54, + -13, -17, 7, 8, 29, 56, 47, 57, + -27, -27, -16, -16, -1, 17, -2, 1, + -9, -26, -9, -10, -2, -4, 3, -3, + -14, 2, -11, -2, -5, 2, 14, 5, + -35, -8, 11, 2, 8, 15, -3, 1, + -1, -18, -9, 10, -15, -25, -31, -50, +] + +eg_queen_table = [ + -9, 22, 22, 27, 27, 19, 10, 20, + -17, 20, 32, 41, 58, 25, 30, 0, + -20, 6, 9, 49, 47, 35, 19, 9, + 3, 22, 24, 45, 57, 40, 57, 36, + -18, 28, 19, 47, 31, 34, 39, 23, + -16, -27, 15, 6, 9, 17, 10, 5, + -22, -23, -30, -16, -16, -23, -36, -32, + -33, -28, -22, -43, -5, -32, -20, -41, +] + +mg_king_table = [ + -65, 23, 16, -15, -56, -34, 2, 13, + 29, -1, -20, -7, -8, -4, -38, -29, + -9, 24, 2, -16, -20, 6, 22, -22, + -17, -20, -12, -27, -30, -25, -14, -36, + -49, -1, -27, -39, -46, -44, -33, -51, + -14, -14, -22, -46, -44, -30, -15, -27, + 1, 7, -8, -64, -43, -16, 9, 8, + -15, 36, 12, -54, 8, -28, 24, 14, +] + +eg_king_table = [ + -74, -35, -18, -18, -11, 15, 4, -17, + -12, 17, 14, 17, 17, 38, 23, 11, + 10, 17, 23, 15, 20, 45, 44, 13, + -8, 22, 24, 27, 26, 33, 26, 3, + -18, -4, 21, 24, 27, 23, 9, -11, + -19, -3, 11, 21, 23, 16, 7, -9, + -27, -11, 4, 13, 14, 4, -5, -17, + -53, -34, -21, -11, -28, -14, -24, -43 +] + +mg_pesto_table = [ + mg_pawn_table, + mg_knight_table, + mg_bishop_table, + mg_rook_table, + mg_queen_table, + mg_king_table +] + +eg_pesto_table = [ + eg_pawn_table, + eg_knight_table, + eg_bishop_table, + eg_rook_table, + eg_queen_table, + eg_king_table +] + +gamephaseInc = [0,0,1,1,1,1,2,2,4,4,0,0] + +mg_table = [ + [82, 82, 82, 82, 82, 82, 82, 82, 180, 216, 143, 177, 150, 208, 116, 71, 76, 89, 108, 113, 147, 138, 107, 62, 68, 95, 88, 103, 105, 94, 99, 59, 55, 80, 77, 94, 99, 88, 92, 57, 56, 78, 78, 72, 85, 85, 115, 70, 47, 81, 62, 59, 67, 106, 120, 60, 82, 82, 82, 82, 82, 82, 82, 82, ], + [82, 82, 82, 82, 82, 82, 82, 82, 47, 81, 62, 59, 67, 106, 120, 60, 56, 78, 78, 72, 85, 85, 115, 70, 55, 80, 77, 94, 99, 88, 92, 57, 68, 95, 88, 103, 105, 94, 99, 59, 76, 89, 108, 113, 147, 138, 107, 62, 180, 216, 143, 177, 150, 208, 116, 71, 82, 82, 82, 82, 82, 82, 82, 82, ], + [170, 248, 303, 288, 398, 240, 322, 230, 264, 296, 409, 373, 360, 399, 344, 320, 290, 397, 374, 402, 421, 466, 410, 381, 328, 354, 356, 390, 374, 406, 355, 359, 324, 341, 353, 350, 365, 356, 358, 329, 314, 328, 349, 347, 356, 354, 362, 321, 308, 284, 325, 334, 336, 355, 323, 318, 232, 316, 279, 304, 320, 309, 318, 314, ], + [232, 316, 279, 304, 320, 309, 318, 314, 308, 284, 325, 334, 336, 355, 323, 318, 314, 328, 349, 347, 356, 354, 362, 321, 324, 341, 353, 350, 365, 356, 358, 329, 328, 354, 356, 390, 374, 406, 355, 359, 290, 397, 374, 402, 421, 466, 410, 381, 264, 296, 409, 373, 360, 399, 344, 320, 170, 248, 303, 288, 398, 240, 322, 230, ], + [336, 369, 283, 328, 340, 323, 372, 357, 339, 381, 347, 352, 395, 424, 383, 318, 349, 402, 408, 405, 400, 415, 402, 363, 361, 370, 384, 415, 402, 402, 372, 363, 359, 378, 378, 391, 399, 377, 375, 369, 365, 380, 380, 380, 379, 392, 383, 375, 369, 380, 381, 365, 372, 386, 398, 366, 332, 362, 351, 344, 352, 353, 326, 344, ], + [332, 362, 351, 344, 352, 353, 326, 344, 369, 380, 381, 365, 372, 386, 398, 366, 365, 380, 380, 380, 379, 392, 383, 375, 359, 378, 378, 391, 399, 377, 375, 369, 361, 370, 384, 415, 402, 402, 372, 363, 349, 402, 408, 405, 400, 415, 402, 363, 339, 381, 347, 352, 395, 424, 383, 318, 336, 369, 283, 328, 340, 323, 372, 357, ], + [509, 519, 509, 528, 540, 486, 508, 520, 504, 509, 535, 539, 557, 544, 503, 521, 472, 496, 503, 513, 494, 522, 538, 493, 453, 466, 484, 503, 501, 512, 469, 457, 441, 451, 465, 476, 486, 470, 483, 454, 432, 452, 461, 460, 480, 477, 472, 444, 433, 461, 457, 468, 476, 488, 471, 406, 458, 464, 478, 494, 493, 484, 440, 451, ], + [458, 464, 478, 494, 493, 484, 440, 451, 433, 461, 457, 468, 476, 488, 471, 406, 432, 452, 461, 460, 480, 477, 472, 444, 441, 451, 465, 476, 486, 470, 483, 454, 453, 466, 484, 503, 501, 512, 469, 457, 472, 496, 503, 513, 494, 522, 538, 493, 504, 509, 535, 539, 557, 544, 503, 521, 509, 519, 509, 528, 540, 486, 508, 520, ], + [997, 1025, 1054, 1037, 1084, 1069, 1068, 1070, 1001, 986, 1020, 1026, 1009, 1082, 1053, 1079, 1012, 1008, 1032, 1033, 1054, 1081, 1072, 1082, 998, 998, 1009, 1009, 1024, 1042, 1023, 1026, 1016, 999, 1016, 1015, 1023, 1021, 1028, 1022, 1011, 1027, 1014, 1023, 1020, 1027, 1039, 1030, 990, 1017, 1036, 1027, 1033, 1040, 1022, 1026, 1024, 1007, 1016, 1035, 1010, 1000, 994, 975, ], + [1024, 1007, 1016, 1035, 1010, 1000, 994, 975, 990, 1017, 1036, 1027, 1033, 1040, 1022, 1026, 1011, 1027, 1014, 1023, 1020, 1027, 1039, 1030, 1016, 999, 1016, 1015, 1023, 1021, 1028, 1022, 998, 998, 1009, 1009, 1024, 1042, 1023, 1026, 1012, 1008, 1032, 1033, 1054, 1081, 1072, 1082, 1001, 986, 1020, 1026, 1009, 1082, 1053, 1079, 997, 1025, 1054, 1037, 1084, 1069, 1068, 1070, ], + [-65, 23, 16, -15, -56, -34, 2, 13, 29, -1, -20, -7, -8, -4, -38, -29, -9, 24, 2, -16, -20, 6, 22, -22, -17, -20, -12, -27, -30, -25, -14, -36, -49, -1, -27, -39, -46, -44, -33, -51, -14, -14, -22, -46, -44, -30, -15, -27, 1, 7, -8, -64, -43, -16, 9, 8, -15, 36, 12, -54, 8, -28, 24, 14, ], + [-15, 36, 12, -54, 8, -28, 24, 14, 1, 7, -8, -64, -43, -16, 9, 8, -14, -14, -22, -46, -44, -30, -15, -27, -49, -1, -27, -39, -46, -44, -33, -51, -17, -20, -12, -27, -30, -25, -14, -36, -9, 24, 2, -16, -20, 6, 22, -22, 29, -1, -20, -7, -8, -4, -38, -29, -65, 23, 16, -15, -56, -34, 2, 13, ] +] + +eg_table = [ + [94, 94, 94, 94, 94, 94, 94, 94, 272, 267, 252, 228, 241, 226, 259, 281, 188, 194, 179, 161, 150, 147, 176, 178, 126, 118, 107, 99, 92, 98, 111, 111, 107, 103, 91, 87, 87, 86, 97, 93, 98, 101, 88, 95, 94, 89, 93, 86, 107, 102, 102, 104, 107, 94, 96, 87, 94, 94, 94, 94, 94, 94, 94, 94, ], + [94, 94, 94, 94, 94, 94, 94, 94, 107, 102, 102, 104, 107, 94, 96, 87, 98, 101, 88, 95, 94, 89, 93, 86, 107, 103, 91, 87, 87, 86, 97, 93, 126, 118, 107, 99, 92, 98, 111, 111, 188, 194, 179, 161, 150, 147, 176, 178, 272, 267, 252, 228, 241, 226, 259, 281, 94, 94, 94, 94, 94, 94, 94, 94, ], + [223, 243, 268, 253, 250, 254, 218, 182, 256, 273, 256, 279, 272, 256, 257, 229, 257, 261, 291, 290, 280, 272, 262, 240, 264, 284, 303, 303, 303, 292, 289, 263, 263, 275, 297, 306, 297, 298, 285, 263, 258, 278, 280, 296, 291, 278, 261, 259, 239, 261, 271, 276, 279, 261, 258, 237, 252, 230, 258, 266, 259, 263, 231, 217, ], + [252, 230, 258, 266, 259, 263, 231, 217, 239, 261, 271, 276, 279, 261, 258, 237, 258, 278, 280, 296, 291, 278, 261, 259, 263, 275, 297, 306, 297, 298, 285, 263, 264, 284, 303, 303, 303, 292, 289, 263, 257, 261, 291, 290, 280, 272, 262, 240, 256, 273, 256, 279, 272, 256, 257, 229, 223, 243, 268, 253, 250, 254, 218, 182, ], + [283, 276, 286, 289, 290, 288, 280, 273, 289, 293, 304, 285, 294, 284, 293, 283, 299, 289, 297, 296, 295, 303, 297, 301, 294, 306, 309, 306, 311, 307, 300, 299, 291, 300, 310, 316, 304, 307, 294, 288, 285, 294, 305, 307, 310, 300, 290, 282, 283, 279, 290, 296, 301, 288, 282, 270, 274, 288, 274, 292, 288, 281, 292, 280, ], + [274, 288, 274, 292, 288, 281, 292, 280, 283, 279, 290, 296, 301, 288, 282, 270, 285, 294, 305, 307, 310, 300, 290, 282, 291, 300, 310, 316, 304, 307, 294, 288, 294, 306, 309, 306, 311, 307, 300, 299, 299, 289, 297, 296, 295, 303, 297, 301, 289, 293, 304, 285, 294, 284, 293, 283, 283, 276, 286, 289, 290, 288, 280, 273, ], + [525, 522, 530, 527, 524, 524, 520, 517, 523, 525, 525, 523, 509, 515, 520, 515, 519, 519, 519, 517, 516, 509, 507, 509, 516, 515, 525, 513, 514, 513, 511, 514, 515, 517, 520, 516, 507, 506, 504, 501, 508, 512, 507, 511, 505, 500, 504, 496, 506, 506, 512, 514, 503, 503, 501, 509, 503, 514, 515, 511, 507, 499, 516, 492, ], + [503, 514, 515, 511, 507, 499, 516, 492, 506, 506, 512, 514, 503, 503, 501, 509, 508, 512, 507, 511, 505, 500, 504, 496, 515, 517, 520, 516, 507, 506, 504, 501, 516, 515, 525, 513, 514, 513, 511, 514, 519, 519, 519, 517, 516, 509, 507, 509, 523, 525, 525, 523, 509, 515, 520, 515, 525, 522, 530, 527, 524, 524, 520, 517, ], + [927, 958, 958, 963, 963, 955, 946, 956, 919, 956, 968, 977, 994, 961, 966, 936, 916, 942, 945, 985, 983, 971, 955, 945, 939, 958, 960, 981, 993, 976, 993, 972, 918, 964, 955, 983, 967, 970, 975, 959, 920, 909, 951, 942, 945, 953, 946, 941, 914, 913, 906, 920, 920, 913, 900, 904, 903, 908, 914, 893, 931, 904, 916, 895, ], + [903, 908, 914, 893, 931, 904, 916, 895, 914, 913, 906, 920, 920, 913, 900, 904, 920, 909, 951, 942, 945, 953, 946, 941, 918, 964, 955, 983, 967, 970, 975, 959, 939, 958, 960, 981, 993, 976, 993, 972, 916, 942, 945, 985, 983, 971, 955, 945, 919, 956, 968, 977, 994, 961, 966, 936, 927, 958, 958, 963, 963, 955, 946, 956, ], + [-74, -35, -18, -18, -11, 15, 4, -17, -12, 17, 14, 17, 17, 38, 23, 11, 10, 17, 23, 15, 20, 45, 44, 13, -8, 22, 24, 27, 26, 33, 26, 3, -18, -4, 21, 24, 27, 23, 9, -11, -19, -3, 11, 21, 23, 16, 7, -9, -27, -11, 4, 13, 14, 4, -5, -17, -53, -34, -21, -11, -28, -14, -24, -43, ], + [-53, -34, -21, -11, -28, -14, -24, -43, -27, -11, 4, 13, 14, 4, -5, -17, -19, -3, 11, 21, 23, 16, 7, -9, -18, -4, 21, 24, 27, 23, 9, -11, -8, 22, 24, 27, 26, 33, 26, 3, 10, 17, 23, 15, 20, 45, 44, 13, -12, 17, 14, 17, 17, 38, 23, 11, -74, -35, -18, -18, -11, 15, 4, -17, ] +] + + +def score(board: chess.Board) -> int: + mg = [0, 0] + eg = [0, 0] + game_phase = 0 + + # evaluate each piece + for sq in range(64): + pc = board.piece_at(sq) + if pc is not None: + color = 0 if pc.color == chess.WHITE else 1 + piece_index = (pc.piece_type-1)*2 + color + mg[color] += mg_table[piece_index][sq] + eg[color] += eg_table[piece_index][sq] + game_phase += gamephaseInc[piece_index] + + # tapered eval + side2move = 0 if board.turn == chess.WHITE else 1 + mg_score = mg[side2move] - mg[OTHER(side2move)] + eg_score = eg[side2move] - eg[OTHER(side2move)] + mg_phase = game_phase + if mg_phase > 24: + # in case of early promotion + mg_phase = 24 + eg_phase = 24 - mg_phase + return (mg_score * mg_phase + eg_score * eg_phase) // 24 + +flip = [ + 56, 57, 58, 59, 60, 61, 62, 63, + 48, 49, 50, 51, 52, 53, 54, 55, + 40, 41, 42, 43, 44, 45, 46, 47, + 32, 33, 34, 35, 36, 37, 38, 39, + 24, 25, 26, 27, 28, 29, 30, 31, + 16, 17, 18, 19, 20, 21, 22, 23, + 8, 9, 10, 11, 12, 13, 14, 15, + 0, 1, 2, 3, 4, 5, 6, 7 +] + + +def _init_tables(): + global mg_table, eg_table + + for i in range(64): + mg_table[WHITE_PAWN][i] = mg_pawn_table[i] + mg_table[BLACK_PAWN][i] = mg_pawn_table[flip[i]] + mg_table[WHITE_KNIGHT][i] = mg_knight_table[i] + mg_table[BLACK_KNIGHT][i] = mg_knight_table[flip[i]] + mg_table[WHITE_BISHOP][i] = mg_bishop_table[i] + mg_table[BLACK_BISHOP][i] = mg_bishop_table[flip[i]] + mg_table[WHITE_ROOK][i] = mg_rook_table[i] + mg_table[BLACK_ROOK][i] = mg_rook_table[flip[i]] + mg_table[WHITE_QUEEN][i] = mg_queen_table[i] + mg_table[BLACK_QUEEN][i] = mg_queen_table[flip[i]] + mg_table[WHITE_KING][i] = mg_king_table[i] + mg_table[BLACK_KING][i] = mg_knight_table[flip[i]] + + eg_table[WHITE_PAWN][i] = eg_pawn_table[i] + eg_table[BLACK_PAWN][i] = eg_pawn_table[flip[i]] + eg_table[WHITE_KNIGHT][i] = eg_knight_table[i] + eg_table[BLACK_KNIGHT][i] = eg_knight_table[flip[i]] + eg_table[WHITE_BISHOP][i] = eg_bishop_table[i] + eg_table[BLACK_BISHOP][i] = eg_bishop_table[flip[i]] + eg_table[WHITE_ROOK][i] = eg_rook_table[i] + eg_table[BLACK_ROOK][i] = eg_rook_table[flip[i]] + eg_table[WHITE_QUEEN][i] = eg_queen_table[i] + eg_table[BLACK_QUEEN][i] = eg_queen_table[flip[i]] + eg_table[WHITE_KING][i] = eg_king_table[i] + eg_table[BLACK_KING][i] = eg_knight_table[flip[i]] + + for piece in range(6): + for field in range(64): + mg_table[piece][field] += mg_value[piece] + eg_table[piece][field] += eg_value[piece] + +_init_tables() + +class PestoStrategy(IStrategy): + def __init__(self, rollout_depth: int = 4): + super().__init__(rollout_depth) + + def pick_next_move(self, board: chess.Board) -> chess.Move | None: + def score_move(move: chess.Move): + bc = board.copy(stack=False) + bc.push(move) + return move, score(bc) + + moves = [score_move(move) for move in board.legal_moves] + ##print(board.turn, [m[1] for m in moves]) + if board.turn != chess.WHITE: + best_move = max(moves, key=lambda m: m[1]) + else: + best_move = min(moves, key=lambda m: m[1]) + #print(best_move) + return best_move[0] + + def analyze_board(self, board: chess.Board) -> int: + return score(board) + +#print("WHITE_PAWN", WHITE_PAWN) +#print("BLACK_PAWN", BLACK_PAWN) +#print("WHITE_KNIGHT", WHITE_KNIGHT) +#print("BLACK_KNIGHT", BLACK_KNIGHT) +#print("WHITE_BISHOP", WHITE_BISHOP) +#print("BLACK_BISHOP", BLACK_BISHOP) +#print("WHITE_ROOK", WHITE_ROOK) +#print("BLACK_ROOK", BLACK_ROOK) +#print("WHITE_QUEEN", WHITE_QUEEN) +#print("BLACK_QUEEN", BLACK_QUEEN) +#print("WHITE_KING", WHITE_KING) +#print("BLACK_KING", BLACK_KING) +#print("EMPTY", EMPTY) + diff --git a/chesspp/random_stockfish_strategy.py b/chesspp/random_stockfish_strategy.py new file mode 100644 index 0000000..f6e0708 --- /dev/null +++ b/chesspp/random_stockfish_strategy.py @@ -0,0 +1,36 @@ +import random + +import chess +import chess.engine + +from chesspp.i_strategy import IStrategy +from chesspp.eval import score_stockfish + + +class RandomStockfishStrategy(IStrategy): + def __init__(self, rollout_depth: int, path="../stockfish/stockfish-windows-x86-64-avx2", + random_seed: random.Random = random.Random()) -> None: + super().__init__(rollout_depth) + self._stockfish = None + self.path = path + self.random_seed = random_seed + + def __del__(self): + if self._stockfish is not None: + self._stockfish.quit() + + @property + def stockfish(self) -> chess.engine.SimpleEngine: + if self._stockfish is None: + self._stockfish = self.stockfish = chess.engine.SimpleEngine.popen_uci(self.path) + return self._stockfish + + @stockfish.setter + def stockfish(self, stockfish): + self._stockfish = stockfish + + def pick_next_move(self, board: chess.Board) -> chess.Move: + return self.random_seed.choice(list(board.legal_moves)) + + def analyze_board(self, board: chess.Board) -> int: + return score_stockfish(board, self.stockfish) diff --git a/chesspp/random_strategy.py b/chesspp/random_strategy.py index 74193d7..28c1bb4 100644 --- a/chesspp/random_strategy.py +++ b/chesspp/random_strategy.py @@ -5,7 +5,8 @@ from chesspp.eval import score_manual class RandomStrategy(IStrategy): - def __init__(self, random_state: random.Random): + def __init__(self, random_state: random.Random, rollout_depth: int = 4): + super().__init__(rollout_depth) self.random_state = random_state def pick_next_move(self, board: chess.Board) -> chess.Move | None: diff --git a/chesspp/simulation.py b/chesspp/simulation.py index 30ae071..52c25f2 100644 --- a/chesspp/simulation.py +++ b/chesspp/simulation.py @@ -5,8 +5,8 @@ import chess.pgn from typing import Tuple, List from enum import Enum from dataclasses import dataclass -from chesspp.i_strategy import IStrategy +from chesspp.engine_factory import StrategyEnum, EngineFactory, EngineEnum from chesspp.engine import Engine, Limit @@ -36,32 +36,43 @@ def simulate_game(white: Engine, black: Engine, limit: Limit, board: chess.Board class Evaluation: - def __init__(self, engine_a: Engine.__class__, strategy_a, engine_b: Engine.__class__, strategy_b, limit: Limit): + def __init__(self, engine_a: EngineEnum, strategy_a, engine_b: EngineEnum, strategy_b, limit: Limit, + stockfish_path: str, lc0_path: str): self.engine_a = engine_a self.strategy_a = strategy_a self.engine_b = engine_b self.strategy_b = strategy_b + self.stockfish_path = stockfish_path + self.lc0_path = lc0_path self.limit = limit def run(self, n_games=100, proc=mp.cpu_count()) -> List[EvaluationResult]: proc = min(proc, mp.cpu_count()) - with mp.Pool(proc) as pool: - args = [(self.engine_a, self.strategy_a, self.engine_b, self.strategy_b, self.limit) for i in range(n_games)] - return pool.map(Evaluation._test_simulate, args) + arg = (self.engine_a, self.strategy_a, self.engine_b, self.strategy_b, self.limit, self.stockfish_path, self.lc0_path) + if proc > 1: + with mp.Pool(proc) as pool: + args = [arg for i in range(n_games)] + return pool.map(Evaluation._test_simulate, args) + return [ + Evaluation._test_simulate(arg) + for _ in range(n_games) + ] @staticmethod - def _test_simulate(arg: Tuple[Engine.__class__, IStrategy, Engine.__class__, IStrategy, Limit]) -> EvaluationResult: - engine_a, strategy_a, engine_b, strategy_b, limit = arg + def _test_simulate(arg: Tuple[EngineEnum, StrategyEnum, EngineEnum, StrategyEnum, Limit, str, str]) -> EvaluationResult: + engine_a, strategy_a, engine_b, strategy_b, limit, stockfish_path, lc0_path = arg flip_engines = bool(random.getrandbits(1)) - board = chess.Board() - if flip_engines: - black, white = engine_a(board.copy(), chess.BLACK, strategy_a), engine_b(board.copy(), chess.WHITE, strategy_b) + black, white = EngineFactory.create_engine(engine_a, strategy_a, chess.BLACK, + stockfish_path, lc0_path), EngineFactory.create_engine( + engine_b, strategy_b, chess.WHITE, stockfish_path, lc0_path) else: - white, black = engine_a(board.copy(), chess.WHITE, strategy_a), engine_b(board.copy(), chess.BLACK, strategy_b) + white, black = EngineFactory.create_engine(engine_a, strategy_a, chess.WHITE, + stockfish_path, lc0_path), EngineFactory.create_engine( + engine_b, strategy_b, chess.BLACK, stockfish_path, lc0_path) - game = simulate_game(white, black, limit, board) + game = simulate_game(white, black, limit, chess.Board()) winner = game.end().board().outcome().winner result = Winner.Draw diff --git a/chesspp/stockfish_strategy.py b/chesspp/stockfish_strategy.py index 4ae3024..dd2fdf2 100644 --- a/chesspp/stockfish_strategy.py +++ b/chesspp/stockfish_strategy.py @@ -1,17 +1,17 @@ -import os import chess from chesspp.i_strategy import IStrategy from chesspp.eval import score_stockfish import chess.engine -_DIR = os.path.abspath(os.path.dirname(__file__)) - class StockFishStrategy(IStrategy): - def __init__(self, path="../stockfish/stockfish-windows-x86-64-avx2"): + def __init__(self, path="../stockfish/stockfish-windows-x86-64-avx2", rollout_depth: int = 4, + limit: chess.engine.Limit = chess.engine.Limit(depth=4)): + super().__init__(rollout_depth) self._stockfish = None self.path = path + self.limit = limit def __del__(self): if self._stockfish is not None: @@ -20,8 +20,7 @@ class StockFishStrategy(IStrategy): @property def stockfish(self) -> chess.engine.SimpleEngine: if self._stockfish is None: - self._stockfish = self.stockfish = chess.engine.SimpleEngine.popen_uci( - os.path.join(_DIR, self.path)) + self._stockfish = self.stockfish = chess.engine.SimpleEngine.popen_uci(self.path) return self._stockfish @stockfish.setter @@ -29,7 +28,7 @@ class StockFishStrategy(IStrategy): self._stockfish = stockfish def pick_next_move(self, board: chess.Board) -> chess.Move | None: - return self.stockfish.play(board, chess.engine.Limit(depth=4)).move + return self.stockfish.play(board, self.limit).move def analyze_board(self, board: chess.Board) -> int: return score_stockfish(board, self.stockfish) diff --git a/chesspp/util_gaussian.py b/chesspp/util_gaussian.py index e6696d3..f2170aa 100644 --- a/chesspp/util_gaussian.py +++ b/chesspp/util_gaussian.py @@ -1,26 +1,37 @@ import math +from functools import cache import torch import torch.distributions as dist from torch import exp -F1: dict[float, float] = {} -F2: dict[float, float] = {} -CDF: dict[float, float] = {} -lookup_count = 0 +total_count = 0 +calculation_count = 0 def get_lookup_count(): - global lookup_count - return lookup_count + global total_count, calculation_count + return total_count - calculation_count + + +@cache +def calc_cdf(alpha: float) -> tuple[float, float, float]: + """ + Returns the calculated CDF and parameters f1,f2 from the input alpha + """ + global calculation_count + calculation_count += 1 + + normal = dist.Normal(0, 1) + cdf_alpha = normal.cdf(torch.tensor(alpha)).item() + pdf_alpha = exp(normal.log_prob(torch.tensor(alpha))).item() + f1 = alpha * cdf_alpha + pdf_alpha + f2 = alpha ** 2 * cdf_alpha * (1 - cdf_alpha) + ( + 1 - 2 * cdf_alpha) * alpha * pdf_alpha - pdf_alpha ** 2 + return cdf_alpha, f1, f2 def max_gaussian(mu1, sigma1, mu2, sigma2) -> tuple[float, float]: - global lookup_count - global F1 - global F2 - global CDF - """ Returns the combined max gaussian of two Gaussians represented by mu1, sigma1, mu2, simga2 :param mu1: mu of the first Gaussian @@ -29,39 +40,22 @@ def max_gaussian(mu1, sigma1, mu2, sigma2) -> tuple[float, float]: :param sigma2: sigma of the second Gaussian :return: mu and sigma maximized """ + global total_count + total_count += 1 + # we assume independence of the two gaussians - #print(mu1, sigma1, mu2, sigma2) - normal = dist.Normal(0, 1) + # print(mu1, sigma1, mu2, sigma2) + # normal = dist.Normal(0, 1) sigma_m = math.sqrt(sigma1 ** 2 + sigma2 ** 2) - alpha = (mu1 - mu2) / sigma_m - if alpha in CDF: - cdf_alpha = CDF[alpha] - lookup_count += 1 - else: - cdf_alpha = normal.cdf(torch.tensor(alpha)).item() - CDF[alpha] = cdf_alpha + # round to two significant digits to enable float lookup + alpha = round((mu1 - mu2) / sigma_m, 2) - pdf_alpha = exp(normal.log_prob(torch.tensor(alpha))).item() - - if alpha in F1: - f1_alpha = F1[alpha] - lookup_count += 1 - else: - f1_alpha = alpha * cdf_alpha + pdf_alpha - F1[alpha] = f1_alpha - - if alpha in F2: - f2_alpha = F2[alpha] - lookup_count += 1 - else: - f2_alpha = alpha ** 2 * cdf_alpha * (1 - cdf_alpha) + ( - 1 - 2 * cdf_alpha) * alpha * pdf_alpha - pdf_alpha ** 2 - F2[alpha] = f2_alpha + cdf_alpha, f1_alpha, f2_alpha = calc_cdf(alpha) mu = mu2 + sigma_m * f1_alpha sigma = math.sqrt(sigma2 ** 2 + (sigma1 ** 2 - sigma2 ** 2) * cdf_alpha + sigma_m ** 2 * f2_alpha) - #sigma = math.sqrt((mu1**2 + sigma1**2) * cdf_alpha + (mu2**2 + sigma2**2) * (1 - cdf_alpha) + (mu1 + mu2) * sigma_m * pdf_alpha - mu**2) + # sigma = math.sqrt((mu1**2 + sigma1**2) * cdf_alpha + (mu2**2 + sigma2**2) * (1 - cdf_alpha) + (mu1 + mu2) * sigma_m * pdf_alpha - mu**2) return mu, sigma @@ -85,20 +79,20 @@ def min_gaussian(mu1, sigma1, mu2, sigma2) -> tuple[float, float]: pdf_alpha_neg = exp(normal.log_prob(torch.tensor(-alpha))).item() mu = mu1 * (1 - cdf_alpha) + mu2 * cdf_alpha - pdf_alpha_neg * sigma_m - sigma = math.sqrt((mu1**2 + sigma1**2) * (1 - cdf_alpha) + (mu2**2 + sigma2**2) * cdf_alpha - (mu1 + mu2) * sigma_m * pdf_alpha - mu**2) + sigma = math.sqrt((mu1 ** 2 + sigma1 ** 2) * (1 - cdf_alpha) + (mu2 ** 2 + sigma2 ** 2) * cdf_alpha - ( + mu1 + mu2) * sigma_m * pdf_alpha - mu ** 2) return mu, sigma except ValueError: print(mu1, sigma1, mu2, sigma2) - def beta_mean(alpha, beta): return alpha / (alpha + beta) def beta_std(alpha, beta): try: - return math.sqrt((alpha * beta) / ((alpha * beta)**2 * (alpha + beta + 1))) + return math.sqrt((alpha * beta) / ((alpha * beta) ** 2 * (alpha + beta + 1))) except ZeroDivisionError: print(alpha, beta) diff --git a/chesspp/web.py b/chesspp/web.py index 9a51745..f4c773e 100644 --- a/chesspp/web.py +++ b/chesspp/web.py @@ -6,7 +6,9 @@ from aiohttp import web import chess from chesspp import engine +from chesspp.engine_factory import EngineFactory from chesspp.stockfish_strategy import StockFishStrategy +from chesspp.pesto_strategy import PestoStrategy _DIR = os.path.abspath(os.path.dirname(__file__)) _DATA_DIR = os.path.abspath(os.path.join(_DIR, "static_data")) @@ -23,12 +25,7 @@ def load_index() -> str: class Simulate: """ Run a simulation of two engines""" - def __init__(self, engine_white=None, engine_black=None): - if engine_white is None: - engine_white = engine.ClassicMctsEngine(chess.WHITE) - if engine_black is None: - engine_black = engine.ClassicMctsEngine(chess.BLACK) - + def __init__(self, engine_white, engine_black): self.white = engine_white self.black = engine_black @@ -44,9 +41,13 @@ class Simulate: class WebInterface: - def __init__(self, white_engine: engine.Engine.__class__, black_engine: engine.Engine.__class__, limit: engine.Limit): + def __init__(self, white_engine, black_engine, strategy1, strategy2, stockfish_path, lc0_path, limit: engine.Limit): self.white = white_engine self.black = black_engine + self.strategy1 = strategy1 + self.strategy2 = strategy2 + self.stockfish_path = stockfish_path + self.lc0_path = lc0_path self.limit = limit @@ -73,8 +74,9 @@ class WebInterface: async def turns(): """ Simulates the game and sends the response to the client """ - runner = Simulate(self.white(chess.Board(), chess.WHITE, StockFishStrategy()), self.black( - chess.Board(), chess.BLACK, StockFishStrategy())).run(self.limit) + white = EngineFactory.create_engine(self.white, self.strategy1, chess.WHITE, self.stockfish_path, self.lc0_path) + black = EngineFactory.create_engine(self.black, self.strategy2, chess.BLACK, self.stockfish_path, self.lc0_path) + runner = Simulate(white, black).run(self.limit) def sim(): return next(runner, None) @@ -100,8 +102,3 @@ class WebInterface: web.static('/img/chesspieces/wikipedia/', _DATA_DIR), ]) web.run_app(app) - - -def run_sample(): - limit = engine.Limit(time=1) - WebInterface(engine.BayesMctsEngine, engine.ClassicMctsEngine, limit).run_app() diff --git a/main.py b/main.py index 326a788..160695c 100644 --- a/main.py +++ b/main.py @@ -1,12 +1,16 @@ import argparse import os import random +import sys import time - import chess import chess.engine import chess.pgn - +from chesspp.classic_mcts import ClassicMcts +from chesspp.baysian_mcts import BayesianMcts +from chesspp.engine_factory import EngineEnum, StrategyEnum +from chesspp.random_strategy import RandomStrategy +from chesspp.stockfish_strategy import StockFishStrategy from chesspp import engine from chesspp import simulation, eval from chesspp import util @@ -89,19 +93,10 @@ def analyze_results(moves: dict): def test_evaluation(): - a, b, s1, s2, n, limit, stockfish_path, proc = read_arguments() + a, b, s1, s2, n, limit, stockfish_path, lc0_path, proc = read_arguments() limit = engine.Limit(time=limit) - if s1 == StockFishStrategy: - strat1 = StockFishStrategy(stockfish_path) - else: - strat1 = s1() - if s2 == StockFishStrategy: - strat2 = StockFishStrategy(stockfish_path) - else: - strat2 = s1() - - evaluator = simulation.Evaluation(a, strat1, b, strat2, limit) + evaluator = simulation.Evaluation(a, s1, b, s2, limit, stockfish_path, lc0_path) results = evaluator.run(n, proc) games_played = len(results) a_wins = len(list(filter(lambda x: x.winner == simulation.Winner.Engine_A, results))) @@ -125,20 +120,25 @@ def read_arguments(): description='Compare two engines by playing multiple games against each other' ) - engines = {"ClassicMCTS": engine.ClassicMctsEngine, "BayesianMCTS": engine.BayesMctsEngine, - "Random": engine.RandomEngine} - strategies = {"Random": RandomStrategy, "Stockfish": StockFishStrategy} + engines = {"ClassicMCTS": EngineEnum.ClassicMcts, "BayesianMCTS": EngineEnum.BayesianMcts, + "Random": EngineEnum.Random, "Stockfish": EngineEnum.Stockfish, "Lc0": EngineEnum.Lc0} + strategies = {"Random": StrategyEnum.Random, "Stockfish": StrategyEnum.Stockfish, "Lc0": StrategyEnum.Lc0, + "RandomStockfish": StrategyEnum.RandomStockfish, "PESTO": StrategyEnum.Pestos} if os.name == 'nt': - stockfish_default = "../stockfish/stockfish-windows-x86-64-avx2" + stockfish_default = "stockfish/stockfish-windows-x86-64-avx2" + lc0_default = "lc0/lc0.exe" else: - stockfish_default = "../stockfish/stockfish-ubuntu-x86-64-avx2" + stockfish_default = "stockfish/stockfish-ubuntu-x86-64-avx2" + lc0_default = "lc0/lc0" parser.add_argument("--proc", default=2, help="Number of processors to use for simulation, default=1") parser.add_argument("--time", default=0.5, help="Time limit for each simulation step, default=0.5") parser.add_argument("-n", default=100, help="Number of games to simulate, default=100") - parser.add_argument("--stockfish", default=stockfish_default, - help=f"Path for stockfish executable, default='{stockfish_default}'") + parser.add_argument("--stockfish_path", default=stockfish_default, + help=f"Path for engine executable, default='{stockfish_default}'") + parser.add_argument("--lc0_path", default=lc0_default, + help=f"Path for engine executable, default='{stockfish_default}'") parser.add_argument("--engine1", "--e1", help="Engine A for the simulation", choices=engines.keys(), required=True) parser.add_argument("--engine2", "--e2", help="Engine B for the simulation", choices=engines.keys(), required=True) parser.add_argument("--strategy1", "--s1", default=list(strategies.keys())[0], @@ -146,7 +146,7 @@ def read_arguments(): choices=strategies.keys()) parser.add_argument("--strategy2", "--s2", default=list(strategies.keys())[0], help="Strategy for engine B for the rollout", - choices=strategies) + choices=strategies.keys()) args = parser.parse_args() engine1 = engines[args.engine1] @@ -154,7 +154,10 @@ def read_arguments(): strategy1 = strategies[args.strategy1] strategy2 = strategies[args.strategy2] - return engine1, engine2, strategy1, strategy2, int(args.n), float(args.time), args.stockfish, int(args.proc) + print(engine1, engine2, strategy1, strategy2, int(args.n), float(args.time), args.stockfish_path, args.lc0_path, + int(args.proc)) + return engine1, engine2, strategy1, strategy2, int(args.n), float( + args.time), args.stockfish_path, args.lc0_path, int(args.proc) def main(): @@ -168,3 +171,8 @@ def main(): if __name__ == '__main__': main() + + # Note: prevent endless wait on StockFish process + # by allowing for cleanup of objects (which closes stockfish) + import gc + gc.collect() diff --git a/web.py b/web.py index f2edfd9..ffadd5e 100644 --- a/web.py +++ b/web.py @@ -1,7 +1,9 @@ from chesspp import engine from chesspp import web +from main import read_arguments if __name__ == '__main__': - limit = engine.Limit(time=0.5) - web.WebInterface(engine.BayesMctsEngine, engine.ClassicMctsEngine, limit).run_app() + engine1, engine2, strategy1, strategy2, n_games, time, stockfish_path, lc0_path, n_proc = read_arguments() + limit = engine.Limit(time=time) + web.WebInterface(engine1, engine2, strategy1, strategy2, stockfish_path, lc0_path, limit).run_app()