From f1a36964dfb6ba005902bb933ece2c6eea92bbd9 Mon Sep 17 00:00:00 2001 From: luk3k Date: Mon, 29 Jan 2024 12:26:19 +0100 Subject: [PATCH] added reuse of subtree for simulations (apply_move), played around with rollout depth --- chesspp/baysian_mcts.py | 25 +++++++++--------- chesspp/classic_mcts.py | 2 +- chesspp/engine.py | 49 ++++++++++++++++++++++------------- chesspp/simulation.py | 7 ++--- chesspp/stockfish_strategy.py | 16 ++++++++++++ chesspp/web.py | 9 +++++-- main.py | 14 ++++++---- 7 files changed, 80 insertions(+), 42 deletions(-) create mode 100644 chesspp/stockfish_strategy.py diff --git a/chesspp/baysian_mcts.py b/chesspp/baysian_mcts.py index 84b12d6..e2edbbd 100644 --- a/chesspp/baysian_mcts.py +++ b/chesspp/baysian_mcts.py @@ -1,17 +1,15 @@ -import chess from chesspp.i_mcts import * from chesspp.i_strategy import IStrategy from chesspp.util_gaussian import gaussian_ucb1, max_gaussian, min_gaussian from chesspp.eval import score_manual -import numpy as np -import math class BayesianMctsNode(IMctsNode): - def __init__(self, board: chess.Board, strategy: IStrategy, color: chess.Color, parent: Self | None, move: chess.Move | None, + def __init__(self, board: chess.Board, strategy: IStrategy, color: chess.Color, parent: Self | None, + move: chess.Move | None, random_state: random.Random, inherit_result: int | None = None, depth: int = 0): super().__init__(board, strategy, parent, move, random_state) - self.color = color # Color of the player whose turn it is + self.color = color # Color of the player whose turn it is self.visits = 0 self.result = inherit_result if inherit_result is not None else 0 self._set_mu_sigma() @@ -20,7 +18,8 @@ class BayesianMctsNode(IMctsNode): def _create_child(self, move: chess.Move) -> IMctsNode: copied_board = self.board.copy() copied_board.push(move) - return BayesianMctsNode(copied_board, self.strategy, not self.color, self, move, self.random_state, self.result, self.depth+1) + return BayesianMctsNode(copied_board, self.strategy, not self.color, self, move, self.random_state, self.result, + self.depth + 1) def _set_mu_sigma(self) -> None: self.mu = self.result @@ -74,7 +73,7 @@ class BayesianMctsNode(IMctsNode): return self._select_best_child() - def rollout(self, rollout_depth: int = 20) -> int: + def rollout(self, rollout_depth: int = 4) -> int: copied_board = self.board.copy() steps = self.depth for i in range(rollout_depth): @@ -124,29 +123,29 @@ class BayesianMctsNode(IMctsNode): self.parent.backpropagate() def print(self, indent=0): - print("\t"*indent + f"move={self.move}, visits={self.visits}, mu={self.mu}, sigma={self.sigma}") + print("\t" * indent + f"move={self.move}, visits={self.visits}, mu={self.mu}, sigma={self.sigma}") for c in self.children: - c.print(indent+1) + c.print(indent + 1) class BayesianMcts(IMcts): def __init__(self, board: chess.Board, strategy: IStrategy, color: chess.Color, seed: int | None = None): super().__init__(board, strategy, seed) - self.root = BayesianMctsNode(board, strategy, color,None, None, self.random_state) + self.root = BayesianMctsNode(board, strategy, color, None, None, self.random_state) self.root.visits += 1 self.color = color def sample(self, runs: int = 1000) -> None: for i in range(runs): - #print(f"sample {i}") + # print(f"sample {i}") leaf_node = self.root.select().expand() _ = leaf_node.rollout() leaf_node.backpropagate() def apply_move(self, move: chess.Move) -> None: self.board.push(move) - self.color = not self.color + self.color = self.board.turn # if a child node contains the move, set this child as new root for child in self.get_children(): @@ -169,4 +168,4 @@ class BayesianMcts(IMcts): def print(self): print("================================") - self.root.print() \ No newline at end of file + self.root.print() diff --git a/chesspp/classic_mcts.py b/chesspp/classic_mcts.py index 46ff5f3..6335b34 100644 --- a/chesspp/classic_mcts.py +++ b/chesspp/classic_mcts.py @@ -35,7 +35,7 @@ class ClassicMcts: self.children.append(child_node) return child_node - def _rollout(self, rollout_depth: int = 20) -> int: + def _rollout(self, rollout_depth: int = 3) -> int: """ Rolls out the node by simulating a game for a given depth. Sometimes this step is called 'simulation' or 'playout'. diff --git a/chesspp/engine.py b/chesspp/engine.py index f11b4ab..d5c606d 100644 --- a/chesspp/engine.py +++ b/chesspp/engine.py @@ -1,11 +1,13 @@ -from abc import ABC, abstractmethod -import chess -import chess.engine import random import time -from chesspp.classic_mcts import ClassicMcts +from abc import ABC, abstractmethod + +import chess +import chess.engine + from chesspp.baysian_mcts import BayesianMcts -from chesspp.random_strategy import RandomStrategy +from chesspp.classic_mcts import ClassicMcts +from chesspp.i_strategy import IStrategy class Limit: @@ -45,11 +47,17 @@ class Limit: class Engine(ABC): + board: chess.Board + """The chess board""" color: chess.Color """The side the engine plays (``chess.WHITE`` or ``chess.BLACK``).""" + strategy: IStrategy + """The strategy used to pick moves when simulating games.""" - def __init__(self, color: chess.Color): + def __init__(self, board: chess.Board, color: chess.Color, strategy: IStrategy): + self.board = board self.color = color + self.strategy = strategy @abstractmethod def play(self, board: chess.Board, limit: Limit) -> chess.engine.PlayResult: @@ -72,27 +80,32 @@ class Engine(ABC): class BayesMctsEngine(Engine): - def __init__(self, color: chess.Color): - super().__init__(color) + mcts: BayesianMcts + """The Bayesian MCTS""" + + def __init__(self, board: chess.Board, color: chess.Color, strategy: IStrategy): + super().__init__(board, color, strategy) + self.mcts = BayesianMcts(board, self.strategy, self.color) @staticmethod def get_name() -> str: return "BayesMctsEngine" def play(self, board: chess.Board, limit: Limit) -> chess.engine.PlayResult: - strategy = RandomStrategy(random.Random()) - bayes_mcts = BayesianMcts(board, strategy, self.color) - bayes_mcts.sample(1000) + if len(board.move_stack) != 0: # apply previous move to mcts --> reuse previous simulation results + self.mcts.apply_move(board.peek()) + self.mcts.sample() # limit.run(lambda: mcts_root.build_tree()) - best_move = max(bayes_mcts.get_moves().items(), key=lambda x: x[1])[0] if board.turn == chess.WHITE else ( - min(bayes_mcts.get_moves().items(), key=lambda x: x[1])[0]) + best_move = max(self.mcts.get_moves().items(), key=lambda x: x[1])[0] if board.turn == chess.WHITE else ( + min(self.mcts.get_moves().items(), key=lambda x: x[1])[0]) print(best_move) + self.mcts.apply_move(best_move) return chess.engine.PlayResult(move=best_move, ponder=None) class ClassicMctsEngine(Engine): - def __init__(self, color: chess.Color): - super().__init__(color) + def __init__(self, board: chess.Board, color: chess.Color, strategy: IStrategy): + super().__init__(board, color, strategy) @staticmethod def get_name() -> str: @@ -108,12 +121,12 @@ class ClassicMctsEngine(Engine): class RandomEngine(Engine): - def __init__(self, color: chess.Color): - super().__init__(color) + def __init__(self, board: chess.Board, color: chess.Color, strategy: IStrategy): + super().__init__(board, color, strategy) @staticmethod def get_name() -> str: - return "Random" + return "RandomEngine" def play(self, board: chess.Board, limit: Limit) -> chess.engine.PlayResult: move = random.choice(list(board.legal_moves)) diff --git a/chesspp/simulation.py b/chesspp/simulation.py index a04fca6..4637f63 100644 --- a/chesspp/simulation.py +++ b/chesspp/simulation.py @@ -21,11 +21,12 @@ class EvaluationResult: game: chess.pgn.Game -def simulate_game(white: Engine, black: Engine, limit: Limit) -> chess.pgn.Game: - board = chess.Board() - +def simulate_game(white: Engine, black: Engine, limit: Limit, board: chess.Board) -> chess.pgn.Game: is_white_playing = True while not board.is_game_over(): + print("simulation board:\n", board) + print() + print("mcts board:\n", white.mcts.board) play_result = white.play(board, limit) if is_white_playing else black.play(board, limit) board.push(play_result.move) is_white_playing = not is_white_playing diff --git a/chesspp/stockfish_strategy.py b/chesspp/stockfish_strategy.py new file mode 100644 index 0000000..270c7e4 --- /dev/null +++ b/chesspp/stockfish_strategy.py @@ -0,0 +1,16 @@ +import chess +from chesspp.i_strategy import IStrategy +import chess.engine + + +class StockFishStrategy(IStrategy): + stockfish: chess.engine.SimpleEngine + + def __init__(self): + self.stockfish = chess.engine.SimpleEngine.popen_uci( + "/home/luke/projects/pp-project/chess-engine-pp/stockfish/stockfish-ubuntu-x86-64-avx2") + + def pick_next_move(self, board: chess.Board) -> chess.Move | None: + move = self.stockfish.play(board, chess.engine.Limit(depth=4)).move + print("stockfish picked:", move) + return move diff --git a/chesspp/web.py b/chesspp/web.py index d9f298b..0c29cf6 100644 --- a/chesspp/web.py +++ b/chesspp/web.py @@ -1,10 +1,14 @@ import os import asyncio +import random + import aiohttp from aiohttp import web import chess from chesspp import engine +from chesspp.stockfish_strategy import StockFishStrategy +from chesspp.random_strategy import RandomStrategy _DIR = os.path.abspath(os.path.dirname(__file__)) _DATA_DIR = os.path.abspath(os.path.join(_DIR, "static_data")) @@ -71,7 +75,8 @@ class WebInterface: async def turns(): """ Simulates the game and sends the response to the client """ - runner = Simulate(self.white(chess.WHITE), self.black(chess.BLACK)).run(limit) + runner = Simulate(self.white(chess.Board(), chess.WHITE, RandomStrategy(random.Random())), self.black( + chess.Board(), chess.BLACK, RandomStrategy(random.Random()))).run(limit) def sim(): return next(runner, None) @@ -100,4 +105,4 @@ class WebInterface: if __name__ == '__main__': limit = engine.Limit(time=0.5) - WebInterface(engine.ClassicMctsEngine, engine.ClassicMctsEngine, limit).run_app() + WebInterface(engine.BayesMctsEngine, engine.ClassicMctsEngine, limit).run_app() diff --git a/main.py b/main.py index eab4762..db144c3 100644 --- a/main.py +++ b/main.py @@ -5,15 +5,18 @@ import chess.pgn from chesspp.classic_mcts import ClassicMcts from chesspp.baysian_mcts import BayesianMcts from chesspp.random_strategy import RandomStrategy +from chesspp.stockfish_strategy import StockFishStrategy from chesspp import engine from chesspp import util from chesspp import simulation, eval def test_simulate(): - white = engine.ClassicMctsEngine(chess.WHITE) - black = engine.ClassicMctsEngine(chess.BLACK) - game = simulation.simulate_game(white, black) + board = chess.Board() + strategy = StockFishStrategy() + white = engine.BayesMctsEngine(board.copy(), chess.WHITE, strategy) + black = engine.RandomEngine(board.copy(), chess.BLACK, RandomStrategy(random.Random())) + game = simulation.simulate_game(white, black, engine.Limit(time=0.5), board) print(game) @@ -92,12 +95,13 @@ def test_evaluation(): def main(): - test_evaluation() - # test_simulate() + # test_evaluation() + test_simulate() # test_mcts() # test_stockfish() # test_stockfish_prob() # test_bayes_mcts() + if __name__ == '__main__': main()