From 194f962746f434ccd37730ac41b821ba584e0ee8 Mon Sep 17 00:00:00 2001 From: Bram van den Heuvel Date: Wed, 1 Jul 2026 22:45:13 +0200 Subject: [PATCH] Introduce maximum number of turns --- elo.py | 1 + elo_tracker/app.py | 67 +++++++++++++++++++++++++++++++++------------- pyclient/client.py | 31 ++++++++++++++++++--- 3 files changed, 77 insertions(+), 22 deletions(-) diff --git a/elo.py b/elo.py index 973cb0b..48847ff 100644 --- a/elo.py +++ b/elo.py @@ -24,6 +24,7 @@ def main() -> int: tracker.start_periodic_matches( game=RoyalGameOfUr.empty(), interval_seconds=9 * 60, + max_turns=1_500, player_count=2, ) diff --git a/elo_tracker/app.py b/elo_tracker/app.py index a33ba13..2d8d20f 100644 --- a/elo_tracker/app.py +++ b/elo_tracker/app.py @@ -14,7 +14,7 @@ from dataclasses import dataclass from datetime import datetime, timezone from typing import Any, Sequence -import agents +import agents.remote import pyclient import time @@ -39,7 +39,7 @@ class PlayerIdentifier: return (self.name, self.url, self.version) @classmethod - def from_server_agent(cls, agent : agents.RemoteAgent) -> "PlayerIdentifier": + def from_server_agent(cls, agent : agents.remote.RemoteAgent) -> "PlayerIdentifier": """ Gain a player identifier from an agent. """ @@ -177,7 +177,7 @@ class Match: @classmethod def from_replay( cls, - players : list[agents.RemoteAgent], + players : list[agents.remote.RemoteAgent], replay : pyclient.GameReplay, timestamp : str | None, ) -> "Match": @@ -291,7 +291,7 @@ class EloTracker: # Thread-unsafe variables # Please use a lock while doing CRUD operations on them - self.players: list[agents.RemoteAgent] = [] + self.players: list[agents.remote.RemoteAgent] = [] self.__matches: list[Match] = [] self.__stats: dict[str, dict[PlayerIdentifier, EloStat]] = {} @@ -436,6 +436,7 @@ class EloTracker: game: Game, interval_seconds: float, player_count: int, + max_turns : int | None = None, ) -> None: """ Perform a schedule in which you play a random game. @@ -446,6 +447,8 @@ class EloTracker: :type interval_seconds: float :param player_count: The number of players that are supposed to participate :type player_count: int + :param max_turns: Optional maximum number of turns to allow + :type max_turns: int | None """ while not self.__scheduler_stop.is_set(): try: @@ -468,7 +471,11 @@ class EloTracker: self.__debug( f"Playing a new scheduled match of {game.game_name()}" ) - self.play_random_match(game=game, player_count=player_count) + self.play_random_match( + game=game, + max_turns=max_turns, + player_count=player_count, + ) except Exception as exc: self.__debug(f"Scheduled match failed: {exc}") # raise exc @@ -582,13 +589,13 @@ class EloTracker: "Expected `players` field to be a list of strings." ) - players : list[agents.RemoteAgent] = [] + players : list[agents.remote.RemoteAgent] = [] for url in urls: if not isinstance(url, str): continue try: - agent = agents.RemoteAgent(url=url) + agent = agents.remote.RemoteAgent(url=url) except ValueError: pass # Not an available player right now else: @@ -604,41 +611,59 @@ class EloTracker: player_id=PlayerIdentifier.from_server_agent(agent) ) - def play_match(self, players: list[str], game: Game) -> pyclient.GameReplay: + def play_match( + self, + players: list[str], + game: Game, + max_turns : int | None = None, + ) -> pyclient.GameReplay | None: """ Play a single match with appointed players. :param players: List of URLs that participate. :type players: list[str] + :param game: The game to start playing + :type game: Game + :param max_turns: Optional maximum number of turns to allow + :type max_turns: int | None :return: A summary of the game. :rtype: pyclient.GameReplay :raises ValueError: One of the URLs could not be accessed. """ - ags : list[Any] = [ agents.RemoteAgent(url=url) for url in players ] + ags : list[Any] = [ agents.remote.RemoteAgent(url=url) for url in players ] - replay = pyclient.PyClient(debug=self.debug).play_game( - players=ags, - start=game, - ) + try: + replay = pyclient.PyClient(debug=self.debug).play_game( + players=ags, + start=game, + max_turns=max_turns, + ) - m = Match.from_replay(players=ags, replay=replay, timestamp=Match.now()) + except ValueError: + return None # Exceeded turn limit + + else: + m = Match.from_replay(players=ags, replay=replay, timestamp=Match.now()) - # Record match - m.log(self.game_file_name) - self.__register_match(m) + # Record match + m.log(self.game_file_name) + self.__register_match(m) - return replay + return replay def play_random_match( self, game: Game, + max_turns : int | None = None, player_count: int | None = None, - ) -> pyclient.GameReplay: + ) -> pyclient.GameReplay | None: """ Play a game with any known players. :param game: The game to start playing :type game: Game + :param max_turns: Optional maximum number of turns to allow + :type max_turns: int | None :param player_count: Optional number of players to select. :type player_count: int | None :raises ValueError: One of the randomly chosen URLs could not be accessed. @@ -657,6 +682,7 @@ class EloTracker: self, game: Game, interval_seconds: float = 300, + max_turns : int | None = None, player_count: int = 2, ) -> None: """ @@ -666,6 +692,8 @@ class EloTracker: :type game: pyclient.Game :param interval_seconds: Number of seconds to sleep between games :type interval_seconds: float + :param max_turns: Optional maximum number of turns to allow + :type max_turns: int | None :param player_count: The number of players that are supposed to participate :type player_count: int """ @@ -679,6 +707,7 @@ class EloTracker: thread = threading.Thread( target=self.__scheduler_loop, args=(game, interval_seconds, player_count), + kwargs={"max_turns": max_turns}, daemon=True, ) self.__scheduler_threads.append(thread) diff --git a/pyclient/client.py b/pyclient/client.py index 37be5d8..79b977d 100644 --- a/pyclient/client.py +++ b/pyclient/client.py @@ -19,7 +19,12 @@ class PyClient: debug : bool - def gen_game(self, players : list[Agent], start : Game) -> Generator[Turn, None, None]: + def gen_game( + self, + players : list[Agent], + start : Game, + max_turns : int | None = None, + ) -> Generator[Turn, None, None]: """ Generate a game by polling the players. @@ -27,12 +32,22 @@ class PyClient: :type players: list[Agent] :param start: The start state of the game. :type start: Game + :param max_turns: The maximum number of turns a game may take + :type max_turns: int :return: A generator that yields turns. :rtype: Generator[Turn, None, None] + :raises ValueError: The game is taking too long. """ current_state = start + turns_taken = 0 while current_state.winner() is None: + if max_turns is not None and turns_taken >= max_turns: + raise ValueError( + f"Turn limit exceeded: played for {turns_taken} turns and still needed more." + ) + + turns_taken += 1 player = current_state.player_to_move() if len(players) < player: @@ -61,7 +76,12 @@ class PyClient: yield Turn(action=payload, player=player, state=current_state) - def play_game(self, players : list[Agent], start : Game) -> GameReplay: + def play_game( + self, + players : list[Agent], + start : Game, + max_turns : int | None = None, + ) -> GameReplay: """ Generate a game by polling the players. Collect all moves in a summary. @@ -70,11 +90,16 @@ class PyClient: :type players: list[Agent] :param start: The start state of the game. :type start: Game + :param max_turns: The maximum number of turns a game may take + :type max_turns: int :return: Summary describing how the game went. :rtype: GameReplay + :raises ValueError: The game is taking too long. """ return GameReplay( game_name=start.game_name(), start=start, - turns=list(self.gen_game(players=players, start=start)), + turns=list(self.gen_game( + players=players, start=start, max_turns=max_turns + )), )