[HOME]

othello.lisp

(defconstant +empty+ #b00)
(defconstant +black+ #b01)
(defconstant +white+ #b10)

(defun bref (board row col)
  (let* ((start (* 2 (+ (* 8 row) col)))
	 (bit-1 (aref board start))
	 (bit-2 (aref board (1+ start))))
    (logior bit-1 (ash bit-2 1))
    ))

(defun set-bref (board row col val)
  (let ((start (* 2 (+ (* 8 row) col))))
    (setf (aref board start) (ash (logand val #b10) -1))
    (setf (aref board (1+ start)) (logand val #b01))
    val))

(defsetf bref set-bref)

(defun make-board ()
  (let ((board (make-sequence '(simple-bit-vector) 128 :initial-element +empty+)))
    (setf (bref board 3 3) +black+)
    (setf (bref board 3 4) +white+)
    (setf (bref board 4 3) +white+)
    (setf (bref board 4 4) +black+)
    board
    ))

(declaim (inline copy-board pos end-pos oponent))

(defun copy-board (board)
  (copy-seq board))

(defun print-board (board)
  (terpri)
  (write-line "          A B C D E F G H")
  (dotimes (j 8)
    (write-string "        ")
    (write (1+ j))
    (write-char #\SPACE)
    (dotimes (i 8)
      (case (bref board j i)
	(#.+empty+ (write-char #\.))
	(#.+black+ (write-char #\X))
	(#.+white+ (write-char #\O)))
      (write-char #\SPACE))
    (terpri))
  (terpri))

(defun pos (move) (first move))

(defun end-pos (move) (rest move))

(defun opponent (side)
  (if (= side +black+)
      +white+
    +black+))

(defun pos->string (pos)
  (assert (and (listp pos) (= (length pos) 2)
	       (numberp (first pos)) (numberp (second pos))
	       (>= (first pos) 0) (<= (first pos) 7)
	       (>= (second pos) 0) (<= (second pos) 7))
	  (pos))
  (let ((letter (code-char (+ (first pos) (char-int #\A))))
	(digit (code-char (+ (second pos) (char-int #\1))))
	(string (make-string 2)))
    (setf (aref string 0) letter)
    (setf (aref string 1) digit)
    string))

(defun string->pos (string)
  (assert (and (stringp string) (= (length string) 2)
	       (char>= (char string 0) #\A) (char<= (char string 0) #\H)
	       (char>= (char string 1) #\1) (char<= (char string 1) #\8))
	  (string))
  (let ((x (- (char-int (char string 0)) (char-int #\A)))
	(y (- (char-int (char string 1)) (char-int #\1))))
    (list x y)))

(defun input-move ()
  (format t "~&Move ? ")
  (loop
   (let ((move
	  (progn
	    (clear-input)
	    (string-upcase (string-trim " " (read-line))))))
     (cond
      ((string= move "QUIT")
       (return move))
      ((string= move "LIST")
       (return move))
      ((string= move "PASS")
       (return move))
      ((not (= (length move) 2))
       (progn
	 (format t "~&Entry has the wrong length!~%")
	 (format t "Reenter ? ")
	 (force-output)))
      (t (progn
	   (let ((letter (char move 0))
		 (digit (char move 1)))
	     (if (or (char< letter #\A) (char> letter #\H)
		     (char< digit #\1) (char> digit #\8))
		 (progn
		   (format t "~&Invalid syntax (expected A-H 1-8 as in A1)~%")
		   (format t "Reenter ? ")
		   (force-output))
	       (return-from input-move move))
	     )))
      ))))

(defun flip (board move side)
  "function: flip
         in: board - nb. modified
             move  - move and end-pos. See find-moves return type for details
             side - +black+ or +white+
    returns: board - pieces flipped
             Destructive on board"
  (let ((pos-1 (pos move)) (end-pos (end-pos move)))
    (dolist (pos-2 end-pos)
      (let* ((x-1 (first pos-1)) (y-1 (second pos-1))
	     (x-2 (first pos-2)) (y-2 (second pos-2))
	     (step-x (signum (- x-2 x-1)))
	     (step-y (signum (- y-2 y-1)))
	     (x x-1) (y y-1)
	     (x-start (if (>= step-x 0) x-2 x-1))
	     (x-end (if (>= step-x 0) x-1 x-2))
	     (y-start (if (>= step-y 0) y-2 y-1))
	     (y-end (if (>= step-y 0) y-1 y-2)))
	(loop until (or (> x x-start) (< x x-end) (> y y-start) (< y y-end)) do
	      (setf (bref board y x) (opponent side))
	      (incf x step-x)
	      (incf y step-y)))))
  board)

(defun find-line (board x-start y-start step-x step-y side)
  (let ((x x-start) (y y-start))
    (incf x step-x)
    (incf y step-y)
    (when (and (>= x 0) (<= x 7) (>= y 0) (<= y 7)
	       (= (bref board y x) (opponent side)))
      (incf x step-x)
      (incf y step-y)
      (loop until (or (< x 0) (> x 7) (< y 0) (> y 7)) do
	    (when (= (bref board y x) +empty+)
	      (return-from find-line nil))
	    (when (= (bref board y x) side)
	      (return-from find-line (list x y)))
	    (incf x step-x)
	    (incf y step-y)
	    ))
    nil))

(defun find-move (board x y side)
  (let ((temp nil))
    (macrolet ((set-tp-cons (var element)
			    `(progn
			       (setf temp ,element)
			       (when temp (setf ,var (cons temp ,var))))))
      (let ((end-pos nil))
	(set-tp-cons end-pos (find-line board x y  1  0 side))
	(set-tp-cons end-pos (find-line board x y -1  0 side))
	(set-tp-cons end-pos (find-line board x y  0  1 side))
	(set-tp-cons end-pos (find-line board x y  0 -1 side))
	(set-tp-cons end-pos (find-line board x y  1  1 side))
	(set-tp-cons end-pos (find-line board x y -1  1 side))
	(set-tp-cons end-pos (find-line board x y  1 -1 side))
	(set-tp-cons end-pos (find-line board x y -1 -1 side))
	end-pos))))

(defun find-moves (board side)
  "function: find-move
         in: board - see make-board
             side  - either X or O
    returns: ((legal-move end-pos) ...)
             legal-move is a position 
             end-pos is a list of end position's to flip along
             position is a tuple of integers (list 0-7 0-7)"
  (let ((moves nil))
    (dotimes (j 8)
      (dotimes (i 8)
	(when (= (bref board j i) +empty+)
	  (let ((pos (list i j))
		(end-pos (find-move board i j side)))
	    (when end-pos
	      (setf moves (cons (cons pos end-pos) moves)))
	    ))))
    (reverse moves)))

(defun print-moves (moves)
  "function: print-moves
         in: list of moves. See find-moves return type for details
    returns: comma seperated moves
         ex: \"A1, B2, C5\""
  (if moves
      (format t "~&Legal move~P ~:[is~;are~] ~A~%"
	      (length moves) (> (length moves) 1)
	      (reduce (lambda (e1 e2) (concatenate 'string e1 ", " e2))
		      (mapcar (lambda (e) (pos->string (pos e))) moves)))
    (format t "~&No moves. You must pass!~%")
    ))

(defun random-move-strategy (board side)
  "Select a legal disc at random"
  (let ((moves (find-moves board side)))
    (when moves
      (nth (random (length moves)) moves))))

(defun count-flipped (move)
  (let ((pos-1 (pos move)) (end-pos (end-pos move))
        (count 0))
    (dolist (pos-2 end-pos)
      (let* ((x-1 (first pos-1)) (y-1 (second pos-1))
             (x-2 (first pos-2)) (y-2 (second pos-2))
             (delta-x (- x-2 x-1)) (delta-y (- y-2 y-1))
             (line-count (1- (floor (sqrt (+ (* delta-x delta-x) (* delta-y delta-y)))))))
        (incf count line-count)))
    count))

(defun maximum-disc-strategy (board side)
  "Select from the moves that turn the largest number of pieces"
  (let ((moves (find-moves board side))
        (biggest-number 0)
        (best-moves nil))
    (dolist (move moves)
      (let ((number (count-flipped move)))
        (cond ((= number biggest-number)
               (setf best-moves (cons move best-moves)))
              ((> number biggest-number)
               (setf biggest-number number)
               (setf best-moves (cons move nil))))
        ))
    (if best-moves
        (nth (random (length best-moves)) best-moves)
      nil)
    ))

(defparameter +weights+
  #2A((120 -20 20  5  5 20 -20 120)
      (-20 -40 -5 -5 -5 -5 -40 -20)
      ( 20  -5 15  3  3 15  -5  20)
      (  5  -5  3  3  3  3  -5   5)
      (  5  -5  3  3  3  3  -5   5)
      ( 20  -5 15  3  3 15  -5  20)
      (-20 -40 -5 -5 -5 -5 -40 -20)
      (120 -20 20  5  5 20 -20 120)))

(defparameter +corner-nabours+
  '(((0 0) (0 1) (1 0) (1 1))
    ((0 7) (0 6) (1 7) (6 1))
    ((7 0) (6 0) (7 1) (1 6))
    ((7 7) (6 7) (7 6) (6 6))))

(defun stable-disc-positional-strategy (board move side)
  (let ((pos (pos move)))
    (aref (second pos) (first pos) +weights+)))

;;; order moves
;;; trasposition table (hash-table) for different moves leading to same position
;;; remember played moves
;;; no global variables (server will play multiple games in multible theads)

(defconstant +top-value+ most-positive-fixnum)
(defconstant +bottom-value+ most-negative-fixnum)
(defconstant +ply-depth+ 1)

(defun alpha-beta-search (board side alpha beta ply fn)
  (let ((moves (find-moves board side)))
    (if (not moves)
	(dolist (move moves)
	  (if (= ply +ply-depth+)
	      (fn move board)
	    (alpha-beta-search board (opponent side) beta alpha (1+ ply) fn))))))

(defun stable-disc-positional-strategy (board side)
  (let ((transposition-table (make-transposition-table)))
    (multiple-value-bind (val best-move)
	(alpha-beta-search board side +bottom-value+ +top-value+ +ply-depth+
			   #'stable-disk-positional-evaluator))))

(defun maximum-mobility-stategy (board side)
  (random-move-strategy board side))

(defun computer-move (board level side)
  (cond ((eq level 'clueless) (random-move-strategy board side))
	((eq level 'beginner) (maximum-disc-strategy board side))
	((eq level 'novice) (stable-disc-positional-strategy board side))
	((eq level 'expert) (maximum-mobility-stategy board side))
	(t (error "Not a legal level!"))))

(defun count-pieces (board)
  (let ((user 0) (computer 0))
    (dotimes (j 8)
      (dotimes (i 8)
	(case (bref board i j)
	  (#.+black+ (incf user))
	  (#.+white+ (incf computer)))))
    (values user computer)))

(defun game-finished (board)
  (let ((computer (find-moves board +white+))
	(user (find-moves board +black+)))
    (and (eq computer nil) (eq user nil))))

(defun move->positions (move moves)
  "Return move + end positions used in flip. nil if it is not a legal move"
  (find-if (lambda (e) (equal move (pos e))) moves))

(defun level-p (level)
  (find-if (lambda (e) (eq level e)) '(clueless beginner novice expert)))

(defun side-p (side)
  (member side (list +black+ +white+)))

(defun othello (&key (level 'beginner) (side +black+))
  (unless (level-p level)
    (format t "Not a legal level! (clueless beginner novice expert)~%")
    (return-from othello))
  (unless (side-p side)
    (format t "Not a legal side! (black white)~%")
    (return-from othello))
  (setf *random-state* (make-random-state t))
  (let ((board (make-board))
	(computer 0) (user 0))
    (format t "~&Othello~%2003 John Thingstad~%")
    (print-board board)
    (loop
     (block main
       (let ((response (input-move)))
	 (cond ((string= response "QUIT")
		(return-from othello nil))
	       ((string= response "LIST")
		(print-moves (find-moves board side))
		(return-from main))
	       (t
		(if (string= response "PASS")
		    (progn
		      (let ((moves (find-moves board side)))
			(when moves
			  (format t "~&You are not allowd to pass when you have legal moves!")
			  (return-from main))))
		  (progn
		    (let ((your-move (move->positions
				      (string->pos response)
				      (find-moves board side))))
		      (when (not your-move)
			(format t "~&~A is not a legal move!~%"  response)
			(return-from main))
		      (setf board (flip board your-move side)))))
		(let ((my-move (computer-move board level (opponent side))))
		  (if my-move
		      (progn
			(format t "~&I chose ~A.~%" (pos->string (first my-move)))
			(setf board (flip board my-move (opponent side))))
		    (format t "~&I have to pass.~%")))
		(print-board board)
		(multiple-value-setq (user computer) (count-pieces  board))
		(format t "~&White(O) - ~D, Black(X) - ~D~2%" computer user)
		))))
       
     (when (game-finished board)
       (cond
	((< user computer) (format t "~&I won!~%"))
	((> user computer) (format t "~&You won!~%"))
	(t (format t "~&It's a draw!~%")))
       (return-from othello nil)))
    ))