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 Type |
Python/Sente type |
|---|---|
int |
|
float |
|
string |
|
vertex |
|
color |
|
move |
|
boolean |
|
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