What is the name of the program that keeps the state of go game but not the ai itself

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?

1 Like

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.

1 Like

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.

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?).

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.

2 Likes

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

2 Likes

You can autoplay in gogui. In the menu: Game/Computer Color/Both

1 Like