Skip to content

Commit

Permalink
Implement tournament CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
Casper-Guo committed Jun 3, 2024
1 parent 62c660c commit bbbab0b
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 16 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ requires-python = ">=3.10"
dependencies=["click>=8.0.0"]

[build-system]
requires = ["setuptools >= 61.0"]
requires = ["setuptools >= 61.0", "click >= 8.0.0"]
build-backend = "setuptools.build_meta"

[tool.ruff]
Expand Down
10 changes: 4 additions & 6 deletions royal_game/modules/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ class Game:
implements select_move.
"""

def __init__(self, player1: Player, player2: Player, board: Optional[Board] = None):
def __init__(
self, player1: Player, player2: Player, board_seed: Optional[int] = 122138132480
):
if "select_move" not in dir(player1):
raise InvalidPlayer(player1)

Expand All @@ -31,11 +33,7 @@ def __init__(self, player1: Player, player2: Player, board: Optional[Board] = No

self.player1 = player1
self.player2 = player2

if board is None:
self.board = Board()
else:
self.board = board
self.board = Board(seed=board_seed)
self.white_turn = True

def __repr__(self):
Expand Down
9 changes: 7 additions & 2 deletions royal_game/players/dummy.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
"""An example player implementation that also serves as a dummy for testing."""
"""
An example player implementation that also serves as a dummy for testing.
Your player class name should be camel case with the first letter capitalized.
Your file name should be the same as the class name except in snake case.
"""

from typing import Iterable

Expand All @@ -12,7 +17,7 @@ class Dummy(Player):

def __init__(self):
"""Choose a name for your player."""
name = "{Dummy}"
name = "Dummy"
super().__init__(name)

def select_move(
Expand Down
5 changes: 2 additions & 3 deletions royal_game/tests/test_game.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from royal_game.modules.board import Board
from royal_game.modules.game import Game
from royal_game.players.dummy import Dummy


def test_end_game():
white_win = Game(Dummy(), Dummy(), Board(599282155520))
white_win = Game(Dummy(), Dummy(), 599282155520)
assert white_win.play()

black_win = Game(Dummy(), Dummy(), Board(966988398624))
black_win = Game(Dummy(), Dummy(), 966988398624)
assert not black_win.play()
125 changes: 121 additions & 4 deletions tournament.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,124 @@
"""CLI for benchmarking player agents against each other."""
"""
CLI for benchmarking player agents against each other.
Accepts player implementations as arguments and play them
against each other pairwise.
"""

import logging
import random
from collections import defaultdict
from itertools import combinations
from pathlib import Path
from typing import Iterable

import click

from royal_game.modules.game import Game
from royal_game.players.dummy import Dummy

game = Game(Dummy(), Dummy())
game.play()
logging.basicConfig(format="%(levelname)s: %(message)s")
logger = logging.getLogger(__name__)


def filename_to_class_name(filename: str) -> str:
"""Convert snake class filename to capitalized camel case class name."""
return "".join([word.capitalize() for word in filename.split("_")])


def output_results(num_wins: defaultdict, num_games: int) -> None:
"""Format tournament results nicely."""
print(f"{'TOURNAMENT RESULTS':_^120}")
player_names = list(num_wins.keys())

# list player names in the top row of the table
print("".join([f"{name:^20}" for name in ([""] + player_names)]))

for name1 in player_names:
# list player names in the first column of the table
# for a scoring matrix look
table_row = f"{name1:^20}"
for name2 in player_names:
if name1 == name2:
table_row += f"{'\\':^20}"
else:
win_percentage = f"{num_wins[name1][name2]}/{num_games}"
table_row += f"{win_percentage:^20}"
print(table_row)


@click.command()
@click.argument("players", nargs=-1, type=click.Path(exists=True, path_type=Path))
@click.option(
"-n",
"--num-games",
default=1000,
type=int,
help="Number of games to simulate between each pair of players.",
)
@click.option(
"-b",
"--board-seed",
default=122138132480,
type=int,
help=(
"Use a non-default initial board. "
"Board representation layout is documented in royal_game.modules.board."
),
)
@click.option(
"-r",
"--random-seed",
default=None,
type=int,
help="Optionally set a random seed for reproducibility.",
)
@click.option(
"-f",
"--full-output",
is_flag=True,
help="Enable saving full debug output to games.log in addition to summary statistics",
)
def main(
players: Iterable[click.Path],
num_games: int,
board_seed: int,
random_seed: int,
full_output: bool,
):
"""Implement tournament runner."""
if not full_output:
logging.getLogger("royal_game.modules.game").setLevel(logging.INFO)
if random_seed is not None:
random.seed(random_seed)

player_classes = []
for player in players:
try:
exec(
(
f"from royal_game.players.{player.stem} import "
f"{filename_to_class_name(player.stem)}"
)
)
player_classes.append(eval(filename_to_class_name(player.stem)))
except ImportError:
logger.critical("Unable to import from %s.", player.stem)
logger.info(
"Your player subclass should be named %s.", filename_to_class_name(player.stem)
)

num_wins = defaultdict(lambda: defaultdict(int))
for player1, player2 in combinations(player_classes, 2):
for _ in range(num_games):
game = Game(player1(), player2(), board_seed)
if game.play():
# player1 wins
num_wins[str(game.player1)][str(game.player2)] += 1
else:
num_wins[str(game.player2)][str(game.player1)] += 1

output_results(num_wins, num_games)


if __name__ == "__main__":
main()

0 comments on commit bbbab0b

Please sign in to comment.