My new solver can deal with n-step problems, and even print out a Sensei's Library compatible solution. Not bad for 650 lines of code! I'll add text/code in a little while: right now I'm off to drink a celebratory beer or six.
* (printsl (solve (nth 23 *ps1*)))
The class responsible for look-ahead is the analyzer:
(defclass Analyzer () ((p :documentation "the problem being solved") (g :documentation "the current game state") (goal :documentation "the goal of this analyzer") (answer :documentation "success or failure") (resolved :documentation "t if answer is known") (children :documentation "the tree of games after this game state") (active-children :documentation "unresolved children") (next :documentation "next active child to get a tick"))
The analyzer has a problem and a current game state. It either declares the problem solved, or creates child analyzers that do the min-max search (each child has the inverse goal of its parent: SaveG, save a goup, is the inverse goal of KillG, kill a group, etc.)
"Tick" is the unit of computation: one tick is the time to create a new game state (e.g. playing a stone.) When an Analyzer gets a tick, it can either create a new game state, or pass the tick onto one of its children (round-robin style.) For now, my code doesn't obey the one game-state per tick design goal. Varously crappy short-cuts are also used in deciding life and death (e.g. 3 or more liberties means a group is safe,) but these are probably adequate for GGPFB1.
Observers will have noted that main classes (Position, Game, Analyzer) are all non-destructive (functional) in that they don't alter themselves, they create new versions of themselves that reflect the state change (e.g. a Position plus a Stone creates a new Position without changing the old Position.) This is by design: I get confused when things change underneath me.
The above diagrams were honestly generated by the code!
This method prints the initial problem and the solution:
(defmethod printsl ((a Analyzer) &optional text) (if text (print text)) (printsl (p a)) (format t *line-break*) (if (resolved a) (printsl (main-line a) (if (eq (current-player (game (p a))) *White*) "W Solution" "B Solution"))))
This one does the 1,2,3 thing to explain the play:
(defmethod printsl ((l Line) &optional text) (let* ((as (as l)) (pos (copy (pos (g (car as))))) (i 1)) (dolist (e as) (setf (at pos (last-move (g e))) (make-instance 'Stone 'PChar (character (format nil "~S" i)))) (setf i (1+ i))) (printsl pos text)))
And this one marks the stones that are the focus of the problem:
(defmethod printsl ((p Problem) &optional text) (if text (print text)) (let ((text (cond ((eq (Action (Goal p)) 'KillG ) (format nil " ~A to kill the marked stones." (name (current-player (game p))))) ((eq (Action (Goal p)) 'SaveG ) (format nil " ~A to save the marked stones." (name (current-player (game p))))) (t "Unknown goal.")))) (let ((pos (copy (Pos (Game p))))) (cond ((or (eq (Action (Goal p)) 'KillG ) (eq (Action (Goal p)) 'SaveG )) (mapcar (lambda (pnt) (setf (at pos pnt) (make-instance 'Stone 'PChar (MChar (at pos pnt))))) (points (at (smap (snake-set (game p))) (Focus (Goal p))))))) (printsl pos text))))
Hehe, I love my code:
* (printsl (solve (nth 24 *ps1*)))
The new code tears through Graded Go Problems For Beginners life and death problems until it hits Problem #41.