;;; Tic-Tac-Toe ;;; ;;; For a board size of 3x3 and assuming that player X makes the first move every time there are: ;;; ;;; - 255,168 possible games ;;; - 131,184 games are won by X ;;; - 77,904 games are won by O ;;; - 46,080 games are a draw ;;; ;;; (play #'random-strategy #'random-strategy) should represent this. ;;; ;;; For playing human against computer use ;;; (play #'human (alpha-beta-searcher)) ;;; (play (alpha-beta-searcher) #'human) ;;; ;;; Beware: (play ... :size 4) and more needs a lot of time! (defconstant empty 0 "Empty field") (defconstant cross 1 "Player X") (defconstant circle 2 "Player O") (defun initial-board (n) "Return a board with empty fields of size NxN." (make-array (list n n) :element-type `(integer ,empty ,circle) :initial-element empty)) ;;; Output (defun print-board (board &optional (print t)) "Print a board." (when print (let ((size (first (array-dimensions board)))) (format t "~2&~3T") (loop for i below size do (format t "~A " (code-char (+ (char-code #\A) i)))) (loop for i below size do (format t "~&~2D" (1+ i)) (loop for j below size do (format t " ~A" (name-of (aref board i j)))))))) (defun name-of (player) "Return character of current player or of empty field." (char ".XO" player)) ;;; Access to board (defun place-piece (move player board) "Place a piece to a board location." (setf (row-major-aref board move) player)) (defun legal-p (move board) "A legal move must be into an empty field." (eql empty (row-major-aref board move))) (defun legal-moves (board) "Return list of legal moves." (loop for move below (array-total-size board) when (legal-p move board) collect move)) ;;; Play it (defun play (strategy-1 strategy-2 &key (size 3) (print t)) "Play the game of Tic-Tac-Toe. STRATEGY is a member of the functions: HUMAN Human player RANDOM-STRATEGY Computer plays legal random move" (let ((board (initial-board size))) (print-board board print) (loop repeat (array-total-size board) ; needed for draw for player = cross then (opponent player) ; X makes always the first move for strategy = (if (eql player cross) strategy-1 strategy-2) for move = (funcall strategy player board) do (place-piece move player board) (print-board board print) until (or (has-won-p cross board) (has-won-p circle board))) (evaluate cross board))) (defun opponent (player) "Return the opponent of PLAYER." (if (eql player cross) circle cross)) (defun has-won-p (player board) "A player has won if either a complete row, a complete column or a complete diagonal is finished." (let ((size (first (array-dimensions board)))) (or (loop for r below size thereis (loop for c below size always (eql player (aref board r c)))) ; complete row? (loop for c below size thereis (loop for r below size always (eql player (aref board r c)))) ; complete column? (loop for r below size for c below size always (eql player (aref board r c))) ; complete 1st diagonal? (loop for r below size for c from (1- size) downto 0 always (eql player (aref board r c)))))) ; complete 2nd diagonal? (defun evaluate (player board) "Evaluation function for BOARD." (cond ((has-won-p player board) 1) ((has-won-p (opponent player) board) -1) (t 0)))