Introduce maximum number of turns
parent
d7ac20a6dd
commit
194f962746
1
elo.py
1
elo.py
|
|
@ -24,6 +24,7 @@ def main() -> int:
|
||||||
tracker.start_periodic_matches(
|
tracker.start_periodic_matches(
|
||||||
game=RoyalGameOfUr.empty(),
|
game=RoyalGameOfUr.empty(),
|
||||||
interval_seconds=9 * 60,
|
interval_seconds=9 * 60,
|
||||||
|
max_turns=1_500,
|
||||||
player_count=2,
|
player_count=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Sequence
|
from typing import Any, Sequence
|
||||||
|
|
||||||
import agents
|
import agents.remote
|
||||||
import pyclient
|
import pyclient
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ class PlayerIdentifier:
|
||||||
return (self.name, self.url, self.version)
|
return (self.name, self.url, self.version)
|
||||||
|
|
||||||
@classmethod
|
@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.
|
Gain a player identifier from an agent.
|
||||||
"""
|
"""
|
||||||
|
|
@ -177,7 +177,7 @@ class Match:
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_replay(
|
def from_replay(
|
||||||
cls,
|
cls,
|
||||||
players : list[agents.RemoteAgent],
|
players : list[agents.remote.RemoteAgent],
|
||||||
replay : pyclient.GameReplay,
|
replay : pyclient.GameReplay,
|
||||||
timestamp : str | None,
|
timestamp : str | None,
|
||||||
) -> "Match":
|
) -> "Match":
|
||||||
|
|
@ -291,7 +291,7 @@ class EloTracker:
|
||||||
|
|
||||||
# Thread-unsafe variables
|
# Thread-unsafe variables
|
||||||
# Please use a lock while doing CRUD operations on them
|
# 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.__matches: list[Match] = []
|
||||||
self.__stats: dict[str, dict[PlayerIdentifier, EloStat]] = {}
|
self.__stats: dict[str, dict[PlayerIdentifier, EloStat]] = {}
|
||||||
|
|
||||||
|
|
@ -436,6 +436,7 @@ class EloTracker:
|
||||||
game: Game,
|
game: Game,
|
||||||
interval_seconds: float,
|
interval_seconds: float,
|
||||||
player_count: int,
|
player_count: int,
|
||||||
|
max_turns : int | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Perform a schedule in which you play a random game.
|
Perform a schedule in which you play a random game.
|
||||||
|
|
@ -446,6 +447,8 @@ class EloTracker:
|
||||||
:type interval_seconds: float
|
:type interval_seconds: float
|
||||||
:param player_count: The number of players that are supposed to participate
|
:param player_count: The number of players that are supposed to participate
|
||||||
:type player_count: int
|
: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():
|
while not self.__scheduler_stop.is_set():
|
||||||
try:
|
try:
|
||||||
|
|
@ -468,7 +471,11 @@ class EloTracker:
|
||||||
self.__debug(
|
self.__debug(
|
||||||
f"Playing a new scheduled match of {game.game_name()}"
|
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:
|
except Exception as exc:
|
||||||
self.__debug(f"Scheduled match failed: {exc}")
|
self.__debug(f"Scheduled match failed: {exc}")
|
||||||
# raise exc
|
# raise exc
|
||||||
|
|
@ -582,13 +589,13 @@ class EloTracker:
|
||||||
"Expected `players` field to be a list of strings."
|
"Expected `players` field to be a list of strings."
|
||||||
)
|
)
|
||||||
|
|
||||||
players : list[agents.RemoteAgent] = []
|
players : list[agents.remote.RemoteAgent] = []
|
||||||
for url in urls:
|
for url in urls:
|
||||||
if not isinstance(url, str):
|
if not isinstance(url, str):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
agent = agents.RemoteAgent(url=url)
|
agent = agents.remote.RemoteAgent(url=url)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass # Not an available player right now
|
pass # Not an available player right now
|
||||||
else:
|
else:
|
||||||
|
|
@ -604,23 +611,38 @@ class EloTracker:
|
||||||
player_id=PlayerIdentifier.from_server_agent(agent)
|
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.
|
Play a single match with appointed players.
|
||||||
|
|
||||||
:param players: List of URLs that participate.
|
:param players: List of URLs that participate.
|
||||||
:type players: list[str]
|
: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.
|
:return: A summary of the game.
|
||||||
:rtype: pyclient.GameReplay
|
:rtype: pyclient.GameReplay
|
||||||
:raises ValueError: One of the URLs could not be accessed.
|
: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 ]
|
||||||
|
|
||||||
|
try:
|
||||||
replay = pyclient.PyClient(debug=self.debug).play_game(
|
replay = pyclient.PyClient(debug=self.debug).play_game(
|
||||||
players=ags,
|
players=ags,
|
||||||
start=game,
|
start=game,
|
||||||
|
max_turns=max_turns,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
return None # Exceeded turn limit
|
||||||
|
|
||||||
|
else:
|
||||||
m = Match.from_replay(players=ags, replay=replay, timestamp=Match.now())
|
m = Match.from_replay(players=ags, replay=replay, timestamp=Match.now())
|
||||||
|
|
||||||
# Record match
|
# Record match
|
||||||
|
|
@ -632,13 +654,16 @@ class EloTracker:
|
||||||
def play_random_match(
|
def play_random_match(
|
||||||
self,
|
self,
|
||||||
game: Game,
|
game: Game,
|
||||||
|
max_turns : int | None = None,
|
||||||
player_count: int | None = None,
|
player_count: int | None = None,
|
||||||
) -> pyclient.GameReplay:
|
) -> pyclient.GameReplay | None:
|
||||||
"""
|
"""
|
||||||
Play a game with any known players.
|
Play a game with any known players.
|
||||||
|
|
||||||
:param game: The game to start playing
|
:param game: The game to start playing
|
||||||
:type game: Game
|
: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.
|
:param player_count: Optional number of players to select.
|
||||||
:type player_count: int | None
|
:type player_count: int | None
|
||||||
:raises ValueError: One of the randomly chosen URLs could not be accessed.
|
:raises ValueError: One of the randomly chosen URLs could not be accessed.
|
||||||
|
|
@ -657,6 +682,7 @@ class EloTracker:
|
||||||
self,
|
self,
|
||||||
game: Game,
|
game: Game,
|
||||||
interval_seconds: float = 300,
|
interval_seconds: float = 300,
|
||||||
|
max_turns : int | None = None,
|
||||||
player_count: int = 2,
|
player_count: int = 2,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -666,6 +692,8 @@ class EloTracker:
|
||||||
:type game: pyclient.Game
|
:type game: pyclient.Game
|
||||||
:param interval_seconds: Number of seconds to sleep between games
|
:param interval_seconds: Number of seconds to sleep between games
|
||||||
:type interval_seconds: float
|
: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
|
:param player_count: The number of players that are supposed to participate
|
||||||
:type player_count: int
|
:type player_count: int
|
||||||
"""
|
"""
|
||||||
|
|
@ -679,6 +707,7 @@ class EloTracker:
|
||||||
thread = threading.Thread(
|
thread = threading.Thread(
|
||||||
target=self.__scheduler_loop,
|
target=self.__scheduler_loop,
|
||||||
args=(game, interval_seconds, player_count),
|
args=(game, interval_seconds, player_count),
|
||||||
|
kwargs={"max_turns": max_turns},
|
||||||
daemon=True,
|
daemon=True,
|
||||||
)
|
)
|
||||||
self.__scheduler_threads.append(thread)
|
self.__scheduler_threads.append(thread)
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,12 @@ class PyClient:
|
||||||
|
|
||||||
debug : bool
|
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.
|
Generate a game by polling the players.
|
||||||
|
|
||||||
|
|
@ -27,12 +32,22 @@ class PyClient:
|
||||||
:type players: list[Agent]
|
:type players: list[Agent]
|
||||||
:param start: The start state of the game.
|
:param start: The start state of the game.
|
||||||
:type start: 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.
|
:return: A generator that yields turns.
|
||||||
:rtype: Generator[Turn, None, None]
|
:rtype: Generator[Turn, None, None]
|
||||||
|
:raises ValueError: The game is taking too long.
|
||||||
"""
|
"""
|
||||||
current_state = start
|
current_state = start
|
||||||
|
turns_taken = 0
|
||||||
|
|
||||||
while current_state.winner() is None:
|
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()
|
player = current_state.player_to_move()
|
||||||
|
|
||||||
if len(players) < player:
|
if len(players) < player:
|
||||||
|
|
@ -61,7 +76,12 @@ class PyClient:
|
||||||
|
|
||||||
yield Turn(action=payload, player=player, state=current_state)
|
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
|
Generate a game by polling the players. Collect all moves in a
|
||||||
summary.
|
summary.
|
||||||
|
|
@ -70,11 +90,16 @@ class PyClient:
|
||||||
:type players: list[Agent]
|
:type players: list[Agent]
|
||||||
:param start: The start state of the game.
|
:param start: The start state of the game.
|
||||||
:type start: 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.
|
:return: Summary describing how the game went.
|
||||||
:rtype: GameReplay
|
:rtype: GameReplay
|
||||||
|
:raises ValueError: The game is taking too long.
|
||||||
"""
|
"""
|
||||||
return GameReplay(
|
return GameReplay(
|
||||||
game_name=start.game_name(),
|
game_name=start.game_name(),
|
||||||
start=start,
|
start=start,
|
||||||
turns=list(self.gen_game(players=players, start=start)),
|
turns=list(self.gen_game(
|
||||||
|
players=players, start=start, max_turns=max_turns
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue