The process of visualizing potential moves done by you and your opponent to learn the result of different moves is called "reading".
In GNU Go, this is done by the functions in `engine/reading.c'. Each of these functions has a separate goal to fill, and they call each other recursively to carry out the reading process.
The reading code makes use of a stack onto which board positions can
be pushed. The parameter stackp
is zero if GNU Go is
examining the true board position; if it is higher than zero, then
GNU Go is examining a hypothetical position obtained by playing
several moves.
Many of the reading functions make use of null pointers.
For example, a call to attack(i, j, &ai, &aj)
will return
1 if the string at (i, j)
can be captured, 2 or 3 if it
can be attacked with ko, and 0 if it is safe. The point of attack
(in case it is vulnerable) is returned in (ai, aj)
. However
many times we do not care about the point of attack. In this case,
we can substitute a null pointer: attack(i, j, NULL, NULL)
.
Depth of reading is controlled by a parameter depth
. This
has a default value DEPTH
(in `liberty.h'), which is
set to 14 in the distribution, but it may also be set at the
command line using the -D option. If depth
is increased, GNU Go
will be stronger and slower. GNU Go will read moves past depth,
but in doing so it makes simplifying assumptions that can cause it
to miss moves.
Specifically, when stackp > depth
, GNU Go assumes that as soon
as the string can get 3 liberties it is alive. This assumption is
sufficient for reading ladders.
Currently the reading code does not try to defend a string by
attacking a boundary string with more than two liberties. Because
of this restriction, it can make oversights. A symptom of this is
two adjacent strings, each having three or four liberties, each
classified as DEAD
. To resolve such situations, a function
small_semeai()
(in `engine/semeai.c') looks for such
pairs of strings and corrects their classification.
The backfill_depth is a similar variable with a default 10. Below this depth, GNU Go will try "backfilling" to capture stones. For example in this situation:
.OOOOOO. on the edge of the board, O can capture X but OOXXXXXO in order to do so he has to first play at a in .aObX.XO preparation for making the atari at b. This is -------- called backfilling.
Backfilling is only tried with stackp <= backfill_depth
. The
parameter backfill_depth
may be set using the -B
option.
The fourlib_depth
is a parameter with a default of only 5.
Below this depth, GNU Go will try to attack strings with
four liberties. The fourlib_depth
may be set using the
-F option.
The parameter ko_depth
is a similar cutoff. If
stackp<ko_depth
, the reading code will make experiments
involving taking a ko even if it is not legal to do so (i.e., it
is hypothesized that a remote ko threat is made and answered
before continuation). This parameter may be set using the
-K option.
The reading functions generally return 1 for success, and 0 for failure. If the result depends on ko, they return 2 or 3. A return code of 2 means that the attack or defense is successful provided the attacker or defender is willing to ignore a ko threat; a return code of 3 means the attack or defense is successful provided the player can come up with a sufficiently large ko threat.
A partial list of the functions in `reading.c':
int attack(int m, int n, int *i, int *j)
:
The basic function
attack(m, n, *i, *j)
determines if the string at(m, n)
can be attacked, and if so,(*i, *j)
returns the attacking move, unless*i
and*j
are null pointers. (Use null pointers if you are interested in the result of the attack but not the attacking move itself.) Returns 1 if the attack succeeds, otherwise 0. Returns 2 or 3 if the result depends on ko: returns 2 if the attack succeeds provided attacker is willing to ignore any ko threat. Returns 3 if attack succeeds provided attacker has a ko threat which must be answered.
find_defense(int m, int n, int *i, int *j)
:
The function
find_defense(m, n, *i, *j)
attempts to find a move that will save the string at(m,n)
. It returns true if such a move is found, with(*i, *j)
the location of the saving move (unless(*i, *j)
are null pointers). It is not checked that tenuki defends, so this may give an erroneous answer if!attack(m,n)
. Returns 2 or 3 if the result depends on ko. Returns 2 if the string can be defended provided (color) is willing to ignore any ko threat. Returns 3 if (color) has a ko threat which must be answered.
safe_move(int i, int j, int color)
:
The function
safe_move(i, j, color)
checks whether a move at(i, j)
is illegal or can immediately be captured. Ifstackp==0
the result is cached. If the move only can be captured by a ko, it's considered safe. This may or may not be a good convention.
The next few functions are essentially special cases of attack
and find_defense
. They are coded individually.
attack2(int m, int n, int *i, int *j)
:
Determine whether a string with 2 liberties can be captured. Usage is similar to
attack
.
attack3(int m, int n, int *i, int *j)
:
Determine whether a string with 3 liberties can be captured. Usage is similar to
attack
.
attack4(int m, int n, int *i, int *j)
:
Determine whether a string with 4 liberties can be captured. Usage is similar to
attack
.
defend1(int m, int n, int *i, int *j)
:
Determine whether a string with 1 liberty can be rescued. Usage is similar to
find_defense
.
defend2()
:
Determine whether a string with 2 liberties can be rescued. Usage is similar to
find_defense
.
defend3()
:
Determine whether a string with 3 liberties can be rescued. Usage is similar to
find_defense
.
find_cap2()
:
If
(m,n)
points to a string with 2 liberties,find_cap2(m,n,&i,&j)
looks for a configuration:O. .*where `O' is an element of the string in question. It tries the move at `*' and returns true this move captures the string, leaving
(i,j)
pointing to *.
chainlinks(int m, int n, int *adj,
int adji[MAXCHAIN], int adjj[MAXCHAIN], int adjsize[MAXCHAIN],
int adjlib[MAXCHAIN])
:
Find the CHAIN surrounding a string. This is the set of adjacent strings of the opposite color. The function
chainlinks()
returns (inadji
,adjj
arrays) these strings surrounding the group at(i, j)
. Ifstackp <= depth
, these are sorted by size (largest first). The size and number of liberties of each string are returned inadjsize
andadjlib
.
break_chain(int si, int sj, int *i, int *j, int *k, int *l)
:
The function
break_chain(si, sj, *i, *j, *k, *l)
returns 1 if part of some surrounding string is in atari, and if capturing this string results in a live string at(si, sj)
. Returns 2 if the capturing string can be taken (as in a snapback), or the the saving move depends on ignoring a ko threat; Returns 3 if the saving move requires making a ko threat and winning the ko. The pointers(i,j)
, if not NULL, are left pointing to the appropriate defensive move. The pointers(k,l)
, if not NULL, are left pointing to the boundary string which is in atari.
break_chain2(int si, int sj, int *i, int *j)
:
The function
break_chain2(si, sj, *i, *j)
returns 1 if there is a string in the surrounding chain having exactly two liberties whose attack leads to the rescue of(si, sj)
. Then *i, *j points to the location of the attacking move. Returns 2 if the attacking stone can be captured, 1 if it cannot.
snapback(snapback(int si, int sj, int i, int j, int color)
:
The function
snapback(si, sj, i, j, color)
considers a move by color at(i, j)
and returns true if the move is a snapback. Algorithm: It removes dead pieces of the other color, then returns 1 if the stone at(si, sj)
has <2 liberties. The purpose of this test is to avoid snapbacks. The locations(i, j)
and(si,sj)
may be either same or different. Also returns 1 if the move at(i, j)
is illegal, with the trace message "ko violation" which is the only way I think this could happen. It is not a snapback if the capturing stone can be recaptured on its own, e.g.XXOOOOO X*XXXXO -------Here `O' capturing at `*' is in atari, but this is not a snapback. Use with caution: you may want to condition the test on the string being captured not being a singleton. For example
XXXOOOOOOOO XO*XXXXXXXO -----------is rejected as a snapback, yet `O' captures more than it gives up.
To speed up the reading process, we note that a position can be reached in several different ways. In fact, it is a very common occurrence that a previously checked position is rechecked, often within the same search but from a different branch in the recursion tree.
This wastes a lot of computing resources, so in a number of places, we store away the current position, the function we are in, and which worm is under attack or to be defended. When the search for this position is finished, we also store away the result of the search and which move made the attack or defense succeed.
All this data is stored in a hash table where Go positions are the key and results of the reading for certain functions and groups are the data. You can increase the size of the Hash table using the -M or --memory option see section Invoking GNU Go: Command line options.
The hash table is created once and for all at the beginning of
the game by the function hashtable_new()
. Although hash
memory is thus allocated only once in the game, the table is
reinitialized at the beginning of each move by a call to
hashtable_clear()
from genmove()
.
The hash algorithm is called Zobrist hashing, and is a standard technique for go and chess programming. The algorithm as used by us works as follows:
attack3
, and it is implicit in the calling function which
player is to move.
The hash table consists of 3 parts:
When the hash table is created, these 3 areas are allocated using
malloc()
. When the hash table is populated, all contents are taken
from the Hash nodes and the Read results. No further allocation is
done and when all nodes or results are used, the hash table is full.
Nothing is deleted from the hash table except when it is totally
emptied, at which point it can be used again as if newly initialized.
When a function wants to use the hash table, it looks up the current
position using hashtable_search()
. If the position doesn't already
exist there, it can be entered using
Once the function has a pointer to the hash node containing a
function, it can search for a result of a previous search using
hashnode_search()
. If a result is found, it can be used, and
if not, a new result can be entered after a search using @findex
hashnode_new_result() hashnode_new_result()
.
Hash nodes which hash to the same position in the hash table (collisions) form a simple linked list. Read results for the same position, created by different functions and different attacked or defended strings also form a linked list.
This is deemed sufficiently efficient for now, but the representation of collisions could be changed in the future. It is also not determined what the optimum sizes for the hash table, the number of positions and the number of results are.
The reading code searches for a path through the move tree to determine whether a string can be captured. We have a tool for investigating this with the --decidestring option. This may be run with or without an output file.
Simply running
gnugo -t -l [input file name] -L [movenumber] --decidestring [location]
will run attack()
to determine whether the string can be captured.
If it can, it will also run find_defense()
to determine whether or
not it can be defended. It will give a count of the number of
variations read. The -t is necessary, or else GNU Go will not
report its findings.
If we add -o output file GNU Go will produce an output file with all variations considered. The variations are numbered in comments.
This file of variations is not very useful without a way of navigating the source code. This is provided with the GDB source file, listed at the end. You can source this from GDB, or just make it your GDB init file.
If you are using GDB to debug GNU Go you may find it less
confusing to compile without optimization. The optimization
sometimes changes the order in which program steps are
executed. For example, to compile `reading.c' without optimization,
edit `engine/Makefile' to remove the string -O2
from
the file, touch `engine/reading.c' and make. Note that the
Makefile is automatically generated and may get overwritten
later.
If in the course of reading you need to analyze a result where a function gets its value by returning a cached position from the hashing code, rerun the example with the hashing turned off by the command line option --hash 0. You should get the same result. (If you do not, please send us a bug report.) Don't run --hash 0 unless you have a good reason to, since it increases the number of variations.
With the source file given at the end of this document loaded, we can now navigate the variations. It is a good idea to use cgoban with a small -fontHeight, so that the variation window takes in a big picture. (You can resize the board.)
Suppose after perusing this file, we find that variation 17 is interesting and we would like to find out exactly what is going on here.
The macro 'jt n' will jump to the n-th variation.
(gdb) set args -l [filename] -L [move number] --decidestring [location] (gdb) tbreak main (gdb) run (gdb) jt 17
will then jump to the location in question.
Actually the attack variations and defense variations are numbered
separately. (But find_defense()
is only run if attack()
succeeds,
so the defense variations may or may not exist.) It is redundant to
have to tbreak main each time. So there are two macros avar and dvar.
(gdb) avar 17
restarts the program, and jumps to the 17-th attack variation.
(gdb) dvar 17
jumps to the 17-th defense variation. Both variation sets are found in the same sgf file, though they are numbered separately.
Other commands defined in this file:
dump
will print the move stack.nv
moves to the next variationascii i j
converts (i,j) to ascii ####################################################### ############### .gdbinit file ############### ####################################################### # this command displays the stack define dump set dump_stack() end # display the name of the move in ascii define ascii set gprintf("%o%m\n",$arg0,$arg1) end # move to the next variation define nv tbreak trymove continue finish next end # move forward to a particular variation define jt while (count_variations < $arg0) nv end nv dump end # restart, jump to a particular attack variation define avar delete tbreak sgf_decidestring run tbreak attack continue jt $arg0 end # restart, jump to a particular defense variation define dvar delete tbreak sgf_decidestring run tbreak attack continue finish next 3 jt $arg0 end