The engine needs to be attached to both the black and white colour separately and the engine needs to know which colour it is being attached to so it knows to analyze the board from the perspective of the correct player. If the engine program can handle the generation of moves regardless of the colour it is attached then it should work.
Indeed, I just checked and it seems like Sabaki does not support using the same gtp engine instance for both colors.
I just downloaded the GoGui gtp client and it does support using the same gtp engine for both colors, but I have to press SHIFT-F5 every time to make it request a new move to my gtp engine. It has no âautoplayâ.
Edit, I downloaded older gtp client Drago, but it also doesnât support autoplaying with 1 gtp engine.
The gtp clients that I checked sofar indeed seem to be tailored for this scenario, but it is not a limitation of the gtp protocol. Perhaps gtp client programmers did not consider autoplay with 1 gtp engine instance a useful feature for their users.
I donât know if other gtp clients support autoplaying with 1 gtp engine instance.
Before I start looking if such a gtp client exists, what is the purpose of the gtp client in your project? I suppose it is not really neccessary to visualise digitally what can already be seen on the physical go board next to the computer? Would some 3rd person interact with the gtp client in some way during the game? Or is it only there to record the game and save it to an sgf file?
I found it, this is the class that implements gtp (converting gtp string commands to method calls and return values to gtp return strings):
Summary
using System;
using System.Linq;
using System.Globalization;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
namespace Beast
{
using Command = Tuple<MethodInfo, Func<object, string>, bool>;
using System.Diagnostics;
/// <summary>
/// http://www.lysator.liu.se/~gunnar/gtp/gtp2-spec-draft2/gtp2-spec.html
/// </summary>
public class GtpServer
{
private Board _board = new Board(19);
private Game _game = new Game();
private readonly Dictionary<string, Command> _commands;
public GtpServer()
{
_commands = CreateCommands();
}
public void Run(Func<string> read, Action<string> write)
{
var whitespace = new[] { '\t', ' ' };
var comment = new[] { '#' };
var game = new Game();
do
{
var input = read();
if (string.IsNullOrEmpty(input))
input = "showboard";
input = input.Split(comment)[0];
var tokens = input.Split(whitespace, StringSplitOptions.RemoveEmptyEntries);
if (tokens.Length < 1)
continue;//empty line
var commandIndex = 0;
var id = string.Empty;
uint number;
if (uint.TryParse(tokens[0], NumberStyles.None, CultureInfo.InvariantCulture, out number))
{
commandIndex++; //id found
id = tokens[0];
}
try
{
if (tokens.Length <= commandIndex)
throw new GtpException(); //only an id??
//get command
var commandToken = tokens[commandIndex].ToLower();
Command command;
if (!_commands.TryGetValue(commandToken, out command))
throw new GtpException("unknown command");
//collect command args
var argIndex = commandIndex + 1;
var argTokens = new string[tokens.Length - argIndex];
Array.Copy(tokens, argIndex, argTokens, 0, tokens.Length - argIndex);
write(FormatResponse('=', id, Invoke(command, argTokens)));
if (commandToken == "quit")
game = null;
}
catch (GtpException ex)
{
write(FormatResponse('?', id, ex.Message));
}
} while (game != null);
}
[DebuggerHidden]
private string Invoke(Command command, string[] argTokens)
{
var method = command.Item1;
var output = command.Item2;
var promptBoard = command.Item3;
var args = ToArgs(method.GetParameters(), argTokens);
try
{
return output(method.Invoke(method.IsStatic ? null : this, args))
+ (promptBoard ? ShowBoard() : string.Empty);
}
catch (TargetInvocationException ex)
{
//preserve stacktrace
typeof(Exception).GetMethod("PrepForRemoting", BindingFlags.NonPublic | BindingFlags.Instance)
.Invoke(ex, new object[0]);
throw ex.InnerException;
}
}
private Dictionary<string, Command> CreateCommands()
{
var commands = new Dictionary<string, Command>();
foreach (var member in typeof(GtpServer).GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static))
foreach (var gtp in member.GetCustomAttributes(true).OfType<GtpCommandAttribute>())
{
var method = GetMethod(member);
commands[gtp.Command] = new Command(method, GetOutput(method.ReturnType), member.GetCustomAttributes(true).OfType<PromptBoardAttribute>().FirstOrDefault() != null);
}
return commands;
}
private static MethodInfo GetMethod(MemberInfo member)
{
switch (member.MemberType)
{
case MemberTypes.Method:
return (MethodInfo)member;
case MemberTypes.Property:
return ((PropertyInfo)member).GetGetMethod();
default:
throw new InvalidOperationException();
}
}
private Func<object, string> GetOutput(Type type)
{
if (type == typeof(void))
return value => string.Empty;
if (type == typeof(string))
return value => (string)value;
if (type == typeof(bool))
return value => BooleanToGtp((bool)value);
if (type == typeof(Point))
return value => PointToGtp((Point)value);
if (type == typeof(IEnumerable<string>))
return value => StringsToGtp((IEnumerable<string>)value);
if (type == typeof(IEnumerable<Point>))
return value => PointsToGtp((IEnumerable<Point>)value);
throw new InvalidOperationException();
}
private static string FormatResponse(char prompt, string id, string message)
{
return string.Format("{0}{1} {2}\n",
prompt,
id,
message);
}
private object[] ToArgs(ParameterInfo[] parameters, string[] argTokens)
{
if (argTokens.Length < parameters.Length)
throw new GtpException("argument missing");
var args = new List<object>();
int p = 0, a = 0;
while (p < parameters.Length)
{
var type = parameters[p++].ParameterType;
if (type == typeof(string))
args.Add(argTokens[a++]);
else if (type == typeof(uint))
args.Add(ParseInt(argTokens[a++]));
if (type == typeof(Color))
args.Add(ParseColor(argTokens[a++]));
else if (type == typeof(Point))
args.Add(ParsePoint(argTokens[a++]));
else if (type == typeof(double))
args.Add(ParseFloat(argTokens[a++]));
else if (type == typeof (Point[]))
{
var points = new List<Point>();
do
{
points.Add(ParsePoint(argTokens[a++]));
} while (a < argTokens.Length);
args.Add(points.ToArray());
}
}
return args.ToArray();
}
private static uint ParseInt(string arg)
{
uint value;
if (!uint.TryParse(arg, NumberStyles.None, CultureInfo.InvariantCulture, out value))
throw new GtpException();
return value;
}
private static double ParseFloat(string arg)
{
double value;
if (!double.TryParse(arg, NumberStyles.Float, CultureInfo.InvariantCulture, out value))
throw new GtpException();
return value;
}
private Point ParsePoint(string arg)
{
arg = arg.ToLower();
Point point;
if (!Point.TryParseFromEuropeanFormat(_board.BoardSize, arg, out point))
throw new GtpException();
return point;
}
private static Color ParseColor(string arg)
{
arg = arg.ToLower();
switch (arg)
{
case "b":
case "black":
return Color.Black;
case "w":
case "white":
return Color.White;
default:
throw new GtpException();
}
}
private string PointToGtp(Point value)
{
return value.ToEuropeanFormat(_board.BoardSize);
}
private string PointsToGtp(IEnumerable<Point> value)
{
return string.Join(" ", value.Select(point => point.ToEuropeanFormat(_board.BoardSize)).ToArray());
}
private static string BooleanToGtp(bool value)
{
return value ? "true" : "false";
}
private static string StringsToGtp(IEnumerable<string> value)
{
return value == null ? null : string.Join("\n", value.ToArray());
}
[GtpCommand("protocol_version")]
public static string GtpVersion
{
get { return "2"; }
}
[GtpCommand("name")]
public static string EngineName
{
get { return typeof (Beast).Assembly.GetName().Name; }
}
[GtpCommand("version")]
public static string EngineVersion
{
get { return typeof (Beast).Assembly.GetName().Version.ToString(); }
}
[GtpCommand("known_command")]
public bool IsKnownCommand(string command)
{
return _commands.ContainsKey((command.ToLower()));
}
[GtpCommand("list_commands")]
public IEnumerable<string> GetKnownCommands()
{
return _commands.Keys;
}
[GtpCommand("quit")]
public void Quit()
{
_board = null;
_game = null;
}
[GtpCommand("boardsize"), PromptBoard]
public void SetBoardSize(uint boardSize)
{
if (boardSize < 2 || boardSize > 25)
throw new GtpException("unacceptable boardsize");
_board = new Board(boardSize);
}
[GtpCommand("clear_board"), PromptBoard]
public void ClearBoard()
{
_board.Clear();
_game.Clear();
}
[GtpCommand("komi")]
public void SetKomi(double value)
{
_game.Komi = value;
}
[GtpCommand("fixed_handicap"), PromptBoard]
public IEnumerable<Point> SetFixedHandicap(int stones)
{
if (!_board.IsEmpty())
throw new GtpException("board not empty");
var points = _board.GetHandicapPoints().Take((stones)).ToArray();
if (points.Count() < stones)
throw new GtpException("invalid number of stones");
foreach (var point in points)
if (!_board.IsValid(point) || !_board.IsEmpty(point))
throw new GtpException("bad vertex list");
_board.SetHandicap(points);
return points;
}
[GtpCommand("place_free_handicap"), PromptBoard]
public IEnumerable<Point> PlaceFreeHandicap(int stones)
{
if (!_board.IsEmpty())
throw new GtpException("board not empty");
//todo
yield break;
}
[GtpCommand("set_free_handicap"), PromptBoard]
public void SetFreeHandicap(Point[] points)
{
if (!_board.IsEmpty())
throw new GtpException("board not empty");
foreach (var point in points)
if (!_board.IsValid(point) || !_board.IsEmpty(point))
if (points.Distinct().Count() < points.Length)
throw new GtpException("bad vertex list");
_board.SetHandicap(points);
}
[GtpCommand("play"), PromptBoard]
public void Play(Color color, Point point)
{
if (!point.IsPass &&
(!_board.IsValid(point)
|| !_board.IsEmpty(point)
|| _board.IsHot(point)
|| _board.IsSuicide(point, color)))
throw new GtpException("illegal move");
var memento = _board.Play(color, point);
_game.History.Push(memento);
_board.AddCaptured(memento, ref _game.BlackStonesCaptured, ref _game.WhiteStonesCaptured);
}
[GtpCommand("genmove")]
public Point GenerateMove(Color color)
{
return Point.Pass;
}
[GtpCommand("undo"), PromptBoard]
public void Undo()
{
if (_game.History.Count == 0)
throw new GtpException("cannot undo");
var memento = _game.History.Pop();
_board.RemoveCaptured(memento, ref _game.BlackStonesCaptured, ref _game.WhiteStonesCaptured);
_board.Undo(memento);
}
[GtpCommand("showboard")]
public string ShowBoard()
{
var sb = new StringBuilder();
sb.AppendLine(_board.ToString());
sb.AppendFormat("Black prisoners {0}\n", _game.WhiteStonesCaptured);
sb.AppendFormat("White prisoners {0}", _game.BlackStonesCaptured);
return sb.ToString();
}
}
}
In case this info can be useful here, both KaTrain and Lizzie have a âforce AI moveâ function, that can be used to let the engine play against itself manually, and KaTrain also has a âgenerate gameâ function where it happens automatically â so it seems to me they both have a way to consult the engine without having to wait for a manual input.
Theyâre both open source projects, so perhaps looking into that might be useful.
Initially I simply want to be able to see the game progressing digitally in real time. No particular reason why that is beneficial though. Just thought it would be cool. Because like you said, people can see whatâs happening on real board. But then i start to see that implementing a gtp engine would be good because there are so many applications supporting it. If I turn this project into a gtp engine peripheral, this can potentially be applied to a lot of different go services. And that would make the project very accessible to people with different preferences of go server.
Yeah I saw that one of the gtp command genmove and that seems to be the automatic playing command. Thanks for the suggestion too! I will read up how KaTrain and Lizzie implement their codes.
Because like you said, people can see whatâs happening on real board. But then i start to see that implementing a gtp engine would be good because there are so many applications supporting it. If I turn this project into a gtp engine peripheral, this can potentially be applied to a lot of different go services.
Indeed, I think implementing a gtp engine is beneficial. But you donât really need a full blown gtp client with a nice graphical UI to test and utilise your gtp engine. In essence, a gtp engine is a console application and you can interact with it through a console by typing gtp commands manually or by some script.
One of the gtp commands it âshowboardâ and this should return a text diagram of the board. So you can visualise the board without a graphical gtp client.
You could write some simple script to interact with your gtp engine and coordinate a full game. For example, cycling between âgenmove bâ and âgenmove wâ until 2 consecutive passes occur.
An illustration of such a console session with a simple gtp engine:
Summary
Regular text lines below are gtp commands and responses of the gtp engine start with an â=â.
name
= Beast
protocol_version
= 2
version
= 1.0.0.0
list_commands
= known_command
list_commands
quit
boardsize
clear_board
komi
fixed_handicap
place_free_handicap
set_free_handicap
play
genmove
undo
showboard
protocol_version
name
version
name
= Beast
protocol_version
= 2
boardsize 19
=
A B C D E F G H J K L M N O P Q R S T
+---------------------------------------+
19| . . . . . . . . . . . . . . . . . . . |19
18| . . . . . . . . . . . . . . . . . . . |18
17| . . . . . . . . . . . . . . . . . . . |17
16| . . . , . . . . . , . . . . . , . . . |16
15| . . . . . . . . . . . . . . . . . . . |15
14| . . . . . . . . . . . . . . . . . . . |14
13| . . . . . . . . . . . . . . . . . . . |13
12| . . . . . . . . . . . . . . . . . . . |12
11| . . . . . . . . . . . . . . . . . . . |11
10| . . . , . . . . . , . . . . . , . . . |10
9| . . . . . . . . . . . . . . . . . . . |9
8| . . . . . . . . . . . . . . . . . . . |8
7| . . . . . . . . . . . . . . . . . . . |7
6| . . . . . . . . . . . . . . . . . . . |6
5| . . . . . . . . . . . . . . . . . . . |5
4| . . . , . . . . . , . . . . . , . . . |4
3| . . . . . . . . . . . . . . . . . . . |3
2| . . . . . . . . . . . . . . . . . . . |2
1| . . . . . . . . . . . . . . . . . . . |1
+---------------------------------------+
A B C D E F G H J K L M N O P Q R S T
Black prisoners 0
White prisoners 0
clear_board
=
A B C D E F G H J K L M N O P Q R S T
+---------------------------------------+
19| . . . . . . . . . . . . . . . . . . . |19
18| . . . . . . . . . . . . . . . . . . . |18
17| . . . . . . . . . . . . . . . . . . . |17
16| . . . , . . . . . , . . . . . , . . . |16
15| . . . . . . . . . . . . . . . . . . . |15
14| . . . . . . . . . . . . . . . . . . . |14
13| . . . . . . . . . . . . . . . . . . . |13
12| . . . . . . . . . . . . . . . . . . . |12
11| . . . . . . . . . . . . . . . . . . . |11
10| . . . , . . . . . , . . . . . , . . . |10
9| . . . . . . . . . . . . . . . . . . . |9
8| . . . . . . . . . . . . . . . . . . . |8
7| . . . . . . . . . . . . . . . . . . . |7
6| . . . . . . . . . . . . . . . . . . . |6
5| . . . . . . . . . . . . . . . . . . . |5
4| . . . , . . . . . , . . . . . , . . . |4
3| . . . . . . . . . . . . . . . . . . . |3
2| . . . . . . . . . . . . . . . . . . . |2
1| . . . . . . . . . . . . . . . . . . . |1
+---------------------------------------+
A B C D E F G H J K L M N O P Q R S T
Black prisoners 0
White prisoners 0
komi 6.5
=
boardsize 19
=
A B C D E F G H J K L M N O P Q R S T
+---------------------------------------+
19| . . . . . . . . . . . . . . . . . . . |19
18| . . . . . . . . . . . . . . . . . . . |18
17| . . . . . . . . . . . . . . . . . . . |17
16| . . . , . . . . . , . . . . . , . . . |16
15| . . . . . . . . . . . . . . . . . . . |15
14| . . . . . . . . . . . . . . . . . . . |14
13| . . . . . . . . . . . . . . . . . . . |13
12| . . . . . . . . . . . . . . . . . . . |12
11| . . . . . . . . . . . . . . . . . . . |11
10| . . . , . . . . . , . . . . . , . . . |10
9| . . . . . . . . . . . . . . . . . . . |9
8| . . . . . . . . . . . . . . . . . . . |8
7| . . . . . . . . . . . . . . . . . . . |7
6| . . . . . . . . . . . . . . . . . . . |6
5| . . . . . . . . . . . . . . . . . . . |5
4| . . . , . . . . . , . . . . . , . . . |4
3| . . . . . . . . . . . . . . . . . . . |3
2| . . . . . . . . . . . . . . . . . . . |2
1| . . . . . . . . . . . . . . . . . . . |1
+---------------------------------------+
A B C D E F G H J K L M N O P Q R S T
Black prisoners 0
White prisoners 0
clear_board
=
A B C D E F G H J K L M N O P Q R S T
+---------------------------------------+
19| . . . . . . . . . . . . . . . . . . . |19
18| . . . . . . . . . . . . . . . . . . . |18
17| . . . . . . . . . . . . . . . . . . . |17
16| . . . , . . . . . , . . . . . , . . . |16
15| . . . . . . . . . . . . . . . . . . . |15
14| . . . . . . . . . . . . . . . . . . . |14
13| . . . . . . . . . . . . . . . . . . . |13
12| . . . . . . . . . . . . . . . . . . . |12
11| . . . . . . . . . . . . . . . . . . . |11
10| . . . , . . . . . , . . . . . , . . . |10
9| . . . . . . . . . . . . . . . . . . . |9
8| . . . . . . . . . . . . . . . . . . . |8
7| . . . . . . . . . . . . . . . . . . . |7
6| . . . . . . . . . . . . . . . . . . . |6
5| . . . . . . . . . . . . . . . . . . . |5
4| . . . , . . . . . , . . . . . , . . . |4
3| . . . . . . . . . . . . . . . . . . . |3
2| . . . . . . . . . . . . . . . . . . . |2
1| . . . . . . . . . . . . . . . . . . . |1
+---------------------------------------+
A B C D E F G H J K L M N O P Q R S T
Black prisoners 0
White prisoners 0
genmove b
= pass
genmove w
= pass
(my gtp engine simply passes when it gets a âgenmoveâ command, so its self-play games arenât very interesting)
Yes, this is nice:
but for testing and such, this may be good enough:
play B K7
=
A B C D E F G H J K L M N O P Q R S T
+---------------------------------------+
19| . . . . . . . . . . . . . . . . . . . |19
18| . . . . . X X . . . . . . X O . . . . |18
17| . . . O O . O X X . X X X X . . X . . |17
16| . O O , O O . O O O O O O O O O X . . |16
15| O X X X X . . . . X . . . . O X . . . |15
14| . O . . . O O . X . X O X . . X . . . |14
13| . . . X . X . . . X . X . X . . . . . |13
12| . . O X . . . X . . . . O X O X X X . |12
11| . . O X . . . . . O O . O X X O O . . |11
10| . . O X . . . . . O . O X O X X O O . |10
9| . . O X . . . . O X O X X O O O X O . |9
8| . . . O X . . . O X . X . X . O X O . |8
7| . O . O X . . . . X . . X . . . . . . |7
6| . . O X . . . . X . . . X . . . O . . |6
5| . . . X . . . X O . X X O O . O . . . |5
4| . . . , . . . . . , . O X O . O X . . |4
3| . . . X . . . . . O . O . . O X X . . |3
2| . . . . . . . . . . . . . O X . X . . |2
1| . . . . . . . . . . . . . . . X . . . |1
+---------------------------------------+
A B C D E F G H J K L M N O P Q R S T
Black prisoners 3
White prisoners 1
That is true. I picked Sabaki because I have it installed and attaching an engine is relatively simple. It even has a terminal for me to run test commands. However, if my engine doesnât work on Sabaki, I donât see how itâll work with other gtp clients, unless Sabaki has some hidden features that distinguish itself from other clients.
I meant to say that using Sabaki for your scenario seems difficult. As you said, you would probably need to run 2 instances of your gtp engine (1 for black and 1 for white) for a single game. Your project software would need to jump through some hoops to make it work with Sabaki.
But this issue is not a gtp limitation. It is a limitation of Sabaki (and perhaps many other gtp clients that are readily available).
You could make some bare-bones gtp client to support your scenario, if you donât really need a nice graphical UI (which you said isnât a necessity).
Another option is to fork Sabaki and modify it to add support for your scenario (if you need some nice graphical UI after all). But it may take some time to find your way in its source code.
I suppose it also depends on what kind of future plans you have with your project, beyond recording a game. If your main goal is to record a locally played game between 2 human players and save the game record in an sgf file, you could write that first and ditch the gtp thing for now, instead of going deeper into that rabbit hole (YAGNI?).
accessible to people with different preferences of go server.
I think that gtp support potentially allows a variety of existing tools to interact with your board, but if playing on a go server (on your physical board) is a major goal of your project, Iâm not really sure if gtp is the way to go. Go servers use gtp to communicate with bots. Perhaps they wonât it like when in fact a human is playing via gtp (appearing like a bot to their opponents).
Thatâs a good point. I need to rethink my path forward. Bare-bone gtp client sounds like a good starting point.
However, there is one thing I want to verify. I copied the code from wally/wally_06.py at main ¡ maksimKorzh/wally ¡ GitHub and save it locally. I created an engine out of this python script and selected the lightning button (Start Engine vs Engine game) beside the Attach engine button. And ⌠it kind of work?? I can see that the engine is calling genmove on both black and white players and they are playing.
I suppose it also depends on what kind of future plans you have with your project, beyond recording a game. If your main goal is to record a locally played game between 2 human players and save the game record in an sgf file, you could write that first and ditch the gtp thing for now, instead of going deeper into that rabbit hole (YAGNI ?).
My ultimate goal of this project is a physical board that act like Sabaki and perhaps online go service. So like a board where you can play with another person in the same room and recording the game moves. You can attach an AI and the board will display with LED where the AI plays. You can also connect to an online go server and play with another player. The move they play will be displayed on the board also with LED. When you play on the board the online go server will see that you place a stone at that coordinate. Itâs still far-fetched and require a lot of work. But yeah thatâs where I want to be.
Which means ⌠maybe what I want isnât a gtp engine but a sgf editor / gtp client like Sabaki or other terminal go client. Iâll need my own implementation of the all the MCT thingy XD
You can autoplay in gogui. In the menu: Game/Computer Color/Both