The Code So Far

    Keywords: Software

Yes, I'm trying to write a Go program. Feel free to jump in comments, corrections, or criticisms.

My first try at was in C++. After 120 lines of code, I realized it was going to be heavy going... types were becoming concrete too quickly and allocation was becoming too explicit. I was quickly being pushed towards writing a custom little language for Go. I downloaded GnuGo and OpenGo, but neither seemed able to manipulate Go stuff at the level I was hoping for.

My apologies for the large amount of computer code on this and referenced pages. I don't want to make SL a CVS repository, but I do want to show what I'm doing without handwaving.


  • dnerra: Can you give some hint what you were missing in GNU Go's board code? That would be interesting.
  • Gorobei: Ouch! My sentence sounds as if GNU Go's board code cannot express the great ideas in my mind :) GNU Go's board code is better than anything I'm going to be able to write in the near future. I should have said that neither makes it easy for me to manipulate Go stuff at my level. I'm a Go beginner, and thus no more understand GNU Go at a fundamental level than I understand Go at a fundamental level. I need an interactive environment where I can try out my ideas, poke around, do "what-ifs", etc. For example, I really like the GNU Go regression failure page, but I find myself wanting to apply an arbitrary function to each point on the board so I can understand what might be going on. My manipulation complaint is about the programming environment, not the specific program.

So I fired up CMUCL, pointed a browser at some doc, and tried to write code. ..

