Included gomill framework for SGF and GTP support, and sketched out SGF game-loading code.
This commit is contained in:
470
gomill/examples/gomill-clop
Executable file
470
gomill/examples/gomill-clop
Executable file
@ -0,0 +1,470 @@
|
||||
#!/usr/bin/env python
|
||||
"""Gomill integration for CLOP.
|
||||
|
||||
Designed for use with CLOP 0.0.8, available from
|
||||
http://remi.coulom.free.fr/CLOP/
|
||||
|
||||
"""
|
||||
|
||||
# The 'connection script' interface is as follows:
|
||||
# - the command-line arguments are:
|
||||
# - any arguments specified in the Script line
|
||||
# - processor
|
||||
# - seed
|
||||
# - then pairs of arguments (parameter name, parameter value)
|
||||
# - the connection script prints a single character to stdout:
|
||||
# 'W' for candidate win, 'D' for draw, 'L' for loss
|
||||
# any further output is ignored
|
||||
# - if the output doesn't start with 'W', 'D', or 'L', it's treated as an error,
|
||||
# and the complete stdout and stderr are reported
|
||||
# (by convention, print "Error:" with a description)
|
||||
# - the connection script's exit status is ignored
|
||||
|
||||
import os
|
||||
import sys
|
||||
from optparse import OptionParser
|
||||
|
||||
from gomill import compact_tracebacks
|
||||
from gomill import competitions
|
||||
from gomill import game_jobs
|
||||
from gomill.competitions import (
|
||||
Competition, CompetitionError, ControlFileError, Player_config)
|
||||
from gomill.job_manager import JobFailed
|
||||
from gomill.ringmasters import (
|
||||
Ringmaster, RingmasterError, RingmasterInternalError)
|
||||
from gomill.settings import *
|
||||
from gomill.common import opponent_of
|
||||
|
||||
|
||||
PARAMETER_TYPES = [
|
||||
"LinearParameter",
|
||||
"IntegerParameter",
|
||||
"GammaParameter",
|
||||
"IntegerGammaParameter",
|
||||
]
|
||||
|
||||
parameter_settings = [
|
||||
Setting('code', interpret_identifier),
|
||||
Setting('type', interpret_enum(*PARAMETER_TYPES)),
|
||||
Setting('min', interpret_float),
|
||||
Setting('max', interpret_float),
|
||||
]
|
||||
|
||||
class Parameter_config(Quiet_config):
|
||||
"""Parameter (ie, dimension) description for use in control files."""
|
||||
# positional or keyword
|
||||
positional_arguments = ('code',)
|
||||
# keyword-only
|
||||
keyword_arguments = tuple(setting.name for setting in parameter_settings
|
||||
if setting.name != 'code')
|
||||
|
||||
class Parameter_spec(object):
|
||||
"""Internal description of a parameter spec from the configuration file.
|
||||
|
||||
Public attributes:
|
||||
code -- identifier
|
||||
|
||||
"""
|
||||
def format_for_clop(self):
|
||||
"""Return the parameter configuration string for the .clop file."""
|
||||
if self.is_integer:
|
||||
fmt = "%s %s %d %d"
|
||||
else:
|
||||
fmt = "%s %s %f %f"
|
||||
return fmt % (self.type, self.code, self.min, self.max)
|
||||
|
||||
def interpret_value(self, s):
|
||||
"""Convert CLOP command-line parameter to an engine parameter.
|
||||
|
||||
Returns an int or float.
|
||||
|
||||
"""
|
||||
if self.is_integer:
|
||||
return int(s)
|
||||
else:
|
||||
return float(s)
|
||||
|
||||
def format_for_display(self, v):
|
||||
return str(v)
|
||||
|
||||
class Clop_tuner(Competition):
|
||||
|
||||
def control_file_globals(self):
|
||||
result = Competition.control_file_globals(self)
|
||||
result.update({
|
||||
'Parameter' : Parameter_config,
|
||||
})
|
||||
return result
|
||||
|
||||
global_settings = (Competition.global_settings +
|
||||
competitions.game_settings + [
|
||||
Setting('candidate_colour', interpret_colour),
|
||||
Setting('parallel', interpret_int, default=1),
|
||||
Setting('clop_H', interpret_float, default=3),
|
||||
Setting('correlations', interpret_enum('all', 'none'), default='all'),
|
||||
Setting('stop_on_error', interpret_bool, default=True),
|
||||
])
|
||||
|
||||
special_settings = [
|
||||
Setting('opponent', interpret_identifier),
|
||||
Setting('parameters',
|
||||
interpret_sequence_of_quiet_configs(Parameter_config)),
|
||||
Setting('make_candidate', interpret_callable),
|
||||
]
|
||||
|
||||
def parameter_spec_from_config(self, parameter_config):
|
||||
"""Make a Parameter_spec from a Parameter_config.
|
||||
|
||||
Raises ControlFileError if there is an error in the configuration.
|
||||
|
||||
Returns a Parameter_spec with all attributes set.
|
||||
|
||||
"""
|
||||
arguments = parameter_config.resolve_arguments()
|
||||
interpreted = load_settings(parameter_settings, arguments)
|
||||
pspec = Parameter_spec()
|
||||
for name, value in interpreted.iteritems():
|
||||
setattr(pspec, name, value)
|
||||
pspec.is_integer = ("Integer" in pspec.type)
|
||||
if pspec.is_integer:
|
||||
if pspec.min != int(pspec.min):
|
||||
raise ControlFileError("'min': should be an integer")
|
||||
if pspec.max != int(pspec.max):
|
||||
raise ControlFileError("'max': should be an integer")
|
||||
return pspec
|
||||
|
||||
def initialise_from_control_file(self, config):
|
||||
Competition.initialise_from_control_file(self, config)
|
||||
|
||||
competitions.validate_handicap(
|
||||
self.handicap, self.handicap_style, self.board_size)
|
||||
|
||||
try:
|
||||
specials = load_settings(self.special_settings, config)
|
||||
except ValueError, e:
|
||||
raise ControlFileError(str(e))
|
||||
|
||||
try:
|
||||
self.opponent = self.players[specials['opponent']]
|
||||
except KeyError:
|
||||
raise ControlFileError(
|
||||
"opponent: unknown player %s" % specials['opponent'])
|
||||
|
||||
self.parameter_specs = []
|
||||
if not specials['parameters']:
|
||||
raise ControlFileError("parameters: empty list")
|
||||
seen_codes = set()
|
||||
for i, parameter_spec in enumerate(specials['parameters']):
|
||||
try:
|
||||
pspec = self.parameter_spec_from_config(parameter_spec)
|
||||
except StandardError, e:
|
||||
code = parameter_spec.get_key()
|
||||
if code is None:
|
||||
code = i
|
||||
raise ControlFileError("parameter %s: %s" % (code, e))
|
||||
if pspec.code in seen_codes:
|
||||
raise ControlFileError(
|
||||
"duplicate parameter code: %s" % pspec.code)
|
||||
seen_codes.add(pspec.code)
|
||||
self.parameter_specs.append(pspec)
|
||||
self.candidate_maker_fn = specials['make_candidate']
|
||||
|
||||
def get_clop_parameter_specs(self):
|
||||
"""Describe the parameters in the format used in the .clop file.
|
||||
|
||||
Returns a list of strings.
|
||||
|
||||
"""
|
||||
return [pspec.format_for_clop() for pspec in self.parameter_specs]
|
||||
|
||||
def interpret_clop_parameters(self, clop_parameters):
|
||||
"""Convert the CLOP command-line parameters to engine parameters.
|
||||
|
||||
clop_parameters -- list of pairs of strings
|
||||
(parameter name, parameter value)
|
||||
|
||||
Returns a list of engine parameters, suitable for passing to
|
||||
make_candidate().
|
||||
|
||||
"""
|
||||
engine_parameters = []
|
||||
try:
|
||||
if len(clop_parameters) != len(self.parameter_specs):
|
||||
raise ValueError
|
||||
for pspec, (name, value) in \
|
||||
zip(self.parameter_specs, clop_parameters):
|
||||
if name != pspec.code:
|
||||
raise ValueError
|
||||
engine_parameters.append(pspec.interpret_value(value))
|
||||
return engine_parameters
|
||||
except ValueError:
|
||||
raise CompetitionError(
|
||||
"couldn't interpret parameters: %s" % repr(clop_parameters))
|
||||
|
||||
def format_engine_parameters(self, engine_parameters):
|
||||
return "; ".join(
|
||||
"%s %s" % (pspec.code, pspec.format_for_display(v))
|
||||
for pspec, v in zip(self.parameter_specs, engine_parameters))
|
||||
|
||||
def make_candidate(self, player_code, engine_parameters):
|
||||
"""Make a player using the specified engine parameters.
|
||||
|
||||
Returns a game_jobs.Player.
|
||||
|
||||
"""
|
||||
try:
|
||||
candidate_config = self.candidate_maker_fn(*engine_parameters)
|
||||
except Exception:
|
||||
raise CompetitionError(
|
||||
"error from make_candidate()\n%s" %
|
||||
compact_tracebacks.format_traceback(skip=1))
|
||||
if not isinstance(candidate_config, Player_config):
|
||||
raise CompetitionError(
|
||||
"make_candidate() returned %r, not Player" %
|
||||
candidate_config)
|
||||
try:
|
||||
candidate = self.game_jobs_player_from_config(
|
||||
player_code, candidate_config)
|
||||
except Exception, e:
|
||||
raise CompetitionError(
|
||||
"bad player spec from make_candidate():\n"
|
||||
"%s\nparameters were: %s" %
|
||||
(e, self.format_engine_parameters(engine_parameters)))
|
||||
return candidate
|
||||
|
||||
def get_game_for_parameters(self, clop_seed, clop_parameters):
|
||||
"""Return the details of the next game to play.
|
||||
|
||||
clop_seed -- second command-line parameter passed by clop
|
||||
clop_parameters -- remaining command-line parameters passed by clop.
|
||||
|
||||
This is like Competition.get_game(), but it never returns
|
||||
NoGameAvailable.
|
||||
|
||||
"""
|
||||
engine_parameters = self.interpret_clop_parameters(clop_parameters)
|
||||
|
||||
candidate = self.make_candidate("#%s" % clop_seed, engine_parameters)
|
||||
job = game_jobs.Game_job()
|
||||
job.game_id = clop_seed
|
||||
if self.candidate_colour == 'b':
|
||||
job.player_b = candidate
|
||||
job.player_w = self.opponent
|
||||
else:
|
||||
job.player_b = self.opponent
|
||||
job.player_w = candidate
|
||||
job.board_size = self.board_size
|
||||
job.komi = self.komi
|
||||
job.move_limit = self.move_limit
|
||||
job.handicap = self.handicap
|
||||
job.handicap_is_free = (self.handicap_style == 'free')
|
||||
job.use_internal_scorer = (self.scorer == 'internal')
|
||||
job.internal_scorer_handicap_compensation = \
|
||||
self.internal_scorer_handicap_compensation
|
||||
job.sgf_event = self.competition_code
|
||||
job.sgf_note = ("Candidate parameters: %s" %
|
||||
self.format_engine_parameters(engine_parameters))
|
||||
return job
|
||||
|
||||
|
||||
|
||||
class Clop_ringmaster(Ringmaster):
|
||||
def __init__(self, *args, **kwargs):
|
||||
Ringmaster.__init__(self, *args, **kwargs)
|
||||
# clop uses .log, so we need something different
|
||||
self.log_pathname = os.path.join(
|
||||
self.base_directory, self.competition_code) + ".elog"
|
||||
|
||||
@staticmethod
|
||||
def _get_competition_class(competition_type):
|
||||
if competition_type == "clop_tuner":
|
||||
return Clop_tuner
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
def ensure_output_directories(self):
|
||||
if self.record_games:
|
||||
try:
|
||||
if not os.path.exists(self.sgf_dir_pathname):
|
||||
os.mkdir(self.sgf_dir_pathname)
|
||||
except EnvironmentError:
|
||||
raise RingmasterError("failed to create SGF directory:\n%s" % e)
|
||||
|
||||
if self.write_gtp_logs:
|
||||
try:
|
||||
if not os.path.exists(self.gtplog_dir_pathname):
|
||||
os.mkdir(self.gtplog_dir_pathname)
|
||||
except EnvironmentError:
|
||||
raise RingmasterError(
|
||||
"failed to create GTP log directory:\n%s" % e)
|
||||
|
||||
def open_logfile(self):
|
||||
try:
|
||||
self.logfile = open(self.log_pathname, "a")
|
||||
except EnvironmentError, e:
|
||||
raise RingmasterError("failed to open log file:\n%s" % e)
|
||||
|
||||
def run_game_for_clop(self, seed, parameters):
|
||||
"""Act as a CLOP connection script.
|
||||
|
||||
seed -- seed string passed by clop
|
||||
parameters -- list of pairs of strings (parameter name, parameter value)
|
||||
|
||||
Returns the message to print.
|
||||
|
||||
"""
|
||||
self._initialise_presenter()
|
||||
try:
|
||||
job = self.competition.get_game_for_parameters(seed, parameters)
|
||||
self._prepare_job(job)
|
||||
start_msg = "starting game %s: %s (b) vs %s (w)" % (
|
||||
job.game_id, job.player_b.code, job.player_w.code)
|
||||
self.log(start_msg)
|
||||
response = job.run()
|
||||
self.log("response from game %s" % response.game_id)
|
||||
for warning in response.warnings:
|
||||
self.warn(warning)
|
||||
for log_entry in response.log_entries:
|
||||
self.log(log_entry)
|
||||
except (CompetitionError, JobFailed), e:
|
||||
raise RingmasterError(e)
|
||||
result = response.game_result
|
||||
candidate_colour = self.competition.candidate_colour
|
||||
if result.winning_colour == candidate_colour:
|
||||
message = "W"
|
||||
elif result.winning_colour == opponent_of(candidate_colour):
|
||||
message = "L"
|
||||
elif result.is_jigo:
|
||||
message = "D"
|
||||
else:
|
||||
if self.competition.stop_on_error:
|
||||
# Don't want the experiment to stop just because a single game
|
||||
# failed (eg, went over the move limit), so treat it as a draw.
|
||||
message = "D"
|
||||
else:
|
||||
raise RingmasterError("unexpected game result: %s" %
|
||||
result.sgf_result)
|
||||
return message
|
||||
|
||||
|
||||
|
||||
clop_template = """\
|
||||
Name %(experiment_name)s
|
||||
|
||||
Script %(connection_pathname)s %(control_filename)s run-game
|
||||
|
||||
%(parameter_specs)s
|
||||
|
||||
%(processor_specs)s
|
||||
|
||||
Replications 1
|
||||
|
||||
DrawElo %(drawelo)s
|
||||
|
||||
H %(clop_H)s
|
||||
|
||||
Correlations %(correlations)s
|
||||
|
||||
StopOnError %(stop_on_error)s
|
||||
|
||||
"""
|
||||
|
||||
def do_setup(ringmaster, arguments, options):
|
||||
"""Create the .clop file, and any needed directories."""
|
||||
connection_pathname = os.path.abspath(__file__)
|
||||
control_filename = os.path.basename(ringmaster.control_pathname)
|
||||
clop_pathname = os.path.join(ringmaster.base_directory,
|
||||
"%s.clop" % ringmaster.competition_code)
|
||||
competition = ringmaster.competition
|
||||
experiment_name = ringmaster.competition_code
|
||||
|
||||
parameter_specs = "\n".join(competition.get_clop_parameter_specs())
|
||||
processor_specs = "\n".join(
|
||||
"Processor par%d" % i for i in xrange(competition.parallel))
|
||||
if competition.komi == int(competition.komi) or competition.stop_on_error:
|
||||
drawelo = "100"
|
||||
else:
|
||||
drawelo = "0"
|
||||
clop_H = competition.clop_H
|
||||
correlations = competition.correlations
|
||||
stop_on_error = "true" if competition.stop_on_error else "false"
|
||||
|
||||
with open(clop_pathname, "w") as f:
|
||||
f.write(clop_template % locals())
|
||||
|
||||
ringmaster.ensure_output_directories()
|
||||
|
||||
def do_run_game(ringmaster, arguments, options):
|
||||
"""Act as a CLOP connection script."""
|
||||
try:
|
||||
processor, seed = arguments[:2]
|
||||
except ValueError:
|
||||
raise RingmasterError("not enough connection script arguments")
|
||||
parameter_args = arguments[2:]
|
||||
parameters = []
|
||||
i = 0
|
||||
try:
|
||||
while i < len(parameter_args):
|
||||
parameters.append((parameter_args[i], parameter_args[i+1]))
|
||||
i += 2
|
||||
except LookupError:
|
||||
raise RingmasterError("parameter without value: %s" % parameter_args[i])
|
||||
ringmaster.set_display_mode('quiet')
|
||||
ringmaster.open_logfile()
|
||||
message = ringmaster.run_game_for_clop(seed, parameters)
|
||||
print message
|
||||
|
||||
_actions = {
|
||||
"setup" : (do_setup, False),
|
||||
"run-game" : (do_run_game, True),
|
||||
}
|
||||
|
||||
def main(argv):
|
||||
usage = ("%prog <control file> <command> [connection script arguments]\n\n"
|
||||
"commands: setup, run-game")
|
||||
description = (
|
||||
"`setup` generates a .clop file for use with clop-gui or clop-console. "
|
||||
"Then `run-game` is used (behind the scenes) as the connection script.")
|
||||
parser = OptionParser(usage=usage, description=description)
|
||||
(options, args) = parser.parse_args(argv)
|
||||
if len(args) == 0:
|
||||
parser.error("no control file specified")
|
||||
if len(args) == 1:
|
||||
parser.error("no command specified")
|
||||
command = args[1]
|
||||
command_args = args[2:]
|
||||
try:
|
||||
action, takes_arguments = _actions[command]
|
||||
except KeyError:
|
||||
parser.error("no such command: %s" % command)
|
||||
if command_args and not takes_arguments:
|
||||
parser.error("too many arguments for %s" % command)
|
||||
|
||||
ctl_pathname = args[0]
|
||||
try:
|
||||
if not os.path.exists(ctl_pathname):
|
||||
raise RingmasterError("control file %s not found" % ctl_pathname)
|
||||
ringmaster = Clop_ringmaster(ctl_pathname)
|
||||
action(ringmaster, command_args, options)
|
||||
exit_status = 0
|
||||
except RingmasterError, e:
|
||||
print >>sys.stderr, "gomill-clop:", e
|
||||
exit_status = 1
|
||||
except KeyboardInterrupt:
|
||||
exit_status = 3
|
||||
except RingmasterInternalError, e:
|
||||
print >>sys.stderr, "gomill-clop: internal error"
|
||||
print >>sys.stderr, e
|
||||
exit_status = 4
|
||||
except:
|
||||
print >>sys.stderr, "gomill-clop: internal error"
|
||||
compact_tracebacks.log_traceback()
|
||||
exit_status = 4
|
||||
if exit_status != 0 and command == 'run-game':
|
||||
# Make sure the problem runner sees an error response
|
||||
print "Error"
|
||||
sys.exit(exit_status)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv[1:])
|
||||
|
Reference in New Issue
Block a user