Some years ago I defined a very simple interface for playing Go engines against each other. Recently I've added GTP support to it.
It was a lot of fun! The basic pieces came together within a couple of hours. Then it was like playing with Legos; plug things together in different ways and see what I can make. :-) Play one engine against another. Play my engine against GnuGo, Aya, SmartGo. Play against Many Faces (which supports only GMP) using SmartGo as an arbiter (it plays against both GTP and GMP). Play an engine as a robot on KGS. The silly KGS Bot Reflector also uses this code.
One of the most bizarre configurations I got working was to play SmartGo as a robot on KGS. SmartGo doesn't play as a GTP engine but will play against GTP engines. I made a 'cross-wired' adapter that would appear to SmartGo like a console-based GTP engine. It would take commands from SmartGo through standard-in and pass them as responses to the KgsGtp proxy over TCP/IP; conversely it would take commands from KGS and present as responses to SmartGo. Pretty wacky but it worked!
IGoEngine
The IEngine interface defines methods for starting, setting up and playing the game:
public interface IGoEngine { void Start(int size, float komi, int level); void Setup(Stone color, int x, int y); GoMove Reply(Stone color); void Play(GoMove move); void Undo(); void Quit(); string Name { get; } string Version { get; } }
This is enough to play. At first I used this in process to play different versions of my engine against each other.
IAdvancedGoEngine
Mainly to support GnuGo I later added the IAdvancedGoEngine interface which extends to add endgame scoring and enumerate top moves generated ('hints'):
public interface IAdvancedGoEngine : IGoEngine { GoScore Score(); GoPoint[] Dead(); GoPoint[] Territory(Stone color); GoPoint[] Ideas(GoColor? color); }
GtpEngineAdapter
The above interface can easily be used by two engines in the same process (to test one version against it's previous self as improvements are made). For remoting I originally exposed it as a WebService (SOAP) which is cool but nobody supports my invented wire protocol of course. GTP, on the other hand has large and growing support (SmartGo, GnuGo, Aya, KGS, etc.).
While GTP is simple, I think my interface is even simpler and have a bunch of things already writen that use it so I decided to map GTP to the existing interface. It worked nicely.
GtpEngineAdapter is constructed from a given IGoEngine and two TextWriters. The TextWriters can wrap any stream. It could be just Console I/O or could be a NetworkStream, etc. Some commands have a direct mapping:
GTP Command IAdvancedGoEngine Command -------------------------------------------- name Name (property) version Version quit Quit() play Play() genmove Reply() undo Undo() final_status_list Dead() / Territory() estimate_score Score() top_moves_black Ideas(Stone.Black) top_moves_white Ideas(Stone.White)
For the GTP commands 'boardsize', 'komi', and 'level', the adapter responds to them immediately but calls IGoEngine.Start(...) just-in-time (before the first play/setup).
'clear_board' causes a new Start(). 'fixed_handicap' calls Setup() with the correct placements. 'place_free_handicap' calls Reply() once for each handicap stone.
Some other GTP commands are handled by the adapter and the engine remains oblivious to them ('protocol_version', 'known_command', and 'list_commands').
GtpReader/Writer
GtpWriter allow writing well-formed values for all the primitives that GTP supports to the given TextWriter:
public abstract class GtpWriter { public GtpWriter(TextWriter writer) : this(writer, null) { } public GtpWriter(TextWriter writer, TextWriter log) { }
public void Write(int x, int y, int size); // vectors - "Q10", "D4" public void Write(GtpPassResign value); // pseudo-vectors - "pass", "resign" public void Write(Stone color); // "B", "W" public void Write(Stone color, int x, int y, int size); // play "B Q10", "W D4" public void Write(bool value); // "true", "false" public void Write(int value); // "123" public void Write(float value); // "1.23" public void Write(string value); // "foo" public void WriteComment(string comment); "# bar" }
From this is derived GtpCommandWriter which is responsible for writing complete commands with a name, optional ID and optional arguments in the form of any of the primitives.
Most commands are a single call. For commands taking an arbitrary number of arguments, call WriteCommandStart(...) followed by calls to write the arguments, and finally WriteCommandEnd().
From GtpWriter is also derived GtpResponseWriter which as you can guess writes well-formed responses to commands with optional ID and optional arguments. It also allows WriteFailure(int id, string message).
Without going into the details, there is a matching set of GtpReader, GtpCommandReader, GtpResponseReader for parsing the protocol.
While the writers assert when used improperly (semantics such as start/args/end) readers throw GtpException. This is because it's assumed that I own the code using the writers but readers are accepting input from who-knows-where.