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