(in-package :remote-snake-server-game) (defclass game () ((initial-size :initarg :initial-size :initform 3) (initial-position :initarg :initial-position :initform (list 0 0)) (snake :accessor snake) (dir :initarg :initial-direction :initform :right) (grid-size :initarg :grid-size :initform (list 30 30)) (food :initform (make-list 0)) (game-over :initform nil :accessor game-over))) ;;; Admin function (defmethod admin ((g game) &key (move nil move-supplied-p)) (with-slots (snake) g (when move-supplied-p (setf (nth 0 snake) move)))) ;;; Class constructor to initialize the snake (defmethod initialize-instance :after ((g game) &key) (with-slots (snake initial-size initial-position) g (setf snake (make-list initial-size :initial-element initial-position)))) ;;; Pretty-print a game (defmethod print-object ((g game) stream) (with-slots (snake dir food) g (format t "Snake: ") (dotimes (i (length snake)) (let ((elem (nth i snake))) (unless (eql i 0) (format t "<=")) (format t "(~a,~a)" (first elem) (second elem)))) (format t "~%Size: ~a" (length snake)) (format t "~%Direction: ~a" dir) (format t "~%Food: ~a" food))) (defgeneric dump (g) (:documentation "Dump a game. Return a plist.")) (defmethod dump ((g game)) (with-slots (snake food game-over) g (list :snake snake :food food :game-over game-over))) ;;; Note that there is no waranty that nb food are added (ex: if food position collide with snake position) (defgeneric add-food (g nb) (:documentation "Add food on the game grid.")) (defgeneric refresh (g &key) (:documentation "Refresh the game g (move forward or change direction).")) (defmethod refresh ((g game) &key (dir nil dir-supplied-p)) ;; First, update direction (with-slots ((active-dir dir)) g (when dir-supplied-p (if (and (or (eq dir :up) (eq dir :down) (eq dir :left) (eq dir :right)) (legal-move active-dir dir)) (setf (slot-value g 'dir) dir) (error "Invalid direction supplied")))) ;; Then, move the snake (with-slots (snake dir) g (let ((last-old-x nil) (last-old-y nil)) (dotimes (i (length snake)) (let ((elem (nth i snake))) (let ((x (first elem)) (y (second elem))) ;; Move snake (if (eql i 0) ; Move head (progn ;; Update last-old-x and last-old-y (to move the body when i>0) (setf last-old-x x) (setf last-old-y y) (cond ((eq dir :up) (incf y)) ((eq dir :down) (decf y)) ((eq dir :left) (decf x)) ((eq dir :right) (incf x)))) (progn ; Move body (rotatef x last-old-x) (rotatef y last-old-y))) (setf (nth i snake) (list x y))))))) ; Apply new snake location (update snake slot) ;; Check if snake eat something (with-slots (snake food) g (unless (eql (member (nth 0 snake) food :test #'equal-coord) nil) (setf food (remove-coord food (nth 0 snake))) ; Remove eated food (setf snake (grow-snake snake 1)))) ; Grow snake of 1 unit ;; Update food (with-slots (food) g (when (< (length food) 1) ; For the moment, allow only one food on the map (add-food g 1))) ;; Check if we loose (with-slots (snake game-over) g (when (member (nth 0 snake) (cdr snake) :test #'equal-coord) ; If head is member of body :D (setf game-over t)))) (defmethod add-food ((g game) nb) (with-slots (snake grid-size food) g (let ((size-x (first grid-size)) (size-y (second grid-size))) (dotimes (i nb) (let ((x (random size-x)) (y (random size-y))) (when (eq (member (list x y) snake :test #'equal-coord) nil) ; Add if there is no conflict between snake and food position (setf food (append food `(,(list x y)))))))) (setf food (remove-duplicates food :test #'equal-coord)))) ; Just in case two food are on the same coordinate