pygo/gomill/gomill/tournament_results.py

312 lines
11 KiB
Python

"""Retrieving and reporting on tournament results."""
from __future__ import division
from gomill import ascii_tables
from gomill.utils import format_float, format_percent
from gomill.common import colour_name
class Matchup_description(object):
"""Description of a matchup (pairing of two players).
Public attributes:
id -- matchup id (very short string)
player_1 -- player code (identifier-like string)
player_2 -- player code (identifier-like string)
name -- string (eg 'xxx v yyy')
board_size -- int
komi -- float
alternating -- bool
handicap -- int or None
handicap_style -- 'fixed' or 'free'
move_limit -- int
scorer -- 'internal' or 'players'
number_of_games -- int or None
If alternating is False, player_1 plays black and player_2 plays white;
otherwise they alternate.
player_1 and player_2 are always different.
"""
def describe_details(self):
"""Return a text description of game settings.
This covers the most important game settings which can't be observed
in the results table (board size, handicap, and komi).
"""
s = "board size: %s " % self.board_size
if self.handicap is not None:
s += "handicap: %s (%s) " % (
self.handicap, self.handicap_style)
s += "komi: %s" % self.komi
return s
class Tournament_results(object):
"""Provide access to results of a single tournament.
The tournament results are catalogued in terms of 'matchups', with each
matchup corresponding to a series of games which have the same players and
settings. Each matchup has an id, which is a short string.
"""
def __init__(self, matchup_list, results):
self.matchup_list = matchup_list
self.results = results
self.matchups = dict((m.id, m) for m in matchup_list)
def get_matchup_ids(self):
"""Return a list of all matchup ids, in definition order."""
return [m.id for m in self.matchup_list]
def get_matchup(self, matchup_id):
"""Describe the matchup with the specified id.
Returns a Matchup_description (which should be treated as read-only).
"""
return self.matchups[matchup_id]
def get_matchups(self):
"""Return a map matchup id -> Matchup_description."""
return self.matchups.copy()
def get_matchup_results(self, matchup_id):
"""Return the results for the specified matchup.
Returns a list of gtp_games.Game_results (in unspecified order).
The Game_results all have game_id set.
"""
return self.results[matchup_id][:]
def get_matchup_stats(self, matchup_id):
"""Return statistics for the specified matchup.
Returns a Matchup_stats object.
"""
matchup = self.matchups[matchup_id]
ms = Matchup_stats(self.results[matchup_id],
matchup.player_1, matchup.player_2)
ms.calculate_colour_breakdown()
ms.calculate_time_stats()
return ms
class Matchup_stats(object):
"""Result statistics for games between a pair of players.
Instantiate with
results -- list of gtp_games.Game_results
player_1 -- player code
player_2 -- player code
The game results should all be for games between player_1 and player_2.
Public attributes:
player_1 -- player code
player_2 -- player code
total -- int (number of games)
wins_1 -- float (score)
wins_2 -- float (score)
forfeits_1 -- int (number of games)
forfeits_2 -- int (number of games)
unknown -- int (number of games)
scores are multiples of 0.5 (as there may be jigos).
"""
def __init__(self, results, player_1, player_2):
self._results = results
self.player_1 = player_1
self.player_2 = player_2
self.total = len(results)
js = self._jigo_score = 0.5 * sum(r.is_jigo for r in results)
self.unknown = sum(r.winning_player is None and not r.is_jigo
for r in results)
self.wins_1 = sum(r.winning_player == player_1 for r in results) + js
self.wins_2 = sum(r.winning_player == player_2 for r in results) + js
self.forfeits_1 = sum(r.winning_player == player_2 and r.is_forfeit
for r in results)
self.forfeits_2 = sum(r.winning_player == player_1 and r.is_forfeit
for r in results)
def calculate_colour_breakdown(self):
"""Calculate futher statistics, broken down by colour played.
Sets the following additional attributes:
played_1b -- int (number of games)
played_1w -- int (number of games)
played_2b -- int (number of games)
played_y2 -- int (number of games)
alternating -- bool
when alternating is true =>
wins_b -- float (score)
wins_w -- float (score)
wins_1b -- float (score)
wins_1w -- float (score)
wins_2b -- float (score)
wins_2w -- float (score)
else =>
colour_1 -- 'b' or 'w'
colour_2 -- 'b' or 'w'
"""
results = self._results
player_1 = self.player_1
player_2 = self.player_2
js = self._jigo_score
self.played_1b = sum(r.player_b == player_1 for r in results)
self.played_1w = sum(r.player_w == player_1 for r in results)
self.played_2b = sum(r.player_b == player_2 for r in results)
self.played_y2 = sum(r.player_w == player_2 for r in results)
if self.played_1w == 0 and self.played_2b == 0:
self.alternating = False
self.colour_1 = 'b'
self.colour_2 = 'w'
elif self.played_1b == 0 and self.played_y2 == 0:
self.alternating = False
self.colour_1 = 'w'
self.colour_2 = 'b'
else:
self.alternating = True
self.wins_b = sum(r.winning_colour == 'b' for r in results) + js
self.wins_w = sum(r.winning_colour == 'w' for r in results) + js
self.wins_1b = sum(
r.winning_player == player_1 and r.winning_colour == 'b'
for r in results) + js
self.wins_1w = sum(
r.winning_player == player_1 and r.winning_colour == 'w'
for r in results) + js
self.wins_2b = sum(
r.winning_player == player_2 and r.winning_colour == 'b'
for r in results) + js
self.wins_2w = sum(
r.winning_player == player_2 and r.winning_colour == 'w'
for r in results) + js
def calculate_time_stats(self):
"""Calculate CPU time statistics.
average_time_1 -- float or None
average_time_2 -- float or None
"""
player_1 = self.player_1
player_2 = self.player_2
times_1 = [r.cpu_times[player_1] for r in self._results]
known_times_1 = [t for t in times_1 if t is not None and t != '?']
times_2 = [r.cpu_times[player_2] for r in self._results]
known_times_2 = [t for t in times_2 if t is not None and t != '?']
if known_times_1:
self.average_time_1 = sum(known_times_1) / len(known_times_1)
else:
self.average_time_1 = None
if known_times_2:
self.average_time_2 = sum(known_times_2) / len(known_times_2)
else:
self.average_time_2 = None
def make_matchup_stats_table(ms):
"""Produce an ascii table showing matchup statistics.
ms -- Matchup_stats (with all statistics set)
returns an ascii_tables.Table
"""
ff = format_float
pct = format_percent
t = ascii_tables.Table(row_count=3)
t.add_heading("") # player name
i = t.add_column(align='left', right_padding=3)
t.set_column_values(i, [ms.player_1, ms.player_2])
t.add_heading("wins")
i = t.add_column(align='right')
t.set_column_values(i, [ff(ms.wins_1), ff(ms.wins_2)])
t.add_heading("") # overall pct
i = t.add_column(align='right')
t.set_column_values(i, [pct(ms.wins_1, ms.total),
pct(ms.wins_2, ms.total)])
if ms.alternating:
t.columns[i].right_padding = 7
t.add_heading("black", span=2)
i = t.add_column(align='left')
t.set_column_values(i, [ff(ms.wins_1b), ff(ms.wins_2b), ff(ms.wins_b)])
i = t.add_column(align='right', right_padding=5)
t.set_column_values(i, [pct(ms.wins_1b, ms.played_1b),
pct(ms.wins_2b, ms.played_2b),
pct(ms.wins_b, ms.total)])
t.add_heading("white", span=2)
i = t.add_column(align='left')
t.set_column_values(i, [ff(ms.wins_1w), ff(ms.wins_2w), ff(ms.wins_w)])
i = t.add_column(align='right', right_padding=3)
t.set_column_values(i, [pct(ms.wins_1w, ms.played_1w),
pct(ms.wins_2w, ms.played_y2),
pct(ms.wins_w, ms.total)])
else:
t.columns[i].right_padding = 3
t.add_heading("")
i = t.add_column(align='left')
t.set_column_values(i, ["(%s)" % colour_name(ms.colour_1),
"(%s)" % colour_name(ms.colour_2)])
if ms.forfeits_1 or ms.forfeits_2:
t.add_heading("forfeits")
i = t.add_column(align='right')
t.set_column_values(i, [ms.forfeits_1, ms.forfeits_2])
if ms.average_time_1 or ms.average_time_2:
if ms.average_time_1 is not None:
avg_time_1_s = "%7.2f" % ms.average_time_1
else:
avg_time_1_s = " ----"
if ms.average_time_2 is not None:
avg_time_2_s = "%7.2f" % ms.average_time_2
else:
avg_time_2_s = " ----"
t.add_heading("avg cpu")
i = t.add_column(align='right', right_padding=2)
t.set_column_values(i, [avg_time_1_s, avg_time_2_s])
return t
def write_matchup_summary(out, matchup, ms):
"""Write a summary block for the specified matchup to 'out'.
matchup -- Matchup_description
ms -- Matchup_stats (with all statistics set)
"""
def p(s):
print >>out, s
if matchup.number_of_games is None:
played_s = "%d" % ms.total
else:
played_s = "%d/%d" % (ms.total, matchup.number_of_games)
p("%s (%s games)" % (matchup.name, played_s))
if ms.unknown > 0:
p("unknown results: %d %s" %
(ms.unknown, format_percent(ms.unknown, ms.total)))
p(matchup.describe_details())
p("\n".join(make_matchup_stats_table(ms).render()))