[Contents]   [Back]   [Prev]   [Up]   [Next]   [Forward]  


Reading

The process of visualizing potential moves done by you and your opponent to learn the result of different moves is called "reading".

Reading Basics

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':

The next few functions are essentially special cases of attack and find_defense. They are coded individually.

Hashing of positions

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

Calculation of the hash value

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:

  1. First we define a GO POSITION. This positions consists of It is not necessary to specify the color to move (white or black) as part of the position. The reason for this is that read results are stored separately for the various reading functions such as attack3, and it is implicit in the calling function which player is to move.
  2. For each location on the board we generate random numbers: These random numbers are generated once at initialization time and then used throughout the life time of the hash table.
  3. The hash key for a position is the XOR of all the random numbers which are applicable for the position (white stones, black stones, and ko position).

Organization of the hash table

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

hashtable_enter_position().

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.

Debugging the reading code

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 variation
ascii 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


[Contents]   [Back]   [Prev]   [Up]   [Next]   [Forward]