-
Notifications
You must be signed in to change notification settings - Fork 0
/
tournament.py
142 lines (123 loc) · 4.27 KB
/
tournament.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
"""
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, combinations_with_replacement
from pathlib import Path
from typing import Iterable
import click
from royal_game.modules.game import Game
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, self_play: bool) -> 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 and not self_play:
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(
"-s",
"--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("-b", "--binary-seed", is_flag=True, help="Interpret the seed argument as binary")
@click.option(
"-r",
"--random-seed",
default=None,
type=int,
help="Optionally set a random seed for reproducibility.",
)
@click.option("-p", "--self-play", is_flag=True)
@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[Path],
num_games: int,
board_seed: int,
binary_seed: bool,
random_seed: int,
self_play: bool,
full_output: bool,
):
"""Implement tournament runner."""
if not full_output:
logging.getLogger("royal_game.modules.game").setLevel(logging.INFO)
if binary_seed:
board_seed = int(str(board_seed), 2)
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))
iterator = (
combinations(player_classes, 2)
if not self_play
else combinations_with_replacement(player_classes, 2)
)
for player1, player2 in iterator:
for i in range(num_games):
white_player, black_player = player1, player2
if i >= num_games // 2:
white_player, black_player = player2, player1
game = Game(white_player(), black_player(), board_seed)
if game.play():
# white wins
num_wins[str(game.player1)][str(game.player2)] += 1
else:
if player1 != player2:
# only log white wins in self-play
# black wins are implied
num_wins[str(game.player2)][str(game.player1)] += 1
output_results(num_wins, num_games, self_play)
if __name__ == "__main__":
main()