317 lines
12 KiB
Python
317 lines
12 KiB
Python
"""Common code for all tournament types."""
|
|
|
|
from collections import defaultdict
|
|
|
|
from gomill import game_jobs
|
|
from gomill import competition_schedulers
|
|
from gomill import tournament_results
|
|
from gomill import competitions
|
|
from gomill.competitions import (
|
|
Competition, NoGameAvailable, CompetitionError, ControlFileError)
|
|
from gomill.settings import *
|
|
from gomill.utils import format_percent
|
|
|
|
# These all appear as Matchup_description attributes
|
|
matchup_settings = competitions.game_settings + [
|
|
Setting('alternating', interpret_bool, default=False),
|
|
Setting('number_of_games', allow_none(interpret_int), default=None),
|
|
]
|
|
|
|
|
|
class Matchup(tournament_results.Matchup_description):
|
|
"""Internal description of a matchup from the configuration file.
|
|
|
|
See tournament_results.Matchup_description for main public attributes.
|
|
|
|
Additional attributes:
|
|
event_description -- string to show as sgf event
|
|
|
|
Instantiate with
|
|
matchup_id -- identifier
|
|
player_1 -- player code
|
|
player_2 -- player code
|
|
parameters -- dict matchup setting name -> value
|
|
name -- utf-8 string, or None
|
|
event_code -- identifier
|
|
|
|
If a matchup setting is missing from 'parameters', the setting's default
|
|
value is used (or, if there is no default value, ValueError is raised).
|
|
|
|
'event_code' is used for the sgf event description (combined with 'name'
|
|
if available).
|
|
|
|
Instantiation raises ControlFileError if the handicap settings aren't
|
|
permitted.
|
|
|
|
"""
|
|
def __init__(self, matchup_id, player_1, player_2, parameters,
|
|
name, event_code):
|
|
self.id = matchup_id
|
|
self.player_1 = player_1
|
|
self.player_2 = player_2
|
|
|
|
for setting in matchup_settings:
|
|
try:
|
|
v = parameters[setting.name]
|
|
except KeyError:
|
|
try:
|
|
v = setting.get_default()
|
|
except KeyError:
|
|
raise ValueError("'%s' not specified" % setting.name)
|
|
setattr(self, setting.name, v)
|
|
|
|
competitions.validate_handicap(
|
|
self.handicap, self.handicap_style, self.board_size)
|
|
|
|
if name is None:
|
|
name = "%s v %s" % (self.player_1, self.player_2)
|
|
event_description = event_code
|
|
else:
|
|
event_description = "%s (%s)" % (event_code, name)
|
|
self.name = name
|
|
self.event_description = event_description
|
|
self._game_id_template = ("%s_" +
|
|
competitions.leading_zero_template(self.number_of_games))
|
|
|
|
def make_game_id(self, game_number):
|
|
return self._game_id_template % (self.id, game_number)
|
|
|
|
|
|
class Ghost_matchup(object):
|
|
"""Dummy Matchup object for matchups which have gone from the control file.
|
|
|
|
This is used if the matchup appears in results.
|
|
|
|
It has to be a good enough imitation to keep write_matchup_summary() happy.
|
|
|
|
"""
|
|
def __init__(self, matchup_id, player_1, player_2):
|
|
self.id = matchup_id
|
|
self.player_1 = player_1
|
|
self.player_2 = player_2
|
|
self.name = "%s v %s" % (player_1, player_2)
|
|
self.number_of_games = None
|
|
|
|
def describe_details(self):
|
|
return "?? (missing from control file)"
|
|
|
|
|
|
class Tournament(Competition):
|
|
"""A Competition based on a number of matchups.
|
|
|
|
"""
|
|
def __init__(self, competition_code, **kwargs):
|
|
Competition.__init__(self, competition_code, **kwargs)
|
|
self.working_matchups = set()
|
|
self.probationary_matchups = set()
|
|
|
|
def make_matchup(self, matchup_id, player_1, player_2, parameters,
|
|
name=None):
|
|
"""Make a Matchup from the various parameters.
|
|
|
|
Raises ControlFileError if any required parameters are missing.
|
|
|
|
See Matchup.__init__ for details.
|
|
|
|
"""
|
|
try:
|
|
return Matchup(matchup_id, player_1, player_2, parameters, name,
|
|
event_code=self.competition_code)
|
|
except ValueError, e:
|
|
raise ControlFileError(str(e))
|
|
|
|
|
|
# State attributes (*: in persistent state):
|
|
# *results -- map matchup id -> list of Game_results
|
|
# *scheduler -- Group_scheduler (group codes are matchup ids)
|
|
# *engine_names -- map player code -> string
|
|
# *engine_descriptions -- map player code -> string
|
|
# working_matchups -- set of matchup ids
|
|
# (matchups which have successfully completed a game in this run)
|
|
# probationary_matchups -- set of matchup ids
|
|
# (matchups which failed to complete their last game)
|
|
# ghost_matchups -- map matchup id -> Ghost_matchup
|
|
# (matchups which have been removed from the control file)
|
|
|
|
def _check_results(self):
|
|
"""Check that the current results are consistent with the control file.
|
|
|
|
This is run when reloading state.
|
|
|
|
Raises CompetitionError if they're not.
|
|
|
|
(In general, control file changes are permitted. The only thing we
|
|
reject is results for a currently-defined matchup whose players aren't
|
|
correct.)
|
|
|
|
"""
|
|
# We guarantee that results for a given matchup always have consistent
|
|
# players, so we need only check the first result.
|
|
for matchup in self.matchup_list:
|
|
results = self.results[matchup.id]
|
|
if not results:
|
|
continue
|
|
result = results[0]
|
|
seen_players = sorted(result.players.itervalues())
|
|
expected_players = sorted((matchup.player_1, matchup.player_2))
|
|
if seen_players != expected_players:
|
|
raise CompetitionError(
|
|
"existing results for matchup %s "
|
|
"are inconsistent with control file:\n"
|
|
"result players are %s;\n"
|
|
"control file players are %s" %
|
|
(matchup.id,
|
|
",".join(seen_players), ",".join(expected_players)))
|
|
|
|
def _set_ghost_matchups(self):
|
|
self.ghost_matchups = {}
|
|
live = set(self.matchups)
|
|
for matchup_id, results in self.results.iteritems():
|
|
if matchup_id in live:
|
|
continue
|
|
result = results[0]
|
|
# player_1 and player_2 might not be the right way round, but it
|
|
# doesn't matter.
|
|
self.ghost_matchups[matchup_id] = Ghost_matchup(
|
|
matchup_id, result.player_b, result.player_w)
|
|
|
|
def _set_scheduler_groups(self):
|
|
self.scheduler.set_groups(
|
|
[(m.id, m.number_of_games) for m in self.matchup_list] +
|
|
[(id, 0) for id in self.ghost_matchups])
|
|
|
|
def set_clean_status(self):
|
|
self.results = defaultdict(list)
|
|
self.engine_names = {}
|
|
self.engine_descriptions = {}
|
|
self.scheduler = competition_schedulers.Group_scheduler()
|
|
self.ghost_matchups = {}
|
|
self._set_scheduler_groups()
|
|
|
|
def get_status(self):
|
|
return {
|
|
'results' : self.results,
|
|
'scheduler' : self.scheduler,
|
|
'engine_names' : self.engine_names,
|
|
'engine_descriptions' : self.engine_descriptions,
|
|
}
|
|
|
|
def set_status(self, status):
|
|
self.results = status['results']
|
|
self._check_results()
|
|
self._set_ghost_matchups()
|
|
self.scheduler = status['scheduler']
|
|
self._set_scheduler_groups()
|
|
self.scheduler.rollback()
|
|
self.engine_names = status['engine_names']
|
|
self.engine_descriptions = status['engine_descriptions']
|
|
|
|
|
|
def get_game(self):
|
|
matchup_id, game_number = self.scheduler.issue()
|
|
if matchup_id is None:
|
|
return NoGameAvailable
|
|
matchup = self.matchups[matchup_id]
|
|
if matchup.alternating and (game_number % 2):
|
|
player_b, player_w = matchup.player_2, matchup.player_1
|
|
else:
|
|
player_b, player_w = matchup.player_1, matchup.player_2
|
|
game_id = matchup.make_game_id(game_number)
|
|
|
|
job = game_jobs.Game_job()
|
|
job.game_id = game_id
|
|
job.game_data = (matchup_id, game_number)
|
|
job.player_b = self.players[player_b]
|
|
job.player_w = self.players[player_w]
|
|
job.board_size = matchup.board_size
|
|
job.komi = matchup.komi
|
|
job.move_limit = matchup.move_limit
|
|
job.handicap = matchup.handicap
|
|
job.handicap_is_free = (matchup.handicap_style == 'free')
|
|
job.use_internal_scorer = (matchup.scorer == 'internal')
|
|
job.internal_scorer_handicap_compensation = \
|
|
matchup.internal_scorer_handicap_compensation
|
|
job.sgf_event = matchup.event_description
|
|
return job
|
|
|
|
def process_game_result(self, response):
|
|
self.engine_names.update(response.engine_names)
|
|
self.engine_descriptions.update(response.engine_descriptions)
|
|
matchup_id, game_number = response.game_data
|
|
game_id = response.game_id
|
|
self.working_matchups.add(matchup_id)
|
|
self.probationary_matchups.discard(matchup_id)
|
|
self.scheduler.fix(matchup_id, game_number)
|
|
self.results[matchup_id].append(response.game_result)
|
|
self.log_history("%7s %s" % (game_id, response.game_result.describe()))
|
|
|
|
def process_game_error(self, job, previous_error_count):
|
|
# ignoring previous_error_count, as we can consider all jobs for the
|
|
# same matchup to be equivalent.
|
|
stop_competition = False
|
|
retry_game = False
|
|
matchup_id, game_data = job.game_data
|
|
if (matchup_id not in self.working_matchups or
|
|
matchup_id in self.probationary_matchups):
|
|
stop_competition = True
|
|
else:
|
|
self.probationary_matchups.add(matchup_id)
|
|
retry_game = True
|
|
return stop_competition, retry_game
|
|
|
|
def write_matchup_report(self, out, matchup, results):
|
|
"""Write the summary block for the specified matchup to 'out'
|
|
|
|
results -- nonempty list of Game_results
|
|
|
|
"""
|
|
# The control file might have changed since the results were recorded.
|
|
# We are guaranteed that the player codes correspond, but nothing else.
|
|
|
|
# We use the current matchup to describe 'background' information, as
|
|
# that isn't available any other way, but we look to the results where
|
|
# we can.
|
|
|
|
ms = tournament_results.Matchup_stats(
|
|
results, matchup.player_1, matchup.player_2)
|
|
ms.calculate_colour_breakdown()
|
|
ms.calculate_time_stats()
|
|
tournament_results.write_matchup_summary(out, matchup, ms)
|
|
|
|
def write_matchup_reports(self, out):
|
|
"""Write summary blocks for all live matchups to 'out'.
|
|
|
|
This doesn't include ghost matchups, or matchups with no games.
|
|
|
|
"""
|
|
first = True
|
|
for matchup in self.matchup_list:
|
|
results = self.results[matchup.id]
|
|
if not results:
|
|
continue
|
|
if first:
|
|
first = False
|
|
else:
|
|
print >>out
|
|
self.write_matchup_report(out, matchup, results)
|
|
|
|
def write_ghost_matchup_reports(self, out):
|
|
"""Write summary blocks for all ghost matchups to 'out'.
|
|
|
|
(This may produce no output. Starts with a blank line otherwise.)
|
|
|
|
"""
|
|
for matchup_id, matchup in sorted(self.ghost_matchups.iteritems()):
|
|
print >>out
|
|
results = self.results[matchup_id]
|
|
self.write_matchup_report(out, matchup, results)
|
|
|
|
def write_player_descriptions(self, out):
|
|
"""Write descriptions of all players to 'out'."""
|
|
for code, description in sorted(self.engine_descriptions.items()):
|
|
print >>out, ("player %s: %s" % (code, description))
|
|
|
|
def get_tournament_results(self):
|
|
return tournament_results.Tournament_results(
|
|
self.matchup_list, self.results)
|
|
|