Included gomill framework for SGF and GTP support, and sketched out SGF game-loading code.
This commit is contained in:
78
gomill/examples/clop_example.ctl
Normal file
78
gomill/examples/clop_example.ctl
Normal file
@ -0,0 +1,78 @@
|
||||
# The following settings are supported:
|
||||
#
|
||||
# - all _common settings_
|
||||
#
|
||||
# - all _game settings_
|
||||
#
|
||||
# - tuning event settings (cf mcts_tuner):
|
||||
# - candidate_colour
|
||||
# - opponent
|
||||
# - parameters
|
||||
# - make_candidate
|
||||
#
|
||||
# - settings for experiment control
|
||||
# - parallel -- number of games to run in parallel
|
||||
# - stop_on_error -- boolean
|
||||
#
|
||||
# - regression parameters:
|
||||
# - clop_H -- float
|
||||
# - correlations -- 'all' (default) or 'none'
|
||||
#
|
||||
## <<
|
||||
# clop_H: 3 is recommended (it is the default value)
|
||||
# correlations:
|
||||
# Even if variables are not correlated "all" should work well. The problem is
|
||||
# that the regression might become very costly if the number of variables is
|
||||
# high. So use "correlations none" only if you are certain parameters are
|
||||
# independent or you have so many variables that "all" is too costly.
|
||||
## >>
|
||||
#
|
||||
|
||||
# The available parameter types are:
|
||||
# LinearParameter
|
||||
# IntegerParameter
|
||||
# GammaParameter
|
||||
# IntegerGammaParameter
|
||||
# For GammaParameter, quadratic regression is performed on log(x)
|
||||
|
||||
competition_type = "clop_tuner"
|
||||
|
||||
description = """\
|
||||
Sample control file for CLOP integration.
|
||||
|
||||
"""
|
||||
|
||||
def gnugo(level):
|
||||
return Player("gnugo --mode=gtp --chinese-rules --capture-all-dead "
|
||||
"--level=%d" % level)
|
||||
|
||||
def pachi(playouts, policy):
|
||||
return Player(
|
||||
"~/src/pachi/pachi "
|
||||
"-d 0 " # silence stderr
|
||||
"-t =%d "
|
||||
"threads=1,max_tree_size=2048 "
|
||||
"policy=%s "
|
||||
% (playouts, policy))
|
||||
|
||||
players = {
|
||||
'gnugo-l7' : gnugo(7),
|
||||
}
|
||||
|
||||
|
||||
parameters = [
|
||||
Parameter('equiv_rave',
|
||||
type = "GammaParameter",
|
||||
min = 40,
|
||||
max = 32000),
|
||||
]
|
||||
|
||||
def make_candidate(equiv_rave):
|
||||
return pachi(2000, policy="ucb1amaf:equiv_rave=%f" % equiv_rave)
|
||||
|
||||
board_size = 19
|
||||
komi = 7.5
|
||||
opponent = 'gnugo-l7'
|
||||
candidate_colour = 'w'
|
||||
parallel = 2
|
||||
|
54
gomill/examples/find_forfeits.py
Normal file
54
gomill/examples/find_forfeits.py
Normal file
@ -0,0 +1,54 @@
|
||||
"""Find forfeited games in tournament results.
|
||||
|
||||
This demonstrates retrieving and processing results from a tournament.
|
||||
|
||||
"""
|
||||
|
||||
import sys
|
||||
from optparse import OptionParser
|
||||
|
||||
from gomill.common import opponent_of
|
||||
from gomill.ringmasters import Ringmaster, RingmasterError
|
||||
|
||||
def show_result(matchup, result, filename):
|
||||
print "%s: %s forfeited game %s" % (
|
||||
matchup.name, result.losing_player, filename)
|
||||
|
||||
def find_forfeits(ringmaster):
|
||||
ringmaster.load_status()
|
||||
tournament_results = ringmaster.get_tournament_results()
|
||||
matchup_ids = tournament_results.get_matchup_ids()
|
||||
for matchup_id in matchup_ids:
|
||||
matchup = tournament_results.get_matchup(matchup_id)
|
||||
results = tournament_results.get_matchup_results(matchup_id)
|
||||
for result in results:
|
||||
if result.is_forfeit:
|
||||
filename = ringmaster.get_sgf_filename(result.game_id)
|
||||
show_result(matchup, result, filename)
|
||||
|
||||
|
||||
_description = """\
|
||||
Read results of a tournament and show all forfeited games.
|
||||
"""
|
||||
|
||||
def main(argv):
|
||||
parser = OptionParser(usage="%prog <filename.ctl>",
|
||||
description=_description)
|
||||
opts, args = parser.parse_args(argv)
|
||||
if not args:
|
||||
parser.error("not enough arguments")
|
||||
if len(args) > 1:
|
||||
parser.error("too many arguments")
|
||||
ctl_pathname = args[0]
|
||||
try:
|
||||
ringmaster = Ringmaster(ctl_pathname)
|
||||
find_forfeits(ringmaster)
|
||||
except RingmasterError, e:
|
||||
print >>sys.stderr, "ringmaster:"
|
||||
print >>sys.stderr, e
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv[1:])
|
||||
|
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:])
|
||||
|
118
gomill/examples/gtp_stateful_player
Executable file
118
gomill/examples/gtp_stateful_player
Executable file
@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python
|
||||
"""GTP engine which maintains the board position.
|
||||
|
||||
This provides an example of a GTP engine using the gtp_states module.
|
||||
|
||||
It plays (and resigns) randomly.
|
||||
|
||||
It supports the following GTP commands, mostly provided by gtp_states:
|
||||
|
||||
Standard
|
||||
boardsize
|
||||
clear_board
|
||||
fixed_handicap
|
||||
genmove
|
||||
known_command
|
||||
komi
|
||||
list_commands
|
||||
loadsgf
|
||||
name
|
||||
place_free_handicap
|
||||
play
|
||||
protocol_version
|
||||
quit
|
||||
reg_genmove
|
||||
set_free_handicap
|
||||
showboard
|
||||
undo
|
||||
version
|
||||
|
||||
Gomill extensions
|
||||
gomill-explain_last_move
|
||||
gomill-genmove_ex
|
||||
gomill-savesgf
|
||||
|
||||
Examples
|
||||
gomill_resign_p <float> -- resign in future with the specified probabiltiy
|
||||
|
||||
"""
|
||||
|
||||
import random
|
||||
import sys
|
||||
|
||||
from gomill import gtp_engine
|
||||
from gomill import gtp_states
|
||||
|
||||
|
||||
|
||||
class Player(object):
|
||||
"""Player for use with gtp_state."""
|
||||
|
||||
def __init__(self):
|
||||
self.resign_probability = 0.1
|
||||
|
||||
def genmove(self, game_state, player):
|
||||
"""Move generator that chooses a random empty point.
|
||||
|
||||
game_state -- gtp_states.Game_state
|
||||
player -- 'b' or 'w'
|
||||
|
||||
This may return a self-capture move.
|
||||
|
||||
"""
|
||||
board = game_state.board
|
||||
empties = []
|
||||
for row, col in board.board_points:
|
||||
if board.get(row, col) is None:
|
||||
empties.append((row, col))
|
||||
result = gtp_states.Move_generator_result()
|
||||
if random.random() < self.resign_probability:
|
||||
result.resign = True
|
||||
else:
|
||||
result.move = random.choice(empties)
|
||||
# Used by gomill-explain_last_move and gomill-savesgf
|
||||
result.comments = "chosen at random from %d choices" % len(empties)
|
||||
return result
|
||||
|
||||
def handle_name(self, args):
|
||||
return "GTP stateful player"
|
||||
|
||||
def handle_version(self, args):
|
||||
return ""
|
||||
|
||||
def handle_resign_p(self, args):
|
||||
try:
|
||||
f = gtp_engine.interpret_float(args[0])
|
||||
except IndexError:
|
||||
gtp_engine.report_bad_arguments()
|
||||
self.resign_probability = f
|
||||
|
||||
def get_handlers(self):
|
||||
return {
|
||||
'name' : self.handle_name,
|
||||
'version' : self.handle_version,
|
||||
'gomill-resign_p' : self.handle_resign_p,
|
||||
}
|
||||
|
||||
|
||||
def make_engine(player):
|
||||
"""Return a Gtp_engine_protocol which runs the specified player."""
|
||||
gtp_state = gtp_states.Gtp_state(
|
||||
move_generator=player.genmove,
|
||||
acceptable_sizes=(9, 13, 19))
|
||||
engine = gtp_engine.Gtp_engine_protocol()
|
||||
engine.add_protocol_commands()
|
||||
engine.add_commands(gtp_state.get_handlers())
|
||||
engine.add_commands(player.get_handlers())
|
||||
return engine
|
||||
|
||||
def main():
|
||||
try:
|
||||
player = Player()
|
||||
engine = make_engine(player)
|
||||
gtp_engine.run_interactive_gtp_session(engine)
|
||||
except (KeyboardInterrupt, gtp_engine.ControllerDisconnected):
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
144
gomill/examples/gtp_test_player
Executable file
144
gomill/examples/gtp_test_player
Executable file
@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python
|
||||
"""GTP engine intended for testing GTP controllers.
|
||||
|
||||
This provides an example of a GTP engine which does not use the gtp_states
|
||||
module.
|
||||
|
||||
It supports the following GTP commands:
|
||||
|
||||
Standard
|
||||
boardsize
|
||||
clear_board
|
||||
genmove
|
||||
known_command
|
||||
komi
|
||||
list_commands
|
||||
name
|
||||
play
|
||||
protocol_version
|
||||
quit
|
||||
version
|
||||
|
||||
Extensions:
|
||||
gomill-force_error [error_type]
|
||||
gomill-delayed_error <move_number> [error_type]
|
||||
|
||||
gomill-force_error immediately causes an error. error_type can be any of the
|
||||
following:
|
||||
error -- return a GTP error response (this is the default)
|
||||
exit -- return a GTP error response and end the GTP session
|
||||
internal -- propagate a Python exception to the GTP engine code
|
||||
kill -- abruptly terminate the engine process
|
||||
protocol -- send an ill-formed GTP response
|
||||
|
||||
gomill-delayed_error causes a later genmove command to produce an error. This
|
||||
will happen the first time genmove is called for the move 'move_number' or
|
||||
later, counting from the start of the game.
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from gomill import gtp_engine
|
||||
from gomill.gtp_engine import GtpError, GtpFatalError
|
||||
|
||||
|
||||
class Test_player(object):
|
||||
"""GTP player used for testing controllers' error handling."""
|
||||
def __init__(self):
|
||||
self.delayed_error_move = None
|
||||
self.delayed_error_args = None
|
||||
self.move_count = 0
|
||||
|
||||
def handle_name(self, args):
|
||||
return "GTP test player"
|
||||
|
||||
def handle_version(self, args):
|
||||
return ""
|
||||
|
||||
def handle_genmove(self, args):
|
||||
"""Handler for the genmove command.
|
||||
|
||||
This honours gomill-delayed_error, and otherwise passes.
|
||||
|
||||
"""
|
||||
self.move_count += 1
|
||||
if (self.delayed_error_move and
|
||||
self.move_count >= self.delayed_error_move):
|
||||
self.delayed_error_move = None
|
||||
self.handle_force_error(self.delayed_error_args)
|
||||
return "pass"
|
||||
|
||||
def handle_play(self, args):
|
||||
self.move_count += 1
|
||||
|
||||
def handle_boardsize(self, args):
|
||||
pass
|
||||
|
||||
def handle_clear_board(self, args):
|
||||
pass
|
||||
|
||||
def handle_komi(self, args):
|
||||
pass
|
||||
|
||||
def handle_force_error(self, args):
|
||||
"""Handler for the gomill-force_error command."""
|
||||
try:
|
||||
arg = args[0]
|
||||
except IndexError:
|
||||
arg = "error"
|
||||
if arg == "error":
|
||||
raise GtpError("forced GTP error")
|
||||
if arg == "exit":
|
||||
raise GtpFatalError("forced GTP error; exiting")
|
||||
if arg == "internal":
|
||||
3 / 0
|
||||
if arg == "kill":
|
||||
os.kill(os.getpid(), 15)
|
||||
if arg == "protocol":
|
||||
sys.stdout.write("!! forced ill-formed GTP response\n")
|
||||
sys.stdout.flush()
|
||||
return
|
||||
raise GtpError("unknown force_error argument")
|
||||
|
||||
def handle_delayed_error(self, args):
|
||||
"""Handler for the gomill-delayed_error command."""
|
||||
try:
|
||||
move_number = gtp_engine.interpret_int(args[0])
|
||||
except IndexError:
|
||||
gtp_engine.report_bad_arguments()
|
||||
self.delayed_error_move = move_number
|
||||
self.delayed_error_args = args[1:]
|
||||
|
||||
def get_handlers(self):
|
||||
return {
|
||||
'name' : self.handle_name,
|
||||
'version' : self.handle_version,
|
||||
'genmove' : self.handle_genmove,
|
||||
'play' : self.handle_play,
|
||||
'boardsize' : self.handle_boardsize,
|
||||
'clear_board' : self.handle_clear_board,
|
||||
'komi' : self.handle_komi,
|
||||
'gomill-force_error' : self.handle_force_error,
|
||||
'gomill-delayed_error' : self.handle_delayed_error,
|
||||
}
|
||||
|
||||
|
||||
def make_engine(test_player):
|
||||
"""Return a Gtp_engine_protocol which runs the specified Test_player."""
|
||||
engine = gtp_engine.Gtp_engine_protocol()
|
||||
engine.add_protocol_commands()
|
||||
engine.add_commands(test_player.get_handlers())
|
||||
return engine
|
||||
|
||||
def main():
|
||||
try:
|
||||
test_player = Test_player()
|
||||
engine = make_engine(test_player)
|
||||
gtp_engine.run_interactive_gtp_session(engine)
|
||||
except (KeyboardInterrupt, gtp_engine.ControllerDisconnected):
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
146
gomill/examples/kgs_proxy.py
Normal file
146
gomill/examples/kgs_proxy.py
Normal file
@ -0,0 +1,146 @@
|
||||
"""GTP proxy for use with kgsGtp.
|
||||
|
||||
This supports saving a game record after each game, if the underlying engine
|
||||
supports gomill-savesgf.
|
||||
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from optparse import OptionParser
|
||||
|
||||
from gomill import gtp_engine
|
||||
from gomill import gtp_proxy
|
||||
from gomill.gtp_engine import GtpError
|
||||
from gomill.gtp_controller import BadGtpResponse
|
||||
|
||||
class Kgs_proxy(object):
|
||||
"""GTP proxy for use with kgsGtp.
|
||||
|
||||
Instantiate with command line arguments.
|
||||
|
||||
Calls sys.exit on fatal errors.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, command_line_args):
|
||||
parser = OptionParser(usage="%prog [options] <back end command> [args]")
|
||||
parser.disable_interspersed_args()
|
||||
parser.add_option("--sgf-dir", metavar="PATHNAME")
|
||||
parser.add_option("--filename-template", metavar="TEMPLATE",
|
||||
help="eg '%03d.sgf'")
|
||||
opts, args = parser.parse_args(command_line_args)
|
||||
|
||||
if not args:
|
||||
parser.error("must specify a command")
|
||||
self.subprocess_command = args
|
||||
|
||||
self.filename_template = "%04d.sgf"
|
||||
try:
|
||||
opts.filename_template % 3
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
self.filename_template = opts.filename_template
|
||||
|
||||
self.sgf_dir = opts.sgf_dir
|
||||
if self.sgf_dir:
|
||||
self.check_sgf_dir()
|
||||
self.do_savesgf = True
|
||||
else:
|
||||
self.do_savesgf = False
|
||||
|
||||
def log(self, s):
|
||||
print >>sys.stderr, s
|
||||
|
||||
def run(self):
|
||||
self.proxy = gtp_proxy.Gtp_proxy()
|
||||
try:
|
||||
self.proxy.set_back_end_subprocess(self.subprocess_command)
|
||||
self.proxy.engine.add_commands(
|
||||
{'genmove' : self.handle_genmove,
|
||||
'kgs-game_over' : self.handle_game_over,
|
||||
})
|
||||
if (self.do_savesgf and
|
||||
not self.proxy.back_end_has_command("gomill-savesgf")):
|
||||
sys.exit("kgs_proxy: back end doesn't support gomill-savesgf")
|
||||
|
||||
# Colour that we appear to be playing
|
||||
self.my_colour = None
|
||||
self.initialise_name()
|
||||
except gtp_proxy.BackEndError, e:
|
||||
sys.exit("kgs_proxy: %s" % e)
|
||||
try:
|
||||
self.proxy.run()
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(1)
|
||||
|
||||
def initialise_name(self):
|
||||
def shorten_version(name, version):
|
||||
"""Clean up redundant version strings."""
|
||||
if version.lower().startswith(name.lower()):
|
||||
version = version[len(name):].lstrip()
|
||||
# For MoGo's stupidly long version string
|
||||
a, b, c = version.partition(". Please read http:")
|
||||
if b:
|
||||
version = a
|
||||
return version[:32].rstrip()
|
||||
|
||||
self.my_name = None
|
||||
try:
|
||||
self.my_name = self.proxy.pass_command("name", [])
|
||||
version = self.proxy.pass_command("version", [])
|
||||
version = shorten_version(self.my_name, version)
|
||||
self.my_name += ":" + version
|
||||
except BadGtpResponse:
|
||||
pass
|
||||
|
||||
def handle_genmove(self, args):
|
||||
try:
|
||||
self.my_colour = gtp_engine.interpret_colour(args[0])
|
||||
except IndexError:
|
||||
gtp_engine.report_bad_arguments()
|
||||
return self.proxy.pass_command("genmove", args)
|
||||
|
||||
def check_sgf_dir(self):
|
||||
if not os.path.isdir(self.sgf_dir):
|
||||
sys.exit("kgs_proxy: can't find save game directory %s" %
|
||||
self.sgf_dir)
|
||||
|
||||
def choose_filename(self, existing):
|
||||
existing = set(existing)
|
||||
for i in xrange(10000):
|
||||
filename = self.filename_template % i
|
||||
if filename not in existing:
|
||||
return filename
|
||||
raise StandardError("too many sgf files")
|
||||
|
||||
def handle_game_over(self, args):
|
||||
"""Handler for kgs-game_over.
|
||||
|
||||
kgsGtp doesn't send any arguments, so we don't know the result.
|
||||
|
||||
"""
|
||||
def escape_for_savesgf(s):
|
||||
return s.replace("\\", "\\\\").replace(" ", "\\ ")
|
||||
|
||||
if self.do_savesgf:
|
||||
filename = self.choose_filename(os.listdir(self.sgf_dir))
|
||||
pathname = os.path.join(self.sgf_dir, filename)
|
||||
self.log("kgs_proxy: saving game record to %s" % pathname)
|
||||
args = [pathname]
|
||||
if self.my_colour is not None and self.my_name is not None:
|
||||
args.append("P%s=%s" % (self.my_colour.upper(),
|
||||
escape_for_savesgf(self.my_name)))
|
||||
try:
|
||||
self.proxy.handle_command("gomill-savesgf", args)
|
||||
except GtpError, e:
|
||||
# Hide error from kgsGtp, though I don't suppose it would care
|
||||
self.log("error: %s" % e)
|
||||
|
||||
|
||||
def main():
|
||||
kgs_proxy = Kgs_proxy(sys.argv[1:])
|
||||
kgs_proxy.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
46
gomill/examples/mogo_wrapper.py
Normal file
46
gomill/examples/mogo_wrapper.py
Normal file
@ -0,0 +1,46 @@
|
||||
"""Proxy for making mogo a better-behaved GTP engine.
|
||||
|
||||
This means the controller sees gomill's GTP implementation, not mogo's.
|
||||
|
||||
This makes quarry willing to run mogo, for example.
|
||||
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from gomill import gtp_engine
|
||||
from gomill import gtp_proxy
|
||||
|
||||
def handle_version(args):
|
||||
# Override remarkably verbose version response
|
||||
return "2007 public release"
|
||||
|
||||
def main(executable):
|
||||
try:
|
||||
if sys.argv[1] not in ("--9", "--13", "--19"):
|
||||
raise ValueError
|
||||
size = sys.argv[1][2:]
|
||||
except Exception:
|
||||
sys.exit("mogo_wrapper: first parameter must be --9, --13, or --19")
|
||||
|
||||
def handle_boardsize(args):
|
||||
# No need to pass this down to mogo.
|
||||
try:
|
||||
if args[0] != size:
|
||||
raise gtp_engine.GtpError("board size %s only please" % size)
|
||||
except IndexError:
|
||||
gtp_engine.report_bad_arguments()
|
||||
|
||||
proxy = gtp_proxy.Gtp_proxy()
|
||||
proxy.set_back_end_subprocess([executable] + sys.argv[1:])
|
||||
proxy.engine.add_command("version", handle_version)
|
||||
proxy.engine.add_command("boardsize", handle_boardsize)
|
||||
proxy.pass_command("boardsize", [size])
|
||||
try:
|
||||
proxy.run()
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main("mogo")
|
||||
|
72
gomill/examples/show_sgf.py
Normal file
72
gomill/examples/show_sgf.py
Normal file
@ -0,0 +1,72 @@
|
||||
"""Show the position from an SGF file.
|
||||
|
||||
This demonstrates the sgf and ascii_boards modules.
|
||||
|
||||
"""
|
||||
|
||||
import sys
|
||||
from optparse import OptionParser
|
||||
|
||||
from gomill import ascii_boards
|
||||
from gomill import sgf
|
||||
from gomill import sgf_moves
|
||||
|
||||
def show_sgf_file(pathname, move_number):
|
||||
f = open(pathname)
|
||||
sgf_src = f.read()
|
||||
f.close()
|
||||
try:
|
||||
sgf_game = sgf.Sgf_game.from_string(sgf_src)
|
||||
except ValueError:
|
||||
raise StandardError("bad sgf file")
|
||||
|
||||
try:
|
||||
board, plays = sgf_moves.get_setup_and_moves(sgf_game)
|
||||
except ValueError, e:
|
||||
raise StandardError(str(e))
|
||||
if move_number is not None:
|
||||
move_number = max(0, move_number-1)
|
||||
plays = plays[:move_number]
|
||||
|
||||
for colour, move in plays:
|
||||
if move is None:
|
||||
continue
|
||||
row, col = move
|
||||
try:
|
||||
board.play(row, col, colour)
|
||||
except ValueError:
|
||||
raise StandardError("illegal move in sgf file")
|
||||
|
||||
print ascii_boards.render_board(board)
|
||||
print
|
||||
|
||||
_description = """\
|
||||
Show the position from an SGF file. If a move number is specified, the position
|
||||
before that move is shown (this is to match the behaviour of GTP loadsgf).
|
||||
"""
|
||||
|
||||
def main(argv):
|
||||
parser = OptionParser(usage="%prog <filename> [move number]",
|
||||
description=_description)
|
||||
opts, args = parser.parse_args(argv)
|
||||
if not args:
|
||||
parser.error("not enough arguments")
|
||||
pathname = args[0]
|
||||
if len(args) > 2:
|
||||
parser.error("too many arguments")
|
||||
if len(args) == 2:
|
||||
try:
|
||||
move_number = int(args[1])
|
||||
except ValueError:
|
||||
parser.error("invalid integer value: %s" % args[1])
|
||||
else:
|
||||
move_number = None
|
||||
try:
|
||||
show_sgf_file(pathname, move_number)
|
||||
except Exception, e:
|
||||
print >>sys.stderr, "show_sgf:", str(e)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv[1:])
|
||||
|
54
gomill/examples/split_sgf_collection.py
Normal file
54
gomill/examples/split_sgf_collection.py
Normal file
@ -0,0 +1,54 @@
|
||||
"""Split an SGF collection into separate files.
|
||||
|
||||
This demonstrates the parsing functions from the sgf_grammar module.
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from optparse import OptionParser
|
||||
|
||||
from gomill import sgf_grammar
|
||||
from gomill import sgf
|
||||
|
||||
def split_sgf_collection(pathname):
|
||||
f = open(pathname)
|
||||
sgf_src = f.read()
|
||||
f.close()
|
||||
dirname, basename = os.path.split(pathname)
|
||||
root, ext = os.path.splitext(basename)
|
||||
try:
|
||||
coarse_games = sgf_grammar.parse_sgf_collection(sgf_src)
|
||||
except ValueError, e:
|
||||
raise StandardError("error parsing file: %s" % e)
|
||||
for i, coarse_game in enumerate(coarse_games):
|
||||
sgf_game = sgf.Sgf_game.from_coarse_game_tree(coarse_game)
|
||||
sgf_game.get_root().add_comment_text(
|
||||
"Split from %s (game %d)" % (basename, i+1))
|
||||
split_pathname = os.path.join(dirname, "%s_%d%s" % (root, i+1, ext))
|
||||
with open(split_pathname, "wb") as f:
|
||||
f.write(sgf_game.serialise())
|
||||
|
||||
|
||||
_description = """\
|
||||
Split a file containing an SGF game collection into multiple files.
|
||||
"""
|
||||
|
||||
def main(argv):
|
||||
parser = OptionParser(usage="%prog <filename>",
|
||||
description=_description)
|
||||
opts, args = parser.parse_args(argv)
|
||||
if not args:
|
||||
parser.error("not enough arguments")
|
||||
pathname = args[0]
|
||||
if len(args) > 1:
|
||||
parser.error("too many arguments")
|
||||
try:
|
||||
split_sgf_collection(pathname)
|
||||
except Exception, e:
|
||||
print >>sys.stderr, "sgf_splitter:", str(e)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv[1:])
|
||||
|
75
gomill/examples/twogtp
Executable file
75
gomill/examples/twogtp
Executable file
@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python
|
||||
"""'Traditional' twogtp implementation.
|
||||
|
||||
This runs a single game between two local GTP engines and reports the result.
|
||||
|
||||
This demonstrates the gtp_games module.
|
||||
|
||||
"""
|
||||
|
||||
import shlex
|
||||
import sys
|
||||
from optparse import OptionParser, SUPPRESS_HELP
|
||||
|
||||
from gomill import ascii_boards
|
||||
from gomill import gtp_games
|
||||
from gomill.gtp_controller import GtpChannelError, BadGtpResponse
|
||||
from gomill.common import format_vertex
|
||||
|
||||
def print_move(colour, move, board):
|
||||
print colour.upper(), format_vertex(move)
|
||||
|
||||
def print_board(colour, move, board):
|
||||
print colour.upper(), format_vertex(move)
|
||||
print ascii_boards.render_board(board)
|
||||
print
|
||||
|
||||
def main():
|
||||
usage = "%prog [options] --black='<command>' --white='<command>'"
|
||||
parser = OptionParser(usage=usage)
|
||||
parser.add_option("--black", help=SUPPRESS_HELP)
|
||||
parser.add_option("--white", help=SUPPRESS_HELP)
|
||||
parser.add_option("--komi", type="float", default=7.5)
|
||||
parser.add_option("--size", type="int", default=19)
|
||||
parser.add_option("--verbose", type="choice", choices=('0','1','2'),
|
||||
default=0, metavar="0|1|2")
|
||||
|
||||
(options, args) = parser.parse_args()
|
||||
if args:
|
||||
parser.error("too many arguments")
|
||||
if not options.black or not options.white:
|
||||
parser.error("players not specified")
|
||||
|
||||
black_command = shlex.split(options.black)
|
||||
white_command = shlex.split(options.white)
|
||||
game = gtp_games.Game(
|
||||
board_size=options.size,
|
||||
komi=options.komi,
|
||||
move_limit=1000)
|
||||
if black_command[0] != white_command[0]:
|
||||
game.set_player_code('b', black_command[0])
|
||||
game.set_player_code('w', white_command[0])
|
||||
if options.verbose == '1':
|
||||
game.set_move_callback(print_move)
|
||||
elif options.verbose == '2':
|
||||
game.set_move_callback(print_board)
|
||||
|
||||
game.allow_scorer('b')
|
||||
game.allow_scorer('w')
|
||||
|
||||
try:
|
||||
game.set_player_subprocess('b', black_command)
|
||||
game.set_player_subprocess('w', white_command)
|
||||
game.ready()
|
||||
game.run()
|
||||
except (GtpChannelError, BadGtpResponse), e:
|
||||
sys.exit("aborting game due to error:\n%s\n" % e)
|
||||
finally:
|
||||
game.close_players()
|
||||
print game.result.describe()
|
||||
if game.late_errors:
|
||||
sys.exit("\n".join(game.late_errors))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
Reference in New Issue
Block a user