Added game statistics
This commit is contained in:
@@ -25,6 +25,7 @@ class Limit:
|
|||||||
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.time = time
|
||||||
self.nodes = nodes
|
self.nodes = nodes
|
||||||
|
self.node_count = 0
|
||||||
|
|
||||||
def run(self, func, *args, **kwargs):
|
def run(self, func, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@@ -36,6 +37,7 @@ class Limit:
|
|||||||
|
|
||||||
if self.nodes:
|
if self.nodes:
|
||||||
self._run_nodes(func, *args, **kwargs)
|
self._run_nodes(func, *args, **kwargs)
|
||||||
|
self.node_count = self.nodes
|
||||||
elif self.time:
|
elif self.time:
|
||||||
self._run_time(func, *args, **kwargs)
|
self._run_time(func, *args, **kwargs)
|
||||||
|
|
||||||
@@ -47,6 +49,7 @@ class Limit:
|
|||||||
start = time.perf_counter_ns()
|
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)
|
func(*args, **kwargs)
|
||||||
|
self.node_count += 1
|
||||||
|
|
||||||
def translate_to_engine_limit(self) -> chess.engine.Limit:
|
def translate_to_engine_limit(self) -> chess.engine.Limit:
|
||||||
if self.nodes:
|
if self.nodes:
|
||||||
@@ -95,6 +98,7 @@ class BayesMctsEngine(Engine):
|
|||||||
def __init__(self, board: chess.Board, color: chess.Color, strategy: IStrategy):
|
def __init__(self, board: chess.Board, color: chess.Color, strategy: IStrategy):
|
||||||
super().__init__(board, color, strategy)
|
super().__init__(board, color, strategy)
|
||||||
self.mcts = BayesianMcts(board, self.strategy, self.color)
|
self.mcts = BayesianMcts(board, self.strategy, self.color)
|
||||||
|
self.node_counts = []
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_name() -> str:
|
def get_name() -> str:
|
||||||
@@ -103,7 +107,16 @@ class BayesMctsEngine(Engine):
|
|||||||
def play(self, board: chess.Board, limit: Limit) -> chess.engine.PlayResult:
|
def play(self, board: chess.Board, limit: Limit) -> chess.engine.PlayResult:
|
||||||
if len(board.move_stack) != 0: # apply previous move to mcts --> reuse previous simulation results
|
if len(board.move_stack) != 0: # apply previous move to mcts --> reuse previous simulation results
|
||||||
self.mcts.apply_move(board.peek())
|
self.mcts.apply_move(board.peek())
|
||||||
limit.run(lambda: self.mcts.sample(1))
|
|
||||||
|
node_count = 0
|
||||||
|
|
||||||
|
def do():
|
||||||
|
nonlocal node_count
|
||||||
|
self.mcts.sample(1)
|
||||||
|
node_count += 1
|
||||||
|
|
||||||
|
limit.run(do)
|
||||||
|
self.node_counts.append(node_count)
|
||||||
best_move = self.get_best_move(self.mcts.get_moves(), board.turn)
|
best_move = self.get_best_move(self.mcts.get_moves(), board.turn)
|
||||||
self.mcts.apply_move(best_move)
|
self.mcts.apply_move(best_move)
|
||||||
return chess.engine.PlayResult(move=best_move, ponder=None)
|
return chess.engine.PlayResult(move=best_move, ponder=None)
|
||||||
@@ -121,6 +134,7 @@ class BayesMctsEngine(Engine):
|
|||||||
class ClassicMctsEngine(Engine):
|
class ClassicMctsEngine(Engine):
|
||||||
def __init__(self, board: chess.Board, color: chess.Color, strategy: IStrategy):
|
def __init__(self, board: chess.Board, color: chess.Color, strategy: IStrategy):
|
||||||
super().__init__(board, color, strategy)
|
super().__init__(board, color, strategy)
|
||||||
|
self.node_counts = []
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_name() -> str:
|
def get_name() -> str:
|
||||||
@@ -128,7 +142,15 @@ class ClassicMctsEngine(Engine):
|
|||||||
|
|
||||||
def play(self, board: chess.Board, limit: Limit) -> chess.engine.PlayResult:
|
def play(self, board: chess.Board, limit: Limit) -> chess.engine.PlayResult:
|
||||||
mcts_root = ClassicMcts(board, self.color, self.strategy)
|
mcts_root = ClassicMcts(board, self.color, self.strategy)
|
||||||
limit.run(lambda: mcts_root.build_tree(1))
|
node_count = 0
|
||||||
|
|
||||||
|
def do():
|
||||||
|
nonlocal node_count
|
||||||
|
mcts_root.build_tree(1)
|
||||||
|
node_count += 1
|
||||||
|
|
||||||
|
limit.run(do)
|
||||||
|
self.node_counts.append(node_count)
|
||||||
best_move = max(mcts_root.children, key=lambda x: x.score).move if board.turn == chess.WHITE else (
|
best_move = max(mcts_root.children, key=lambda x: x.score).move if board.turn == chess.WHITE else (
|
||||||
min(mcts_root.children, key=lambda x: x.score).move)
|
min(mcts_root.children, key=lambda x: x.score).move)
|
||||||
return chess.engine.PlayResult(move=best_move, ponder=None)
|
return chess.engine.PlayResult(move=best_move, ponder=None)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import multiprocessing as mp
|
import multiprocessing as mp
|
||||||
import random
|
import random
|
||||||
|
import time
|
||||||
import chess
|
import chess
|
||||||
import chess.pgn
|
import chess.pgn
|
||||||
from typing import Tuple, List
|
from typing import Tuple, List
|
||||||
@@ -16,23 +17,61 @@ class Winner(Enum):
|
|||||||
Draw = 2
|
Draw = 2
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GameStatistics:
|
||||||
|
white: str
|
||||||
|
black: str
|
||||||
|
average_time_white: float
|
||||||
|
average_time_black: float
|
||||||
|
nodes_white: int
|
||||||
|
nodes_black: int
|
||||||
|
length: int
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class EvaluationResult:
|
class EvaluationResult:
|
||||||
winner: Winner
|
winner: Winner
|
||||||
game: str
|
game: str
|
||||||
|
statistics: GameStatistics
|
||||||
|
|
||||||
|
|
||||||
def simulate_game(white: Engine, black: Engine, limit: Limit, board: chess.Board) -> chess.pgn.Game:
|
def simulate_game(white: Engine, black: Engine, limit: Limit, board: chess.Board) -> (chess.pgn.Game, GameStatistics):
|
||||||
is_white_playing = True
|
is_white_playing = True
|
||||||
|
times_white = []
|
||||||
|
times_black = []
|
||||||
|
game_length = 0
|
||||||
while not board.is_game_over():
|
while not board.is_game_over():
|
||||||
|
start = time.time()
|
||||||
play_result = white.play(board, limit) if is_white_playing else black.play(board, limit)
|
play_result = white.play(board, limit) if is_white_playing else black.play(board, limit)
|
||||||
|
end = time.time()
|
||||||
|
times_white.append(end - start) if is_white_playing else times_black.append(end - start)
|
||||||
board.push(play_result.move)
|
board.push(play_result.move)
|
||||||
is_white_playing = not is_white_playing
|
is_white_playing = not is_white_playing
|
||||||
|
game_length += 1
|
||||||
|
|
||||||
game = chess.pgn.Game.from_board(board)
|
game = chess.pgn.Game.from_board(board)
|
||||||
game.headers['White'] = white.get_name()
|
game.headers['White'] = white.get_name()
|
||||||
game.headers['Black'] = black.get_name()
|
game.headers['Black'] = black.get_name()
|
||||||
return game
|
|
||||||
|
if hasattr(white, "node_counts"):
|
||||||
|
white_nodes = sum(white.node_counts) // len(white.node_counts)
|
||||||
|
else:
|
||||||
|
white_nodes = 0
|
||||||
|
|
||||||
|
if hasattr(black, "node_counts"):
|
||||||
|
black_nodes = sum(black.node_counts) // len(black.node_counts)
|
||||||
|
else:
|
||||||
|
black_nodes = 0
|
||||||
|
|
||||||
|
statistics = GameStatistics(white=white.get_name(),
|
||||||
|
black=black.get_name(),
|
||||||
|
average_time_white=(sum(times_white)/len(times_white)),
|
||||||
|
average_time_black=(sum(times_black)/len(times_black)),
|
||||||
|
nodes_white=white_nodes,
|
||||||
|
nodes_black=black_nodes,
|
||||||
|
length=game_length
|
||||||
|
)
|
||||||
|
|
||||||
|
return game, statistics
|
||||||
|
|
||||||
|
|
||||||
class Evaluation:
|
class Evaluation:
|
||||||
@@ -73,7 +112,7 @@ class Evaluation:
|
|||||||
stockfish_path, lc0_path, stockfish_elo), EngineFactory.create_engine(
|
stockfish_path, lc0_path, stockfish_elo), EngineFactory.create_engine(
|
||||||
engine_b, strategy_b, chess.BLACK, stockfish_path, lc0_path, stockfish_elo)
|
engine_b, strategy_b, chess.BLACK, stockfish_path, lc0_path, stockfish_elo)
|
||||||
|
|
||||||
game = simulate_game(white, black, limit, chess.Board())
|
game, statistics = simulate_game(white, black, limit, chess.Board())
|
||||||
winner = game.end().board().outcome().winner
|
winner = game.end().board().outcome().winner
|
||||||
|
|
||||||
result = Winner.Draw
|
result = Winner.Draw
|
||||||
@@ -87,4 +126,4 @@ class Evaluation:
|
|||||||
case (chess.BLACK, False):
|
case (chess.BLACK, False):
|
||||||
result = Winner.Engine_B
|
result = Winner.Engine_B
|
||||||
|
|
||||||
return EvaluationResult(result, str(game))
|
return EvaluationResult(result, str(game), statistics)
|
||||||
|
|||||||
15
main.py
15
main.py
@@ -95,6 +95,21 @@ def test_evaluation():
|
|||||||
|
|
||||||
evaluator = simulation.Evaluation(a, s1, b, s2, limit, stockfish_path, lc0_path, stockfish_elo)
|
evaluator = simulation.Evaluation(a, s1, b, s2, limit, stockfish_path, lc0_path, stockfish_elo)
|
||||||
results = evaluator.run(n, proc)
|
results = evaluator.run(n, proc)
|
||||||
|
|
||||||
|
for r in results:
|
||||||
|
stats = r.statistics
|
||||||
|
print("====================================")
|
||||||
|
print(f"Game length: {stats.length} moves")
|
||||||
|
print(f"{stats.white} (White):")
|
||||||
|
print(f"Average node count: {stats.nodes_white}")
|
||||||
|
print(f"Average simulation time: {stats.average_time_white}")
|
||||||
|
print()
|
||||||
|
print(f"{stats.black} (Black):")
|
||||||
|
print(f"Average node count: {stats.nodes_black}")
|
||||||
|
print(f"Average simulation time: {stats.average_time_black}")
|
||||||
|
print("====================================")
|
||||||
|
print()
|
||||||
|
|
||||||
games_played = len(results)
|
games_played = len(results)
|
||||||
a_wins = len(list(filter(lambda x: x.winner == simulation.Winner.Engine_A, results)))
|
a_wins = len(list(filter(lambda x: x.winner == simulation.Winner.Engine_A, results)))
|
||||||
b_wins = len(list(filter(lambda x: x.winner == simulation.Winner.Engine_B, results)))
|
b_wins = len(list(filter(lambda x: x.winner == simulation.Winner.Engine_B, results)))
|
||||||
|
|||||||
Reference in New Issue
Block a user