It's embarassing to note that it took me 20 hours to write 130 lines of CL. That's about 10 hours of reading tutorials and specs, 2 hours of learning the debugger, 1 hour of writing code, 5 hours of tightening code (mapcan rocks,) and 2 hours refactoring. "Refactoring" is a term us computer pros use: it means we screwed up the design (e.g. didn't realize a point on the Goban was a useful entity in its own right) and had to fix it later.

Here is a Common LISP Wiki site: [ext] http://ww.telent.net/cliki/index

Finally, i can write:

 * (setq *GGPFB-1-1* (make-position 13
          '(( 6 . 5 ) ( 5 . 6 ) ( 6 . 7 ))
         '(( 6 . 6 ) ( 5 . 7 ) ( 5 . 8 ))))
 * (printb *GGPFB-1-1* )
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . X O O . . . .
 . . . . . X O X . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .

The code so far is enough to make finding strings easy:

 (defmethod take-string ((b Position) (p Point) (s Stone))
   (setf (at b p) *Empty*)
   (cons p (mapcan (lambda (pa)
       (if (eq (at b pa) s)
    (take-string b pa s)))
     (adjacent p))))
 (defmethod strings ((bo Position))
   (let ((b (copy bo)))
     (mapcan (lambda (p)
        (if (not (empty? b p))
     (list (take-string b p (at b p)))))
      (all-points b))))

Thusly:

 * (strings *GGPFB-1-1*)
 ((#<POINT {480334C5}>)
  (#<POINT {480336BD}> #<POINT {480338B5}>)
  (#<POINT {48034CB5}>)
  (#<POINT {48034EAD}>)
  (#<POINT {480350A5}>))

Before playing with strings, I need a way to represent a game:

Woohoo! We can now almost do real stuff:

 * (printb *g01*)
 . X O . . . . . . . . . .
 X . X O . . . . . . . . .
 . X O . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
     White to play.
 * (printb (play *g01* '(1 . 1)))
 . X O . . . . . . . . . .
 X O . O . . . . . . . . .
 . X O . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
 . . . . . . . . . . . . .
     Black to play. (ko at 1, 2)

Finally, we define a printer for Sensei's Library, so we can say:

 * (printsl *G181* )
[Diagram]

White to play.


At this time, I'll try to write a Simple Life and Death solver.

March 17, 2002: a Better Life and Death solver.

March 18, 2002: It solves a two eyes to live problem. In retrospect, how did I miss the fact that passing is a fundamental concept: maybe because you can't pass in Bridge play, Chess, or Checkers?


At LordOfPi's request, here is the code and problem sets: [ext] http://members.bellatlantic.net/~mmn/go/go.tar.gz Note that it's really crappy, and I am trying to improve it by the hour.

Earlier, I said that I didn't want to use SL as a CVS repository. My code is changing quite quickly, however, and thus I think I need to version the code so that text can be compared with the code as it existed at the time I wrote the text. Here's The current code, SL's diff feature works nicely here. I'll ask in MessagesToPeopleCurrentlyPresentInTheLibrary if this is beyond the pale.

[Diagram]

1-45b: White to save the marked stones.

Here's the next problem: white thinks she is safe because she has 3 liberties. Worse, the key point (the corner) is not even initially visible to my solver because it only considers adjacent stones.

While I try to figure out what to do, I fix up the the code (revision 4) a little (based on input from a friend that actually knows Lisp.) Things get better names, globals get declared more nicely, I learn the case form, when, :reader and other good things.

I added the first ladder problem from LessonsInTheFundamentalsOfGo to my problem set: yuck, my code is nowhere near to being able to solve it.

Several bugs get removed (e.g my s-uniq function didn't work,) and playing with the tick routine convinced me it needs a rewrite.


I think:

  • a tick must be the generation of one new possible future position. This is the unit of CPU usage cost.
  • analysis strategies can be compared by looking at the number of ticks each took to solve certain problems.

An analyzer must decide between generating a new move for its position and giving the tick to one of its children. I.e. the tree of analyzers form a pruned search of the best move with each analyzer making its own pruning decisions. My initial tick routine was doing this pruning by just excluding all plays that did not abut the target group of stones. It's good feature was that it analyzes low-branching lines more deeply than high-branching lines (due to the recursive round-robin tick scheduler.)

Problem 1-45b requires a better pruning algorithm. I could just punt and increase the candidate child space (e.g. add plays that are one step away from the target string,) but this just slows down the solver while postponing the inevitable (I'll need two steps, then threes, etc, to solve more advanced problems.)


I change the Analyzer class so that it now has an Evaluator (a judge of goodness,) a Scheduler (the thing that decides who gets the next tick,) and a Generator (the creator of new positions for this analyzer.) This proves to be way too ambitious: the classes interact too strongly, the code gets ugly fast. Worse, it doesn't help me close on my main problem: how will I distribute ticks to the analyzer tree?

I abandon that approach and try something more conservative.... Everything above the class of Position (now called Game-Board) got basically rewritten: the Analyzers use a real min-max search, the Generators submit candidates as a group for fairer scheduling. The problems get an answer slot (the move that kills/saves,) and reg-tests become a lot less verbose. Various uglinesses are removed from the code (but many remain.) Several bugs are fixed. The worst bug was a misimplementation of the ko rule (Graded Go 1-63 exposed this.) For now, I remove all ko code: I'll add back it when I need it. The code still doesn't understand when a group is safe: it can't resolve tight situations, as some problems from the regtest show...



 1-1 Right: Black to kill. 8 ticks, move is 7.6
 1-2 Right: Black to kill. 7 ticks, move is 12.7
 1-3 Right: Black to kill. 9 ticks, move is 11.12
 1-4 Right: Black to kill. 10 ticks, move is 2.5
 1-5 Right: Black to kill. 15 ticks, move is 12.6
 1-6 Right: Black to kill. 9 ticks, move is 12.6
 1-7 Right: Black to kill. 11 ticks, move is 5.4
 1-8 Right: Black to kill. 10 ticks, move is 5.5
 1-9 Right: Black to kill. 12 ticks, move is 5.4
 1-10 Right: Black to kill. 11 ticks, move is 6.6
 1-11 Right: Black to kill. 12 ticks, move is 12.6
 1-12 Right: Black to kill. 13 ticks, move is 12.3
 1-13 Right: Black to kill. 11 ticks, move is 6.6
 1-14 Right: Black to kill. 14 ticks, move is 0.5
 1-15 Right: Black to live. 8 ticks, move is 8.9
 1-16 Right: Black to live. 1500 ticks, move is 3.4      !!!!
 1-17 Right: Black to live. 1500 ticks, move is 6.10     !!!!
 1-18 Right: Black to live. 1500 ticks, move is 12.6     !!!!
 1-19 Right: Black to live. 1500 ticks, move is 4.4      !!!!
 1-20 Right: Black to live. 1500 ticks, move is 12.6     !!!!
 1-21 Right: Black to live. 1500 ticks, move is 7.5      !!!!
 1-23 Right: Black to kill. 85 ticks, move is 12.6
 1-24 Right: Black to kill. 535 ticks, move is 7.6
 1-25 Right: Black to kill. 114 ticks, move is 4.5
 1-26 Right: Black to kill. 636 ticks, move is 6.5
 1-27 Right: Black to kill. 114 ticks, move is 5.6
 1-28 Right: Black to kill. 85 ticks, move is 5.6
 1-41 Right: Black to live. 523 ticks, move is 0.4
 1-42 Right: Black to kill. 250 ticks, move is 1.6
 1-43 Right: Black to live. 298 ticks, move is 3.7
 1-45a Right: White to live. 92 ticks, move is 0.6
 1-45b Right: White to live. 202 ticks, move is 12.0

April 9 2002: Time to think about optimizing board generation (1-45b was solved by letting the generator produce 1-hop moves at a lower priority.) This helps solve several problems, but the tick-counts are growing too fast for my liking.

I reshuffle the code again: having the move generator be in the analyzer's children was getting just too ugly (e.g. the methods max-status and min-status.) More code becomes idiomatic. I start work on a few tools to inspect the analyzer trees (raw dumps are unreadable for over 100 or so leaves.) The code is almost ready for modified alpha-beta pruning.


May 23 2002: Moved to a new apartment, had a baby, got DSL reinstalled. Being as I'm on late-night baby feeding duty, I take the opportunity to learn more LISP (mostly reading Paul Graham's On Lisp,) and fix some of my ugly code (I really like the separate namespaces for functions and variables.)

I spend today writing a really crappy SGF file reader -- this makes it much easier to create problem sets. Next I'll add a SGF writer so I can see what the solver is doing.

dnerra: Best wishes for your family!

Btw -- if you want to use problem sets -- why not just implement small parts of the GTP protocol ("loadsgf" and "genmove"), and you could use most of the GNU Go test suite problems? Should save you a lot of hassle creating problems!

Gorobei: GTP looks like an excellent idea (I look at my ~/gnugo-3.0.0 directory.) I will add that as soon as the code gets out of the "toy" range.


May 26, 2000: Code cleanup continues: things get named what they are (e.g. death to single-character arguments,) I define several print-object methods, I rename some classes to match GnuGo better.

My program is getting stuck: the fan-out on games is so high that it can't see deeply enough to solve the next group of harder problems. GGPFG1-22 is the first sign of this, but any board with a lot of stones is causing severe problems. I'm rapidly getting disillusioned with the alpha-beta search I'm using - whole boards are just too big, if I use restrict myself to subsets of the board, how did I deal with ko?

I delete all the code concerned with alpha-beta searching, the generator class, most of the analyzer class. I write a little SGF printer for the gutted analyzer.

I have decent representations for Boards, Games, Worms, and Problems. I'll try to write a toy static move guesser on top of these.

This page is getting too big - time for [Chapter 2 | /2]


This is a copy of the living page "The Code So Far" at Sensei's Library.
(OC) 2005 the Authors, published under the OpenContent License V1.0.
[Welcome to Sensei's Library!]
StartingPoints
ReferenceSection
About