;;;; -*- 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 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 #'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)))