r3186: *** empty log message ***
[reversi.git] / original / othello-orig.lisp
diff --git a/original/othello-orig.lisp b/original/othello-orig.lisp
new file mode 100644 (file)
index 0000000..d964c83
--- /dev/null
@@ -0,0 +1,451 @@
+;;;; -*- Mode: Lisp; Syntax: Common-Lisp -*-
+;;;; Code from Paradigms of AI Programming
+;;;; Copyright (c) 1991 Peter Norvig
+
+;;;; File othello.lisp: An othello monitor, with all strategies
+;;;; up to and including section 18.8
+
+;;; One bug fix by Alberto Segre, segre@cs.cornell.edu, March 1993.
+
+(defun mappend (fn list)
+  "Append the results of calling fn on each element of list.
+  Like mapcon, but uses append instead of nconc."
+  (apply #'append (mapcar fn list)))
+
+(defun random-elt (seq) 
+  "Pick a random element out of a sequence."
+  (elt seq (random (length seq))))
+
+(defun concat-symbol (&rest args)
+  "Concatenate symbols or strings to form an interned symbol"
+  (intern (format nil "~{~a~}" args)))
+
+(defun cross-product (fn xlist ylist)
+  "Return a list of all (fn x y) values."
+  (mappend #'(lambda (y)
+               (mapcar #'(lambda (x) (funcall fn x y))
+                       xlist))
+           ylist))
+
+(defconstant all-directions '(-11 -10 -9 -1 1 9 10 11))
+
+(defconstant empty 0 "An empty square")
+(defconstant black 1 "A black piece")
+(defconstant white 2 "A white piece")
+(defconstant outer 3 "Marks squares outside the 8x8 board")
+
+(deftype piece () `(integer ,empty ,outer))
+
+(defun name-of (piece) (char ".@O?" piece))
+
+(defun opponent (player) (if (eql player black) white black))
+
+(deftype board () '(simple-array piece (100)))
+
+(defun bref (board square) (aref board square))
+(defsetf bref (board square) (val) 
+  `(setf (aref ,board ,square) ,val))
+
+(defun copy-board (board)
+  (copy-seq board))
+
+(defconstant all-squares
+  (loop for i from 11 to 88 when (<= 1 (mod i 10) 8) collect i))
+
+(defun initial-board ()
+  "Return a board, empty except for four pieces in the middle."
+  ;; Boards are 100-element vectors, with elements 11-88 used,
+  ;; and the others marked with the sentinel OUTER.  Initially
+  ;; the 4 center squares are taken, the others empty.
+  (let ((board (make-array 100 :element-type 'piece
+                           :initial-element outer)))
+    (dolist (square all-squares)
+      (setf (bref board square) empty))
+    (setf (bref board 44) white   (bref board 45) black
+          (bref board 54) black   (bref board 55) white)
+    board))
+
+(defun count-difference (player board)
+  "Count player's pieces minus opponent's pieces."
+  (- (count player board)
+     (count (opponent player) board)))
+
+(defun valid-p (move)
+  "Valid moves are numbers in the range 11-88 that end in 1-8."
+  (and (integerp move) (<= 11 move 88) (<= 1 (mod move 10) 8)))
+
+(defun legal-p (move player board)
+  "A Legal move must be into an empty square, and it must
+  flip at least one opponent piece."
+  (and (eql (bref board move) empty)
+       (some #'(lambda (dir) (would-flip? move player board dir))
+             all-directions)))
+
+(defun make-move (move player board)
+  "Update board to reflect move by player"
+  ;; First make the move, then make any flips
+  (setf (bref board move) player)
+  (dolist (dir all-directions)
+    (make-flips move player board dir))
+  board)
+
+(defun make-flips (move player board dir)
+  "Make any flips in the given direction."
+  (let ((bracketer (would-flip? move player board dir)))
+    (when bracketer
+      (loop for c from (+ move dir) by dir until (eql c bracketer)
+            do (setf (bref board c) player)))))
+
+(defun would-flip? (move player board dir)
+  "Would this move result in any flips in this direction?
+  If so, return the square number of the bracketing piece."
+  ;; A flip occurs if, starting at the adjacent square, c, there
+  ;; is a string of at least one opponent pieces, bracketed by 
+  ;; one of player's pieces
+  (let ((c (+ move dir)))
+    (and (eql (bref board c) (opponent player))
+         (find-bracketing-piece (+ c dir) player board dir))))
+
+(defun find-bracketing-piece (square player board dir)
+  "Return the square number of the bracketing piece."
+  (cond ((eql (bref board square) player) square)
+        ((eql (bref board square) (opponent player))
+         (find-bracketing-piece (+ square dir) player board dir))
+        (t nil)))
+
+(defun next-to-play (board previous-player print)
+  "Compute the player to move next, or NIL if nobody can move."
+  (let ((opp (opponent previous-player)))
+    (cond ((any-legal-move? opp board) opp)
+          ((any-legal-move? previous-player board) 
+           (when print
+             (format t "~&~c has no moves and must pass."
+                     (name-of opp)))
+           previous-player)
+          (t nil))))
+
+(defun any-legal-move? (player board)
+  "Does player have any legal moves in this position?"
+  (some #'(lambda (move) (legal-p move player board))
+        all-squares))
+
+(defun random-strategy (player board)
+  "Make any legal move."
+  (random-elt (legal-moves player board)))
+
+(defun legal-moves (player board)
+  "Returns a list of legal moves for player"
+  ;;*** fix, segre, 3/30/93.  Was remove-if, which can share with all-squares.
+  (loop for move in all-squares
+       when (legal-p move player board) collect move))
+
+(defun maximize-difference (player board)
+  "A strategy that maximizes the difference in pieces."
+  (funcall (maximizer #'count-difference) player board))
+
+(defun maximizer (eval-fn)
+  "Return a strategy that will consider every legal move,
+  apply EVAL-FN to each resulting board, and choose 
+  the move for which EVAL-FN returns the best score.
+  FN takes two arguments: the player-to-move and board"
+  #'(lambda (player board)
+      (let* ((moves (legal-moves player board))
+             (scores (mapcar #'(lambda (move)
+                                (funcall
+                                 eval-fn
+                                 player
+                                 (make-move move player
+                                            (copy-board board))))
+                             moves))
+             (best  (apply #'max scores)))
+        (elt moves (position best scores)))))
+
+(defparameter *weights*
+  '#(0   0   0  0  0  0  0   0   0 0
+     0 120 -20 20  5  5 20 -20 120 0
+     0 -20 -40 -5 -5 -5 -5 -40 -20 0
+     0  20  -5 15  3  3 15  -5  20 0
+     0   5  -5  3  3  3  3  -5   5 0
+     0   5  -5  3  3  3  3  -5   5 0
+     0  20  -5 15  3  3 15  -5  20 0
+     0 -20 -40 -5 -5 -5 -5 -40 -20 0
+     0 120 -20 20  5  5 20 -20 120 0
+     0   0   0  0  0  0  0   0   0 0))
+
+(defun weighted-squares (player board)
+  "Sum of the weights of player's squares minus opponent's."
+  (let ((opp (opponent player)))
+    (loop for i in all-squares
+          when (eql (bref board i) player) 
+          sum (aref *weights* i)
+          when (eql (bref board i) opp)
+          sum (- (aref *weights* i)))))
+
+(defconstant winning-value most-positive-fixnum)
+(defconstant losing-value  most-negative-fixnum)
+
+(defun final-value (player board)
+  "Is this a win, loss, or draw for player?"
+  (case (signum (count-difference player board))
+    (-1 losing-value)
+    ( 0 0)
+    (+1 winning-value)))
+
+(defun minimax (player board ply eval-fn)
+  "Find the best move, for PLAYER, according to EVAL-FN,
+  searching PLY levels deep and backing up values."
+  (if (= ply 0)
+      (funcall eval-fn player board)
+      (let ((moves (legal-moves player board)))
+        (if (null moves)
+            (if (any-legal-move? (opponent player) board)
+                (- (minimax (opponent player) board
+                            (- ply 1) eval-fn))
+                (final-value player board))
+            (let ((best-move nil)
+                  (best-val nil))
+              (dolist (move moves)
+                (let* ((board2 (make-move move player
+                                          (copy-board board)))
+                       (val (- (minimax
+                                 (opponent player) board2
+                                 (- ply 1) eval-fn))))
+                  (when (or (null best-val)
+                            (> val best-val))
+                    (setf best-val val)
+                    (setf best-move move))))
+              (values best-val best-move))))))
+
+(defun minimax-searcher (ply eval-fn)
+  "A strategy that searches PLY levels and then uses EVAL-FN."
+  #'(lambda (player board)
+      (multiple-value-bind (value move)
+          (minimax player board ply eval-fn) 
+        (declare (ignore value))
+        move)))
+
+(defun alpha-beta (player board achievable cutoff ply eval-fn)
+  "Find the best move, for PLAYER, according to EVAL-FN,
+  searching PLY levels deep and backing up values,
+  using cutoffs whenever possible."
+  (if (= ply 0)
+      (funcall eval-fn player board)
+      (let ((moves (legal-moves player board)))
+        (if (null moves)
+            (if (any-legal-move? (opponent player) board)
+                (- (alpha-beta (opponent player) board
+                               (- cutoff) (- achievable)
+                               (- ply 1) eval-fn))
+                (final-value player board))
+            (let ((best-move (first moves)))
+              (loop for move in moves do
+                (let* ((board2 (make-move move player
+                                          (copy-board board)))
+                       (val (- (alpha-beta
+                                 (opponent player) board2
+                                 (- cutoff) (- achievable)
+                                 (- ply 1) eval-fn))))
+                  (when (> val achievable)
+                    (setf achievable val)
+                    (setf best-move move)))
+                until (>= achievable cutoff))
+              (values achievable best-move))))))
+
+(defun alpha-beta-searcher (depth eval-fn)
+  "A strategy that searches to DEPTH and then uses EVAL-FN."
+  #'(lambda (player board)
+      (multiple-value-bind (value move)
+          (alpha-beta player board losing-value winning-value
+                      depth eval-fn) 
+        (declare (ignore value))
+        move)))
+
+(defun modified-weighted-squares (player board)
+  "Like WEIGHTED-SQUARES, but don't take off for moving
+  near an occupied corner."
+  (let ((w (weighted-squares player board)))
+    (dolist (corner '(11 18 81 88))
+      (when (not (eql (bref board corner) empty))
+        (dolist (c (neighbors corner))
+          (when (not (eql (bref board c) empty))
+            (incf w (* (- 5 (aref *weights* c))
+                       (if (eql (bref board c) player)
+                           +1 -1)))))))
+    w))
+
+(let ((neighbor-table (make-array 100 :initial-element nil)))
+  ;; Initialize the neighbor table
+  (dolist (square all-squares)
+    (dolist (dir all-directions)
+      (if (valid-p (+ square dir))
+          (push (+ square dir)
+                (aref neighbor-table square)))))
+
+  (defun neighbors (square)
+    "Return a list of all squares adjacent to a square."
+    (aref neighbor-table square)))
+
+(let ((square-names 
+        (cross-product #'concat-symbol
+                       '(? a b c d e f g h ?)
+                       '(? 1 2 3 4 5 6 7 8 ?))))
+
+  (defun h8->88 (str)
+    "Convert from alphanumeric to numeric square notation."
+    (or (position (string str) square-names :test #'string-equal)
+        str))
+
+  (defun 88->h8 (num)
+    "Convert from numeric to alphanumeric square notation."
+    (if (valid-p num)
+        (elt square-names num)
+        num)))
+
+(defun human (player board)
+  "A human player for the game of Othello"
+  (format t "~&~c to move ~a: " (name-of player)
+          (mapcar #'88->h8 (legal-moves player board)))
+  (h8->88 (read)))
+
+(defvar *move-number* 1 "The number of the move to be played")
+
+(defun othello (bl-strategy wh-strategy 
+                &optional (print t) (minutes 30))
+  "Play a game of othello.  Return the score, where a positive
+  difference means black, the first player, wins."
+  (let ((board (initial-board))
+        (clock (make-array (+ 1 (max black white))
+                           :initial-element 
+                           (* minutes 60 
+                              internal-time-units-per-second))))
+    (catch 'game-over
+      (loop for *move-number* from 1
+            for player = black then (next-to-play board player print)
+            for strategy = (if (eql player black) 
+                               bl-strategy
+                               wh-strategy)
+            until (null player)
+            do (get-move strategy player board print clock))
+      (when print
+        (format t "~&The game is over.  Final result:")
+        (print-board board clock))
+      (count-difference black board))))
+
+(defvar *clock* (make-array 3) "A copy of the game clock")
+(defvar *board* (initial-board) "A copy of the game board")
+
+(defun get-move (strategy player board print clock)
+  "Call the player's strategy function to get a move.
+  Keep calling until a legal move is made."
+  ;; Note we don't pass the strategy function the REAL board.
+  ;; If we did, it could cheat by changing the pieces on the board.
+  (when print (print-board board clock))
+  (replace *clock* clock)
+  (let* ((t0 (get-internal-real-time))
+         (move (funcall strategy player (replace *board* board)))
+         (t1 (get-internal-real-time)))
+    (decf (elt clock player) (- t1 t0))
+    (cond
+      ((< (elt clock player) 0)
+       (format t "~&~c has no time left and forfeits."
+               (name-of player))
+       (THROW 'game-over (if (eql player black) -64 64)))
+      ((eq move 'resign)
+       (THROW 'game-over (if (eql player black) -64 64)))
+      ((and (valid-p move) (legal-p move player board))
+       (when print
+         (format t "~&~c moves to ~a." 
+                 (name-of player) (88->h8 move)))
+       (make-move move player board))
+      (t (warn "Illegal move: ~a" (88->h8 move))
+         (get-move strategy player board print clock)))))
+
+(defun print-board (&optional (board *board*) clock)
+  "Print a board, along with some statistics."
+  ;; First print the header and the current score
+  (format t "~2&    a b c d e f g h   [~c=~2a ~c=~2a (~@d)]"
+          (name-of black) (count black board)
+          (name-of white) (count white board)
+          (count-difference black board))
+  ;; Print the board itself
+  (loop for row from 1 to 8 do
+        (format t "~&  ~d " row)
+        (loop for col from 1 to 8
+              for piece = (bref board (+ col (* 10 row)))
+              do (format t "~c " (name-of piece))))
+  ;; Finally print the time remaining for each player
+  (when clock
+    (format t "  [~c=~a ~c=~a]~2&"
+            (name-of black) (time-string (elt clock black))
+            (name-of white) (time-string (elt clock white)))))
+
+(defun time-string (time)
+  "Return a string representing this internal time in min:secs."
+  (multiple-value-bind (min sec)
+      (floor (round time internal-time-units-per-second) 60)
+    (format nil "~2d:~2,'0d" min sec)))
+
+(defun random-othello-series (strategy1 strategy2 
+                              n-pairs &optional (n-random 10))
+  "Play a series of 2*n games, starting from a random position."
+  (othello-series
+    (switch-strategies #'random-strategy n-random strategy1)
+    (switch-strategies #'random-strategy n-random strategy2)
+    n-pairs))
+
+(defun switch-strategies (strategy1 m strategy2)
+  "Make a new strategy that plays strategy1 for m moves,
+  then plays according to strategy2."
+  #'(lambda (player board)
+      (funcall (if (<= *move-number* m) strategy1 strategy2)
+               player board)))
+
+(defun othello-series (strategy1 strategy2 n-pairs)
+  "Play a series of 2*n-pairs games, swapping sides."
+  (let ((scores
+          (loop repeat n-pairs
+             for random-state = (make-random-state)
+             collect (othello strategy1 strategy2 nil)
+             do (setf *random-state* random-state)
+             collect (- (othello strategy2 strategy1 nil)))))
+    ;; Return the number of wins (1/2 for a tie),
+    ;; the total of the point differences, and the
+    ;; scores themselves, all from strategy1's point of view.
+    (values (+ (count-if #'plusp scores)
+               (/ (count-if #'zerop scores) 2))
+            (apply #'+ scores)
+            scores)))
+
+(defun round-robin (strategies n-pairs &optional
+                    (n-random 10) (names strategies))
+  "Play a tournament among the strategies.
+  N-PAIRS = games each strategy plays as each color against
+  each opponent.  So with N strategies, a total of
+  N*(N-1)*N-PAIRS games are played."
+  (let* ((N (length strategies))
+         (totals (make-array N :initial-element 0))
+         (scores (make-array (list N N)
+                             :initial-element 0)))
+    ;; Play the games
+    (dotimes (i N)
+      (loop for j from (+ i 1) to (- N 1) do 
+          (let* ((wins (random-othello-series
+                         (elt strategies i)
+                         (elt strategies j)
+                         n-pairs n-random))
+                 (losses (- (* 2 n-pairs) wins)))
+            (incf (aref scores i j) wins)
+            (incf (aref scores j i) losses)
+            (incf (aref totals i) wins)
+            (incf (aref totals j) losses))))
+    ;; Print the results
+    (dotimes (i N)
+      (format t "~&~a~20T ~4f: " (elt names i) (elt totals i))
+      (dotimes (j N)
+        (format t "~4f " (if (= i j) '---
+                             (aref scores i j)))))))
+
+(defun mobility (player board)
+  "The number of moves a player has."
+  (length (legal-moves player board)))
+