r3186: *** empty log message ***
[reversi.git] / base.lisp
diff --git a/base.lisp b/base.lisp
new file mode 100644 (file)
index 0000000..587a843
--- /dev/null
+++ b/base.lisp
@@ -0,0 +1,507 @@
+;;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10; Package: reversi -*-
+;;;;***************************************************************************
+;;;;
+;;;; FILE IDENTIFICATION
+;;;; 
+;;;;  Name:           base.lisp
+;;;;  Purpose:        Basic functions for reversi
+;;;;  Programer:      Kevin Rosenberg based on code by Peter Norvig
+;;;;  Date Started:   1 Nov 2001
+;;;;
+;;;; $Id: base.lisp,v 1.1 2002/10/25 12:42:42 kevin Exp $
+;;;;
+;;;; This file is Copyright (c) 2001-2002 by Kevin M. Rosenberg 
+;;;; and Copyright (c) 1998-2002 Peter Norvig
+;;;;
+;;;; Reversi users are granted the rights to distribute and use this software
+;;;; as governed by the terms of the Lisp Lesser GNU Public License
+;;;; (http://opensource.franz.com/preamble.html), also known as the LLGPL.
+;;;;***************************************************************************
+
+(in-package :reversi)
+(declaim (optimize (safety 1) (debug 3) (speed 3) (compilation-speed 0)))
+
+
+(defparameter +all-directions+ '(-11 -10 -9 -1 1 9 10 11))
+(defconstant +default-max-minutes+ 30)
+
+(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")
+;;(declaim (type (unsigned-byte 8) empty black white outer))
+(declaim (type fixnum empty black white outer))
+
+#|
+(deftype piece () '(unsigned-byte 8))
+(deftype player () '(unsigned-byte 8))
+(deftype move () '(unsigned-byte 8))
+(deftype square () '(unsigned-byte 8))
+|#
+
+(deftype piece () 'fixnum)
+(deftype player () 'fixnum)
+(deftype move () 'fixnum)
+(deftype square () 'fixnum)
+(deftype dir () 'fixnum)
+(deftype board () '(simple-array fixnum (100)))
+;;(deftype board () '(simple-array (unsigned-byte 8) (100)))
+(deftype clock () '(simple-array integer (3)))
+
+(defun make-moves ()
+  (make-array 60 :element-type 'cons :fill-pointer 0
+             :adjustable nil))
+(deftype moves () '(array cons (60)))
+
+
+(defclass reversi-game ()
+  ((bl-strategy :initarg :bl-strategy
+               :documentation "Strategy function for black"
+               :reader bl-strategy)
+   (wh-strategy :initarg :wh-strategy
+               :documentation "Strategy function for white"
+               :reader wh-strategy)
+   (board :type board :initarg :board
+         :documentation "The board configuration"
+         :reader board)
+   (move-number :type fixnum :initarg :move-number 
+               :documentation "The number of the move to be played"
+               :accessor move-number)
+   (player :type player :initarg :player
+               :documentation "ID of next player to move"
+               :accessor player)
+   (moves :type moves :initarg :moves
+         :documentation "An array of moves played in the game"
+         :accessor moves)
+   (print? :type boolean :initarg :print?
+          :documentation "Whether to print progress of this game"
+          :reader print?)
+   (record-game? :type boolean :initarg :record-game?
+          :documentation "Whether to record moves and clcck of this game"
+          :reader record-game?)
+   (final-result :type fixnum :initarg :final-result
+                :documentation "Final count, is NIL while game in play"
+                :accessor final-result)
+   (max-minutes :type fixnum :initarg :max-minutes
+               :documentation "Maximum minites for each player"
+               :reader max-minutes)
+   (clock :type clock :initarg :clock :initform nil
+         :documentation "An array of time-units left"
+         :accessor clock))
+  (:default-initargs 
+      :bl-strategy nil
+    :wh-strategy nil
+    :board (initial-board)
+    :move-number 1
+    :player black
+    :moves (make-moves)
+    :print? nil
+    :record-game? nil
+    :final-result nil
+    :max-minutes +default-max-minutes+
+    :clock (make-clock +default-max-minutes+)))
+
+
+(defun name-of (piece) (char ".@O?" piece))
+(defun title-of (piece) (nth (1- piece) '("Black" "White")) )
+       
+(defmacro opponent (player) 
+  `(if (= ,player black) white black))
+
+(defmacro bref (board square)
+  `(the piece (aref (the board ,board) (the square ,square))))
+
+(defparameter all-squares
+    (loop for i from 11 to 88 when (<= 1 (mod i 10) 8) collect i)
+  "A list of all squares")
+
+(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 'fixnum
+                           :initial-element outer
+                          :adjustable nil :fill-pointer nil)))
+    (declare (type board board))
+    (dolist (square all-squares)
+      (declare (fixnum square))
+      (setf (bref board square) empty))
+    (setf (bref board 44) white   (bref board 45) black
+          (bref board 54) black   (bref board 55) white)
+    board))
+
+(defun copy-board (board)
+  (copy-seq board))
+
+(defgeneric make-clock (clock))
+(defmethod make-clock ((clock array))
+  (make-array (+ 1 (max black white))
+             :element-type 'integer
+             :initial-contents clock
+             :adjustable nil
+             :fill-pointer nil))
+
+(defmethod make-clock ((minutes integer))
+  (make-array (+ 1 (max black white))
+             :element-type 'integer
+             :initial-element 
+             (* minutes 60 
+                internal-time-units-per-second)
+             :adjustable nil
+             :fill-pointer nil))
+
+(defun count-difference (player board)
+  "Count player's pieces minus opponent's pieces."
+  (declare (type board board)
+          (fixnum player))
+  (- (count player board)
+     (count (opponent player) board)))
+
+(defun valid-p (move)
+  (declare (type move move))
+  "Valid moves are numbers in the range 11-88 that end in 1-8."
+  (and (typep move 'move) (<= 11 move 88) (<= 1 (mod move 10) 8)))
+
+#+ignore
+(defun legal-p (move player board)
+  "A Legal move must be into an empty square, and it must
+  flip at least one opponent piece."
+  (declare (type board board)
+          (type move move)
+          (type player player))
+  (and (= (the piece (bref board move)) empty)
+       (some #'(lambda (dir) (declare (type dir dir)) (would-flip? move player board dir))
+             +all-directions+)))
+
+#+ignore
+(defun legal-p (move player board)
+  "A Legal move must be into an empty square, and it must
+  flip at least one opponent piece."
+  (declare (type board board)
+          (type move move)
+          (type player player)
+          (optimize speed (safety 0))
+)
+  (if (= (bref board move) empty)
+      (block search
+       (let ((i 0))
+         (declare (fixnum i))
+         (tagbody t
+           (when (>= i 8) (return-from search nil))
+           (when (would-flip? move player board (aref +all-directions+ i))
+             (return-from search t))
+           (incf i)
+           (go t))))
+    nil))
+
+(defun legal-p (move player board)
+  "A Legal move must be into an empty square, and it must
+  flip at least one opponent piece."
+  (declare (type board board)
+          (type move move)
+          (type player player)
+          (optimize (speed 3) (safety 0))
+)
+  (if (= (the piece (bref board move)) empty)
+      (block search
+       (dolist (dir +all-directions+)
+         (declare (type dir dir))
+         (when (would-flip? move player board dir)
+           (return-from search t)))
+       (return-from search nil))
+    nil))
+
+(defstruct (state (:constructor make-state-struct))
+  move player board clock)
+
+(defun make-state (move player clock board)
+  (make-state-struct :move move :player player :clock (make-clock clock) :board (copy-board board)))
+
+(defun make-game-move (game move player)
+  (when (record-game? game)
+    (vector-push (make-state move player (clock game) (board game))
+                (moves game)))
+  (make-move move player (board game))
+  (incf (move-number game)))
+
+(defun reset-game (game &optional (move-number 1))
+  (if (record-game? game)
+      (when (< move-number (move-number game))
+       (let ((old-state (aref (moves game) (1- move-number))))
+         (if old-state
+             (progn
+               (setf (player game) (state-player old-state))
+               (replace-board (board game) (state-board old-state))
+               (replace (clock game) (state-clock old-state))
+               (setf (fill-pointer (moves game)) (1- move-number))
+               (setf (move-number game) move-number))
+           (warn "Couldn't find old state"))))
+  (warn "Tried to reset game, but game is not being recorded")))
+  
+(defun make-move (move player board)
+  "Update board to reflect move by player"
+  ;; First make the move, then make any flips
+  (declare (type board board)
+          (type move move)
+          (type player)
+          (optimize (speed 3) (safety 0))
+)
+  (setf (bref board move) player)
+  (dolist (dir +all-directions+)
+    (declare (type dir dir))
+    (make-flips move player board dir))
+  board)
+
+(defun make-flips (move player board dir)
+  "Make any flips in the given direction."
+  (declare (type board board)
+          (type move move)
+          (type player player)
+          (type dir dir)
+          (optimize (speed 3) (safety 0))
+)
+  (let ((bracketer (would-flip? move player board dir)))
+    (when bracketer
+      (loop for c from (+ move dir) by dir until (= c (the fixnum 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
+  (declare (type board board)
+          (type move move)
+          (type player player)
+          (type dir dir)
+          (optimize (speed 3) (safety 0))
+)
+  (let ((c (+ move dir)))
+    (declare (type square c))
+    (and (= (the piece (bref board c)) (the player (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."
+  (declare (type board board)
+          (type square square)
+          (type player player)
+          (type dir dir)
+          (optimize (speed 3) (safety 0))
+)
+  (cond ((= (bref board square) player) square)
+        ((= (bref board square) (the player (opponent player)))
+         (find-bracketing-piece (the square (+ square dir)) player board dir))
+        (t nil)))
+
+(defun next-to-play (board previous-player &optional (print nil))
+  "Compute the player to move next, or NIL if nobody can move."
+  (declare (type board board)
+          (type player previous-player)
+          (type boolean print))
+  (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?"
+  (declare (type player player)
+          (type board board))
+  (some #'(lambda (move) (declare (type move move)) (legal-p move player board))
+        all-squares))
+
+
+(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.
+  (declare (type player player)
+          (type board board))
+  (loop for move in all-squares
+      when (legal-p move player board) collect move))
+
+#-allegro
+(defun replace-board (to from)
+  (replace to from))
+
+#+ignore
+(defun replace-board (to from)
+  (declare (type board to from)
+           (optimize (safety 0) (debug 0) (speed 3))
+)
+  (dotimes (i 100)
+    (declare (type 'fixnum i))
+    (setf (aref to i) (aref from i)))
+  to)
+
+#+allegro
+(defun replace-board (to from)
+  (declare (type board to from))
+  (ff::fslot-memory-copy to 0 400 from)
+  to)
+
+(defvar *ply-boards*
+  (apply #'vector (loop repeat 40 collect (initial-board))))
+
+
+
+(defvar *move-number* 1 "The number of the move to be played")
+(declaim (type fixnum *move-number*))
+
+(defun make-game (bl-strategy wh-strategy 
+                 &key 
+                 (print t) 
+                 (minutes +default-max-minutes+)
+                 (record-game nil))
+  (let ((game
+        (make-instance 'reversi-game :bl-strategy bl-strategy
+                       :wh-strategy wh-strategy
+                       :print? print
+                       :record-game? record-game
+                       :max-minutes minutes)))
+    (setf (clock game) (make-clock minutes))
+    game))
+
+(defun play-game (game)
+  (catch 'game-over
+    (until (null (player game))
+          (setq *move-number* (move-number game))
+          (get-move game
+                    (if (= (player game) black) 
+                        (bl-strategy game)
+                      (wh-strategy game))
+                    (player game)
+                    (board game) (print? game) (clock game))
+          (setf (player game) 
+            (next-to-play (board game) (player game) (print? game)))
+          (incf (move-number game))))
+  (when (print? game)
+    (format t "~&The game is over.  Final result:")
+    (print-board (board game) (clock game)))
+  (count-difference black (board game)))
+
+
+
+(defun reversi (bl-strategy wh-strategy 
+                &optional (print t) (minutes +default-max-minutes+))
+  (play-game (make-game bl-strategy wh-strategy :print print
+                       :record-game nil :minutes minutes)))
+
+
+#+ignore
+(defun reversi (bl-strategy wh-strategy 
+                &optional (print t) (minutes 30))
+  "Play a game of reversi.  Return the score, where a positive
+  difference means black, the first player, wins."
+  (let ((board (initial-board))
+        (clock (make-clock minutes)))
+    (declare (type board board))
+    (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 nil 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-clock +default-max-minutes+) "A copy of the game clock")
+(defvar *board* (initial-board) "A copy of the game board")
+
+(defun get-move (game 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* 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-game-move game move player))
+      (t (warn "Illegal move: ~a" (88->h8 move))
+         (get-move game strategy player board print clock)))))
+
+
+(defun random-reversi-series (strategy1 strategy2 
+                              n-pairs &optional (n-random 10))
+  "Play a series of 2*n games, starting from a random position."
+  (reversi-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 reversi-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 (reversi strategy1 strategy2 nil)
+             do (setf *random-state* random-state)
+             collect (- (reversi 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-reversi-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 (eql i j) '---
+                          (aref scores i j)))))))
+