Creating Private GTP Extensions

It is common to expand the Go Text Protocol with additional commands that are not defined by the official spec. Commands defined this way are called “private extensions” and are outlined in Section 2.13 of the GTP 2 Spec. Leela zero for example defines a private extension command called analyze that provides an analysis of the board. The Lizzie frontend for leela zero then uses this command to generate it’s well-known analysis graphs.

The GTP.Session object provides utilities to define private GTP extensions that can then be executed.

Making an Echo command

GTP does not define an echo command as it has little utility in GTP applications. However it makes a great example of how to implement private GTP extensions in sente.

To begin, we will use the basic GTP Code from the previous section Setting Up the Python file.

 1"""
 2
 3Author: Arthur Wesley
 4
 5"""
 6
 7from sente import GTP
 8
 9
10def main():
11    """
12
13    main method
14
15    """
16
17    session = GTP.Session("engine", "0.0.1")
18
19    while session.active():
20
21        response = session.interpret(input(""))
22        print(response)
23
24
25if __name__ == "__main__":
26    main()

Private GTP extensions can be defined in sente using decorators similar to those for Session.GenMove. Like Session.GenMove decorators, Session.Command decorators can only be applied to functions with python type hints.

GTP Recognizes 7 different data types seen in the table below. Sente maps each of these data types to a python or sente data type seen in the second column. All arguments and return types must match one of these for sente to create a private GTP extension.

GTP Variables

GTP Type

Python/Sente type

int

int

float

float

string

str

vertex

sente.Vertex

color

sente.stone

move

sente.Move

boolean

bool

An echo command takes a string argument and returns the same string. This is an extremely simple function and we simply have to return the message argument.

 1"""
 2
 3Author: Arthur Wesley
 4
 5"""
 6
 7from sente import GTP
 8
 9
10def main():
11    """
12
13    main method
14
15    """
16
17    session = GTP.Session("engine", "0.0.1")
18
19    @session.Command
20    def echo(message: str) -> str:
21        """
22
23        a simple echo command
24
25        :param message: message to echo
26        :return: the message
27        """
28        return message
29
30    while session.active():
31
32        response = session.interpret(input(">> ")) # add the prompt back for debugging
33        print(response)
34
35
36if __name__ == "__main__":
37    main()

We can now run the program and and test our echo command:

$ python echo_command.py
>> echo hello
? unknown command

>>

Why wasn’t the command echo recognized?

Naming conventions

Section 2.13 of the GTP spec advises that private GTP extensions include the name of the engine followed by a dash (“-“) followed by the name of the command. Sente automatically adds formats the names of it’s commands to match this pattern. Therefore, the echo command we have created oauth to be “engine-echo”. We can conform this by running the “list_commands” command:

$ python echo_command.py
>> echo
? unknown command

>> list_commands
= play
[...]
engine-echo
[...]
loadsgf

>>

therefore, we can run the echo command by using the name “engine-echo”:

$ python echo_command.py
>> engine-echo hello
= hello

>> engine-echo "hello world"
= hello world

>>

Note

Officially, GTP String literals are not allowed to have spaces in them. However the sente interpreter allows strings to with spaces in them so long as the strings are enclosed in quotes.

Returning Error messages

Sometimes it is desirable to add error messages when an invalid variable is passed to a GTP command. For example, the standard play command will error out if an illegal move is requested.

GTP Commands that return custom error messages must return a tuple containing two elements: a boolean representing the status of the command and a GTP.

For Example…

return True, "This is a successful status!"

…would indicate that the function has been completed successfully. Meanwhile…

return False, "This is an unsuccessful status :("

…would indicate that the function has had an error.

Let’s create a simple GTP command with error messages, note that the return type is labeled as tuple.

session = GTP.Session("engine", "0.0.1")

@session.Command
def error_message(output: bool) -> tuple:
    if output:
        return True, "This is a successful status!"
    else:
        return False, "This is an unsuccessful status :("

When we run this code however, we get an error:

$ python error_message.py
Traceback (most recent call last):
  File "error_message.py", line 37, in <module>
    main()
  File "error_message.py", line 20, in main
    def error_message(output: bool) -> tuple:
TypeError: Custom GTP command returned invalid response, expected GTP compatible type, got <class 'tuple'>

As noted earlier, the error_message function is labeled as returning a tuple. By default, python tuples are weakly typed, which violates the principle that every private GTP extension must be strongly strongly typed.

We can work around this by using the typing.Tuple class from the builtin python typing library which allows strongly typed tuples.

import typing

session = GTP.Session("engine", "0.0.1")

@session.Command
def error_message(output: bool) -> typing.Tuple[bool, str]:
    if output:
        return True, "This is a successful status!"
    else:
        return False, "This is an unsuccessful status :("

The command will now be accepted and can be tested in our GTP shell

$ python error_message.py
>> engine-error_message true
= This is a successful status!

>> engine-error_message false
? This is an unsuccessful status :(

>>

Unions in return types

In addition to accepting typing.Tuple return types, sente also accepts the typing.Union type. This allows greater flexibility in return types and enables functions to either return strings or GTP data types depending on whether or not the command was successful.

For example, if we were to create a command that calculates 1/x and errors out if x = 0, we could use typing.Union[float, typing.Tuple[bool, str]] as follows:

@session.Command
def one_over_x(x: float) -> typing.Union[float, typing.Tuple[bool, str]]:
    if x == 0:
        return False, "cannot divide by zero"
    else:
        return 1 / x

…or alternatively with Python 3.10…

@session.Command
def one_over_x(x: float) -> float | typing.Tuple[bool, str]:
    if x == 0:
        return False, "cannot divide by zero"
    else:
        return 1 / x