Showing posts with label Video Games. Show all posts
Showing posts with label Video Games. Show all posts

Saturday, March 3, 2012

StrifeBarge Update and My Secure Misconceptions

Before I get to the StrifeBarge update, I've been thinking about a problem. It's one I vaguely assumed had been definitively solved, but it turns out that's only a matter of perspective. Now that I really sit down to think about how I'd implement a "solution", it's quite obvious that there is no good way of countering a sufficiently long-running and elaborate man-in-the-middle attack.

  1. You can thwart mere eavesdroppers by using DH or similar
  2. You can counter short-term MitM attacks by relying on keys that you exchange with Alice before the attack started
  3. You can counter long-term MitM attacks by relying on some additional authentication channel like a phonecall, video conference or SMS chain
  4. You can counter elaborate, long-term MitM attacks by relying on a web of trust to confirm Alice's key
  5. You can counter elaborate, network-saturating, long-term MitM attacks by flying out and actually meeting Alice, exchanging keys in both senses

I'm not entirely sure how you can counter supremely elaborate, long-term MitM attacks, but it probably involves several independent notaries that hate each other as well as Alice[1], or possibly a DNA sample in addition to the key exchange. That list was cumulative, by the way, not independent.

Handshakes or public-key crypto get you past regular old surveillance. Both together can get you past an active attacker on the current line, but that's basically it. After that, any technique you try only buys you a bit of extra confidence that your messages aren't being tampered with, and that confidence approaches 100% without ever getting there. Certificate authorities can be compromised, networks of trust can be saturated by an attacker's agents[2], and any other authentication mechanism I can think of can be faked by a sufficiently motivated attacker.

Now, the good news is that StrifeBarge isn't anything like a juicy enough target to tempt the Chinese government into trying something. The bad news is that it's never really possible to fully trust a given user. The best you can hope to do is maximize the chance that they're really who they say they are. From another perspective, the best you can do is make the resource expenditure required to fool your authentication greater than the potential payoff in succeeding. It's relevant, because you don't want an opponent to be able to peek over at your map during a game for some hopefully obvious reasons[3]. This really, truly looked like it should have been solved already, until I plucked it from the periphery of my thoughts and focused on it for a couple of days. Upon closer inspection, I'm not sure why I ever thought that.

With that, let's move on to the highlights.

The big change is that I've settled on a license. The intention is that this game should be out in public for educational purposes in case anyone actually cares, so the AGPL seems most appropriate[4]. I may also pull out the basic stuff into a separate turn-based-HTTP-game framework that I will release under MIT/BSD for people to more easily mess with. That's a long way off[5], but I thought I'd mention it.

The big, noticeable change is that styles, images and js have been incorporated semi-well into the program. It's gone into space rather than the traditional naval setting. Collecting the sprites was easier than it should have been, by the way, and that's entirely thanks to OpenGameArt. The star background is going to have to be replaced by something snazzier at some point, but the rest of the graphics are actually quite nice.

Part of integrating CSS included using cl-css. I tried to avoid it, I really did, because I wanted you to be able to install StrifeBarge just by ql:quickloading the single appropriate package name, but doing CSS3 directives manually sucks some of the biggest donkey testicles that exist on this earth. I decided that the ability to define functions like

;;; css.lisp

(defun css-rotate (degrees)
  (let ((d (format nil "rotate(~adeg)" degrees)))
    `(:transform ,d
      :-ms-transform ,d
      :-webkit-transform ,d
      :-o-transform ,d
      :-moz-transform ,d)))

instead of manually writing this shit every time just about trumps ease of installation. Once the web stabilizes enough that the big browser writers don't feel the need to implement their own cutely named directives and ignore CSS3 equivalents, I'll ditch my CSS generator. Until then, I will happily debase myself for the ability to define higher level constructs which will deal with the equine testicle sucking on my behalf. I guess I could submit the library for consideration in quicklisp, but I really, really don't want to until I've ironed out that case-insensitivity issue, and possibly defined CSS3-abstracting-functions as part of the default library (though I'm still pretty sure cl-css will ever attempt to validate your input).

Finally, the default mapcan bit me, so I needed a replacement.

;;; a/util.lisp
... 

(defun mapcan-f (fn a-list)
  "Functional unary mapcan"
  (loop for i in a-list append (funcall fn i)))

...

I estimate having spent a good twenty minutes or so puzzling over why (ships a-player) was suddenly returning a very odd list of elements once a game had started. The reason is that mapcan uses nconc to put its results together, which means that the arguments are going to be modified destructively. So I guess this is another place where State Is Hard. I ended up defining the functional, unary mapcan seen above, which suffices for my purposes.

And on we go to the diffs. I don't actually expect anyone to read past this, by the way, but thinking about code I wrote well enough to explain it prosaically has proven to be a very effective technique[6]. Oh, also, before I forget, we've gone from 220 lines to about 550 (and that new count doesn't include the license info, generated css/js, or any of the images being tracked as binary files).

diff --git a/board.lisp b/board.lisp
index b18c662..b25c106 100644
--- a/board.lisp
+++ b/board.lisp
@@ -16,22 +16,39 @@
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 (defmethod space-at ((b board) x y) (nth x (nth y (spaces b))))
 
+(defun create-point (direction x y i j)
+  (if (eq :vertical direction)
+      (list (+ i x) (+ j y))
+      (list (+ j x) (+ i y))))
+
 (defmethod assign-ship-spaces ((s ship) direction x y)
-  (loop for i from 0 to (- (space-count s) 1)
-       if (eq :vertical direction)
-         collect (cons x (+ i y))
-       else
-         collect (cons (+ i x) y)))
+  "Given a ship, a direction and an initial x/y, 
+returns a list of spaces that the ship will occupy."
+  (mapcan (lambda (i) 
+           (mapcar (lambda (j)
+                     (create-point direction x y i j)) 
+                   (range 0 (- (len s) 1))))
+         (range 0 (- (wid s) 1))))
 
 (defmethod position-ship ((s ship) (b board))
-  (let* ((x (random (- (width b) (space-count s))))
-        (y (random (- (height b) (space-count s))))
-        (direction (pick '(:vertical :horizontal)))
+  "Given a ship and a board, positions a ship on the board,
+ensuring there are no collisions."
+  (let* ((direction (pick '(:vertical :horizontal)))
+        (ship-v-padding (+ 1 (if (eq :vertical direction) (len s) (wid s))))
+        (ship-h-padding (+ 1 (if (eq :vertical direction) (wid s) (len s)))) 
+        (x (random (- (width b) ship-h-padding))) 
+        (y (random (- (height b) ship-v-padding)))
         (ship-spaces (assign-ship-spaces s direction x y)))
-    (if (every (lambda (p) (empty-space-at? b (car p) (cdr p))) ship-spaces)
-       (progn (setf (direction s) direction)
-              (loop for (space-x . space-y) in ship-spaces
-                    do (setf (contents (space-at b space-x space-y)) s)))
+    (if (every (lambda (p) (empty-space-at? b (car p) (cadr p))) ship-spaces)
+       (progn (setf (direction s) direction 
+                    (x s) x 
+                    (y s) y)
+              (loop for (space-x space-y) in ship-spaces
+                    do (let ((current-space (space-at b space-x space-y))) 
+                         (setf (contents current-space) s))))
        (position-ship s b))))
 
 (defun make-board (list-of-ships)

I didn't do a complete overhaul of the ship placement routines, but I got pretty close. I abstracted point creation a bit and replaced loop with a nested mapcan/mapcar on the results of the range utility function I mentioned last time. That's back by the way. mapcan doesn't cause any trouble here because range creates an entirely new list, which means that there are no outside references that might get tripped up by nconc. Also, the reason for nesting iterations in the assign-ship-spaces function is that I'm now generating ships wider than one square[7].

@@ -44,8 +61,21 @@
 ;;;;;;;;;;;;;;;;;;;; display
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 (defmethod echo ((b board) (p player))
-  (with-html-output (*standard-output* nil :indent t)
-    (:table :id "game-board"
-           (mapc (lambda (row) 
-                   (htm (:tr (mapc (lambda (s) (echo s p)) row)))) 
-                 (spaces b)))))

+  (html-to-stout
+    (:div :id "board-wrapper"
+         (loop for s in (ships p)
+               do (str (echo s p)))
+         (:table :id "game-board"
+                 (mapc (lambda (row) 
+                         (htm (:tr (mapc (lambda (s) (echo s p)) row)))) 
+                       (spaces b))))))
+
+(defmethod image-file ((s ship)) (format nil "/img/ships/~(~a~).png" (type-of s)))
+
+(defmethod echo ((s ship) (p player))
+  (let ((direction (direction s)))
+    (html-to-str (:img :class "ship" 
+                      :style (inline-css `(:left ,(css-left s) :top ,(px (board-scale (y s)))
+                                           :width ,(board-scale (len s)) :height ,(board-scale (wid s))
+                                           ,@(when (eq :vertical direction) (css-rotate 90))))
+                      :src (image-file s)))))

The rest of the additions here just deal with outputting the appropriate html and styles for ships and boards; that's the price you pay for choosing a more pleasing visual representation than just ascii characters in a table. There's enough ship-specific code at this point for me to consider pulling it out into its own file, but not really enough to actually force me to do it yet. Also, I get the feeling I should be referring to these as "barges" instead. In fact, that's two mental notes for after the write-up.

;;; css.lisp
(in-package #:strifebarge)

(defun css-space-size ()
    (let ((d (format nil "~apx" *board-square-size*)))
      `(:width ,d :height ,d)))

(defmacro css-transform-origin (x y)
  (let ((d (format nil "~a ~a" x y)))
    `'(:transform-origin ,d
       :-ms-transform-origin ,d
       :-webkit-transform-origin ,d
       :-moz-transform-origin ,d
       :-o-transform-origin ,d)))

(defun css-rotate (degrees)
  (let ((d (format nil "rotate(~adeg)" degrees)))
    `(:transform ,d
      :-ms-transform ,d
      :-webkit-transform ,d
      :-o-transform ,d
      :-moz-transform ,d)))

(defun css-scale (scale-factor)
  (let ((d (format nil "scale(~a,~a)" scale-factor scale-factor)))
    `(:transform ,d
      :-ms-transform ,d
      :-webkit-transform ,d
      :-o-transform ,d
      :-moz-transform ,d)))

(defun px (num) (format nil "~apx" num))

(defmethod css-left ((s ship))
  (px (if (eq :vertical (direction s))
          (board-scale (+ (x s) (wid s)))
          (board-scale (x s)))))

(compile-css "css/strifebarge.css"
             `((body :background-color \#000 :background-image "url(/img/galaxy.png)" :padding 0px :margin 0px :font-family sans-serif)

               (.menu-item :padding 10px :background-color \#eee :font-size large :font-weight bolder)
               ("#player-console .menu-item" :background-color transparent)

               (\#board-wrapper :position absolute)
               (\#game-board :border-spacing 0px :color \#fff)
               ("#game-board .miss" :font-family courier :font-size x-small)
               ("#game-board .hit" ,@(css-space-size))
               ("#game-board td" ,@(css-space-size) :padding 0px)
               ("#game-board .shot-link" :height 100% :width 100% :display block)
               ("#game-board .shot-link:hover" :background-position center :border none :background-image "url(/img/crosshairs/crosshair9.png)")

               (\#player-console :float right :width 110px :margin ,(px *board-square-size*) :padding 5px :background-color \#eee)
               (.ship-stats :margin-bottom 15px)
               (".ship-stats img" :width 60px :margin "5px 20px")
               (".ship-stats .total-hp" :height 25px :background-color red :width 100px :border "2px solid black")
               (".ship-stats .hp-remaining" :height 25px :background-color green :font-weight bold)

               (.ship ,@(css-transform-origin 0 0) :position absolute :z-index -10000)))

We'll dispense with the diff here because css.lisp is an entirely new file. This is basically just how I like to write CSS in Lisp projects. The library I'm using is cl-css; a lightweight, non-validating CSS generator of my own devising. Use it if you like, but remember that it does downcase everything[8] you pass it at the moment, so you may have a rough time using it with certain JS libraries that insist on using CamelCase or snakeCase rather than lisp-case for their DOM classes. As mentioned, the main reason I resorted to this can be seen in the css-scale, css-rotate and css-transform-origin macros, which just paper over the fact that browser developers don't quite seem to be cooperating yet.

diff --git a/game.lisp b/game.lisp
index 476baa5..c4c82e6 100644
--- a/game.lisp
+++ b/game.lisp
@@ -9,15 +9,48 @@
     p))
 
 (defun make-game (&rest players)
-  (let ((board (make-board (mapcan #'ships players))))
+  (let ((board (make-board (mapcan-f #'ships players))))
     (make-instance 'game :board board :players players :waiting-for players :turn-stack players)))

This is where that mapcan bug bit. As you can see, it's been replaced by mapcan-f which we've already gone over.

+;;;;;;;;;;;;;;;;;;;; predicates
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+(defmethod turn-p ((g game) &optional (player (session-value :player))) 
+  (eq (car (turn-stack g)) player))
 
 ;;;;;;;;;;;;;;;;;;;; display
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-(defmethod echo ((g game) (p player))
-  (with-html-output-to-string (*standard-output* nil :prologue t :indent t)
-    (:html (:body (echo (board g) p)))))
+(defmethod echo ((g game) (p player)) (echo (board g) p))
+
+(defmethod echo-console ((g game) (p player))
+  (html-to-stout (:div :id "player-console" 
+                      (:div :id "turn-marker" (str (if (turn-p *game*) "Your turn" "Their turn")))
+                      (echo-stats p)
+                      (:a :class "menu-item" :href "/quit-game" "Quit Game"))))
+
+(defmethod echo-stats ((p player))
+  (html-to-stout 
+    (:div :class "player-ships"
+         (loop for s in (ships p)
+               do (str (echo-stats s))))))
+
+(defmethod echo-stats ((s ship))
+  (html-to-str
+    (:div :id (instance-to-id s) :class "ship-stats" (:img :src (image-file s))
+         (htm (:div :class "total-hp" 
+                    (:div :class "hp-remaining" 
+                          :style (inline-css `(:width ,(format nil "~a%" (hp-% s))))
+                          (:span :class "num-hp" (str (remaining-hp s))) "/" (:span :class "num-total-hp" (str (hp s)))))))))
+
+(defmethod remaining-hp ((s ship))
+  (- (space-count s) (damage s)))
+
+(defmethod hp ((s ship)) (space-count s))
+
+(defmethod hp-% ((s ship))
+  (round (* 100 (/ (remaining-hp s) (space-count s)))))
+
+(defmethod to-json ((s ship))
+  (encode-json-to-string `((ship-id . ,(instance-to-id s)) (hp . ,(remaining-hp s)) (percent . ,(hp-% s)))))

Again, most of the additions here have to do with generating html/json representations of classes we've already gone over. The semi-interesting bits are the ship hp calculations (which would be good candidates for moving out into that ship-specific file I mentioned thinking about). instance-to-id is something we'll go over later because I'm unsure of its implementation. The point is to take an instance and return a string suitable for use as a CSS id while being ambiguous enough that a player can't get too much information from it[9].

@@ -27,9 +60,15 @@
       (setf (turn-stack g) (players g))))
 
 (defmethod fire ((g game) (p player) x y)
-  (let ((result (make-instance 
-                (if (empty-space-at? (board g) x y) 'miss 'hit)
-                :player p :x x :y y)))
-    (push result (history g))
-    (setf (move (space-at (board g) x y)) result)
+  (let* ((space (space-at (board g) x y))
+        (result (make-instance 
+                 (if (empty-space? space) 'miss 'hit)
+                 :player p :x x :y y)))
+    (push-record g "shot" (to-json result))
+    (unless (empty-space? space)
+      (let ((ship (contents space)))
+       (setf (ship result) ship)
+       (incf (damage ship))
+       (push-record g "ship-damage" (to-json ship))))
+    (setf (move space) result)
     result))

fire has changed pretty substantially. First off, it's now making calls to push-record (which we'll go over in the next file), and it's also applying damage to the stricken ship in addition to placing the shot marker on the board as before. The new stuff is all happening in that unless block.

;;; history-event.lisp
(in-package :strifebarge)

;;;;;;;;;;;;;;;;;;;; creation
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod to-json ((m move))
  (encode-json-to-string `((x . ,(x m)) (y . ,(y m)) 
                           (text . ,(echo m (session-value :player))))))

(defmethod push-record ((g game) event-type message)
  (push (make-instance 'history-event
                       :id (length (history g))
                       :event-type event-type
                       :message message)
        (history g)))

;;;;;;;;;;;;;;;;;;;; display
;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod emit-record ((g game) (p player))
  (apply #'concatenate 
         (cons 'string 
               (mapcar (lambda (r) (emit-record r p)) 
                       (reverse (take 10 (history g)))))))

(defmethod emit-record ((e history-event) (p player))
  (format nil "id: ~a~%event: ~a~%data: ~a~%~%"
          (id e) (event-type e) (message e)))

This is another new file. The interactions involving game history were getting complex enough that I wanted them on their own, but I ended up refactoring a lot of it out, leaving this rather sparse file. This stuff deals with the information we'll be passing back to the clients in-flight. To that end, I've defined history-event as its own class, and made sure that it handles echos properly. push-record is also a shortcut method you've already seen used. All of this could fit comfortable in the game file, and it probably will be moved before the next check-in.

;;; js.lisp
(in-package :strifebarge)

(compile-js "js/strifebarge.js" "strifebarge-js.lisp"
            (ps 
              (define-event-source source "update-map")

              (define-event-listener source "turn"
                (lambda (e) ($ "#turn-marker" (text (chain e data)))))

              (define-event-listener source "ship-damage"
                (lambda (e)
                  (let* ((d (parse-json (chain e data))) 
                         (ship-id (+ "#" (@ d "shipId"))))
                    ($ ship-id (find ".num-hp") (text (@ d "hp")))
                    ($ ship-id (find ".hp-remaining") (width (+ (@ d "percent") "%"))))))

              (define-event-listener source "shot"
                (lambda (e) 
                  (let ((d (parse-json (chain e data))))
                    ($-space-at ((@ d "x") (@ d "y")) (html (@ d "text"))))))

              (defun send-shot (x y)
                (post-to "/turn" 
                         (create :x x :y y) 
                         (lambda (data)
                           ($-space-at (x y) (html data))
                           ($ "#turn-marker" (text "Their Turn")))))))

Another entirely new file. This is what parenscript tends to look like if you use it with jQuery. Most of this deals with the server-sent-event stuff we need to send incremental, on-the-fly updates to the client. Particularly note the define-event-listeners and define-event-source. Neither are primitives, and we'll discuss implications in a moment. The only other thing we've got here is send-shot, which is actually an ajax call to /turn. It doesn't bother doing anything if it's not your turn (defined implicitly by the asserts we saw last time), but places the shot flag and updates the turn marker when needed.

;;; js-macros.lisp
(in-package :strifebarge)

(defun compile-js (file-name origin js)
  (with-open-file (stream file-name :direction :output :if-exists :supersede :if-does-not-exist :create)
    (format stream "//////////~%// This is a generated file. ~%// If you want to edit this javascript, tweak '~a' and re-evaluate it instead.~%//////////~%~%" origin)
    (format stream js)))

;;;;;;;;;;;;;;; basic shortcuts
(defpsmacro $ (selector &body chains)
  `(chain (j-query ,selector) ,@chains))

(defpsmacro fn (&body body) `(lambda () ,@body))

(defpsmacro doc-ready (&body body) 
  `($ document (ready (fn ,@body))))

(defpsmacro parse-json (target)
  `(chain j-query (parse-j-s-o-n ,target)))

(defpsmacro post-to (target-page data-hash on-success)
  "target-page is a page url.
data-hash is the data sent along as the post request; declared as (create :k v ...)
on-success is a function to run on a successful response; 
it should expect a single argument (the data returned by the target handler)"
  `(chain $ (post ,target-page
                  ,data-hash
                  ,on-success)))

;;;;;;;;;;;;;;; SSE specific
(defpsmacro define-event-source (name source-url)
  `(defvar ,name (new (-event-source ,source-url))))

(defpsmacro define-event-listener (event-source-name message-type on-receive)
  "event-source-name must be a defined event source. 
message-type is the event label sent by the server for this action (the default label is 'message').
on-receive is a function called when a satisfying message is received. It should take one argument (the event)."
  `(chain ,event-source-name (add-event-listener ,message-type ,on-receive false)))

;;;;;;;;;;;;;;; StrifeBarge specific
(defpsmacro $-space-at ((x y) &rest chains)
  `($ "#game-board tr" (eq ,y) (children "td") (eq ,x) ,@chains))

This one's... a bit complicated. I went a bit heavy on the comments for exactly that reason. compile-js is just responsible for generating js files from parenscript code. The next few macros are jQuery-oriented shortcuts that I've already discussed here[10]. The define pair let me specify SSE feed sources and event handlers much more simply than I could in vanilla JS. $-space-at just lets me shortcut the selection of a map square.

diff --git a/model.lisp b/model.lisp
index 0ee1685..f6bb48c 100644
--- a/model.lisp
+++ b/model.lisp
@@ -1,23 +1,32 @@
 (in-package :strifebarge)
 
 (defclass ship ()
-  ((space-count :reader space-count :initarg :space-count)
+  ((wid :reader wid :initarg :wid :initform 1)
+   (len :reader len :initarg :len)
+   (x :accessor x :initarg :x)
+   (y :accessor y :initarg :y)
+   (space-count :accessor space-count :initarg :space-count)
    (player :reader player :initarg :player)
    (damage :accessor damage :initform 0)
    (direction :accessor direction :initarg :direction)))
 
-(defclass carrier (ship) ((space-count :initform 5)))
-(defclass cruiser (ship) ((space-count :initform 3)))
-(defclass destroyer (ship) ((space-count :initform 2)))
+(define-ship carrier 5 2)
+(define-ship cruiser 3)
+(define-ship destroyer 2)
 
 (defclass move ()
   ((player :reader player :initarg :player)
    (x :reader x :initarg :x)
    (y :reader y :initarg :y)))
 
-(defclass hit (move) ())
+(defclass hit (move) ((ship :accessor ship :initarg :ship)))
 (defclass miss (move) ())
 
+(defclass history-event ()
+  ((id :reader id :initarg :id)
+   (event-type :reader event-type :initarg :event-type)
+   (message :reader message :initarg :message)))
+
 (defclass player ()
   ((score :accessor score :initform 0)
    (sunken :accessor sunken :initarg :sunken)

Back into merely modified files. As you can see, ship definitions got shorter (we'll see the macro behind that in the util file), history-events became their own explicitly defined objects, and ship got a few new slots to make wider pieces possible. Incidentally, if you look at those new ship slots, you'll see one of the CLOS speedbumps that Yegge whinged about quite a while ago. length is a polymorphic function and not a method. Meaning that if you have a non-sequence foo that would benefit from having a length method, you either need to name it foo-length or len or some other annoyingly minute variation of the word. I went with len and have no particular regrets there (other than not actually being able to use length as a method).

diff --git a/package.lisp b/package.lisp
index 94e808f..57ab4e9 100644
--- a/package.lisp
+++ b/package.lisp
@@ -1,8 +1,9 @@
 ;;;; package.lisp
 
 (defpackage #:strifebarge
-  (:use #:cl #:cl-who #:clsql #:hunchentoot #:parenscript)
-  (:import-from #:swank #:find-definition-for-thing)
+  (:use #:cl #:cl-who #:cl-css #:clsql #:hunchentoot #:parenscript)
+  (:import-from #:json #:encode-json-to-string #:decode-json-from-string)
+  (:import-from #:cl-ppcre #:scan-to-strings)
   (:import-from #:ironclad 
                #:encrypt-in-place #:decrypt-in-place #:make-cipher #:digest-sequence 
                #:octets-to-integer #:integer-to-octets
@@ -11,4 +12,8 @@
 
 (in-package #:strifebarge)
 
-(defparameter *web-server* (start (make-instance 'hunchentoot:easy-acceptor :port 5050)))
\ No newline at end of file
+;;;;;;;;;;;;;;;;;;;; config variable
+
+(defparameter *server-port* 5050)
+(defparameter *board-square-size* 35)
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

Not much new to see here. I've imported cl-css, as well as choice functions from cl-ppcre and :json. Still importing :ironclad chunklets, and still haven't put them to use. I did pull out the server port number and board square size into their own variables though. Thanks to my use of :parenscript and :cl-css, that single parameter should be all you need to change in order to mess with square size. The server startup, as well as a couple of related statements were moved out to a separate start file.

;;; start.lisp
(in-package :strifebarge)

(defparameter *web-server* (start (make-instance 'hunchentoot:easy-acceptor :port *server-port*)))
(web-folders "js" "img" "css")

And here it is. The defparameter line starts a new hunchentoot instance listening on *server-port*, and web-folders does exactly what you'd expect (we'll discuss it in very slightly more detail in the util file).

diff --git a/strifebarge.lisp b/strifebarge.lisp
index d651ba9..2630341 100644
--- a/strifebarge.lisp
+++ b/strifebarge.lisp
@@ -4,37 +4,57 @@
 
 (defparameter *game* nil)
 
+;;;;;;;;;;;;;;;;;;;; full handlers
+;;; (all of these either directly return, or redirect to complete pages)
+
 (define-easy-handler (index :uri "/") ()
-  (let ((players (list (make-player 'carrier 'cruiser 'destroyer)
-                      (make-player 'carrier 'cruiser 'destroyer))))
-    (echo (apply #'make-game players) (car players))))
+  (html-to-str
+    (:html (:head (:title "StrifeBarge")
+                 (:link :rel "stylesheet" :type "text/css" :href "/css/strifebarge.css"))
+          (:body (:a :class "menu-item" :href "/new-game" "New Game")
+                 (:a :class "menu-item" :href "/join-game" "Join Game")))))
 
 (define-easy-handler (new-game :uri "/new-game") (player-count)
   (let* ((p-count (if player-count (parse-integer player-count) 2)) 
-        (players (loop for i from 1 to p-count
-                       collect (make-player 'carrier 'cruiser 'destroyer))))
+        (players (loop repeat p-count collect (make-player 'carrier 'cruiser 'destroyer))))
     (setf *game* (apply #'make-game players))
     (redirect "/join-game")))
 
 (define-easy-handler (join-game :uri "/join-game") ()
-  (assert (and (not (null (waiting-for *game*)))
-              (null (session-value :player))))
+  (redirect-unless (and (not (null (waiting-for *game*)))
+                   (null (session-value :player))))
   (setf (session-value :player) (pop (waiting-for *game*)))
   (redirect "/show-game"))
 
 (define-easy-handler (show-game :uri "/show-game") ()
-  (assert (not (null (session-value :player))))
-  (echo *game* (session-value :player)))
+  (redirect-unless (not (null (session-value :player))))
+  (html-to-str
+    (:html (:head
+           (:title "StrifeBarge")
+           (:script :type "text/javascript" :src "/js/jquery-1.7.1.min.js")
+           (:script :type "text/javascript" :src "/js/strifebarge.js")
+           (:link :rel "stylesheet" :type "text/css" :href "/css/strifebarge.css"))
+          (:body (echo-console *game* (session-value :player)) 
+                 (echo *game* (session-value :player))))))
 
 (define-easy-handler (quit-game :uri "/quit-game") ()
-  (assert (not (null (session-value :player))))
+  (redirect-unless (not (null (session-value :player))))
   (push (waiting-for *game*) (session-value :player))
   (setf (session-value :player) nil)
   "You have quit the game")
 
+;;;;;;;;;;;;;;;;;;;; ajax handlers
+;;; these return either errors or partial json/html. 
+;;; The caller is expected to transform their output before final display
+
+(define-easy-handler (update-map :uri "/update-map") ()
+  (assert (not (null (session-value :player))))
+  (setf (header-out :cache-control) "no-cache"
+       (content-type*) "text/event-stream")
+  (emit-record *game* (session-value :player)))
+
 (define-easy-handler (turn :uri "/turn") (x y)
-  (assert (and (eq (car (turn-stack *game*)) (session-value :player))
-              (stringp x) (stringp y)))
+  (assert (and (turn-p *game*) (stringp x) (stringp y)))
   (advance-turn *game*)
-  (fire *game* (session-value :player) (parse-integer x) (parse-integer y))
-  (redirect "/show-game"))

+  (echo (fire *game* (session-value :player) (parse-integer x) (parse-integer y)) 
+       (session-value :player)))

This is possibly the most changed file at the moment. Firstly, we've added an ajax handler for player actions. Second, update-map shows you exactly what you need to do on the server to set up a Server-Sent-Event source that the client will actually support. emit-record was shown earlier, but I didn't really explain it. It still takes a player as an argument, because the record digest used to be subjective[11]. In the non-ajax handlers, note that the various asserts have all been replaced with equivalent redirect-unless calls. That's a new utility function that does exactly what you'd think. It may seem to be more complex than necessary at this point, but I will eventually shunt the player off into different pages depending on where they came from (you know, improving signage and all that).

diff --git a/util.lisp b/util.lisp
index 76f62b4..1de23d8 100644
--- a/util.lisp
+++ b/util.lisp
@@ -2,4 +2,44 @@
 
 (defun pick (a-list)
   "Randomly selects an element from the given list with equal probability."
-  (nth (random (length a-list)) a-list))
\ No newline at end of file
+  (nth (random (length a-list)) a-list))
+
+(defun range (a b)
+  (loop for i from a to b collect i))
+
+(defun board-scale (num) (* *board-square-size* num))
+
+(defun mapcan-f (fn a-list)
+  "Functional implementation of unary mapcan"
+  (loop for i in a-list append (funcall fn i)))
+
+(defmacro web-folders (&body body)
+  "Sets up folder dispatchers for the given folders"
+  `(progn ,@(mapcar #'(lambda (f) 
+                       `(push (create-folder-dispatcher-and-handler ,(format nil "/~a/" f) ,(format nil "~a/" f)) *dispatch-table*))
+                   body)))
+
+(defmacro redirect-unless (predicate &optional (target "/"))
+  `(unless ,predicate (redirect ,target)))
+
+(defmacro html-to-stout (&body body)
+  "Outputs HTML to standard out."
+  `(with-html-output (*standard-output* nil :indent t) ,@body))
+
+(defmacro html-to-str (&body body)
+  "Returns HTML as a string, as well as printing to standard-out"
+  `(with-html-output-to-string (*standard-output*) ,@body))
+
+(defun instance-to-id (instance)
+  (aref (nth-value 1 (scan-to-strings "{\(.*?\)}" (format nil "~a" instance))) 0))
+
+(defun take (num a-list)
+  (if (> (length a-list) num)
+      (subseq a-list 0 num)
+      a-list))
+
+(defmacro define-ship (name length &optional (width 1))
+  `(defclass ,name (ship) 
+     ((len :initform ,length)
+      (wid :initform ,width)
+      (space-count :initform ,(* length width)))))
\ No newline at end of file

Finally, util has grown quite a bit. Range has returned after being removed temporarily. We've already gone through mapcan-f and redirect-unless. take is a naive definition of the equivalent function from Scheme, define-ship is a minimal shortcut for cutting out ship definition boilerplate now that ships are two-dimensional, and the html-to- twins are the thinnest possible useful wrappers around the equivalent with-html-output from :cl-who. web-folders is a shortcut that makes it easier to have hunchentoot serve folders. Its operation should be obvious if you know how this normally works; the only note I'll make is that if you were to actually run StrifeBarge in production for some bizarre reason, you'd actually want to set up nginx or some other lightweight HTTP server out front to serve these static directories instead of having your Lisp process handle everything.

The only really interesting piece here is instance-to-id, which takes a CLOS instance and returns a sufficiently-ambiguous string mostly useful as a DOM id. I initially had this implemented as

(regex-replace-all "[#<>{} ]" (format nil "~a" instance))

instead, but found that a player can get too much information from that (though it did perform marginally better in profiling reports). Running it on a carrier would produce an id like CARRIER100AABD943, from which it's trivial to find out what kind of ship just got hit. The actual definition above instead returns 100AABD943 in the same situation. This is specific enough to unambiguously identify the DOM element for any JS function we need invoked, but it's general enough that a player probably can't get any useful, new information just by pulling down update-map and reading the output manually. I may change my mind on this later, by the by, since I'm already taking quite a few liberties with the rules, but it stays for now.

So that's almost that for the game itself. I still need to make ships send out a "You sunk my..." message when they get taken down, and there's a few usability-related things I'd like to add to the client side (ok, yes, and there needs to be support for multiple games at once, as well as actually winning), but most of the remaining development actually needs to happen outside the game itself. I hinted at it earlier, but just to make it explicit, I still need to implement

  • A chat-room/lobby for people to start games up from (in-game chat might be nice too)
  • A leaderboard system to show off high scores (possibly with replays too)
  • A formalized way of automating games

Sounds like this'll actually keep me busier than I expected.


Footnotes

1 - [back] - That'd be a meatspace, belligerent network of trust. The idea being that if you can get n people who certainly aren't cooperating to vouch for Alice face-to-face, your odds are better than if you merely had n of Alice's random online social contacts do the same.

2 - [back] - And don't really help you much in the general case, unless you're assuming that you'll always be able to trace a line from your direct contacts to the other party, or you're assuming belligerence.

3 - [back] - I have to be honest though; preventing people from cheating in a hobby-horse project of mine isn't why I was researching this. I'm doing some development at work that will need me to build at least reasonably secure components, so I'm beefing up on the basics of crypto and computer security.

4 - [back] - It also lets me use various GPL licensed sprites.

5 - [back] - I still need to implement multiple games per server, some sort of lobby/leaderboard system, and some way of actually winning before I think about implying that I've solved most game-related problems. Even for something as simple as this.

6 - [back] - I'm actually giving more and more thought to the literate programming idea for specifically this reason.

7 - [back] - There really isn't a technical reason for that; I took a look at the spaceship sprite sets at OpenGameArt and thought it would be a shame not to be able to use some of the oddly-shaped ones.

8 - [back] - No longer actually true; the version up at github currently lets you preserver case on selectors by passing them in as strings, and it incorporates the CSS3 transformation abstractions from this project. I'll probably be adding animation and transition to the pile shortly.

9 - [back] - Specifically, to stick to the classic game as closely as possible, it shouldn't be possible for a player to identify what kind of ship they hit until they either sink it, or deduce it from the number of squares it occupies.

10 - [back] - Though I did start calling the anonymous function shortcut fn rather than \ for ease of exporting.

11 - [back] - It's not at the moment, but this is something I won't be changing in the short term, since it will become true again once I implement a chat client.

Monday, February 20, 2012

StrifeBarge - Turn Based Web-games in Common Lisp

I've been in kind of a blah mood lately. To the point that I didn't really feel like struggling for huge new insights at all this weekend. Hopefully, that passes, because I really don't want to get mired in mediocrity any time soon. Anyway, you didn't come here to hear me being a whiny little bitch, so let me share the small insights I have had the energy to pursue.

First off, quickproject is fairly useful. It's missing some stuff I obsess over (I'm specifically thinking license boilerplate generators and an automatic git init+.gitignore call), and it does one or two small details in a way I don't like (mainly to do with the README file), but it still beats writing the package and asd file by hand. Next time I don't particularly feel like hunting down large insights, I'll probably fork this little utility and add the stuff I want. Between this and quicklisp, it's about high time I get a reasonably-sized pile of money together and send it to Zach, because that fucker earned it if anyone has.

With that out of the way, here's what I ended up using a chunk of my weekend to do. Unlike my previous piece on Hunchentoot development, this is meant to be less a lesson and more of an open code review by the invisible peanut gallery. Pot shots and patches are welcome. I've had this idea of putting together a turn-based web-game for a while now, and that's the sort of thing that doesn't really require any kind of deep learning. Just some straightforward thinking from first principles, and some light iteration. So, I whipped out quickproject and whipped up an 0.01. Lets start with the asd and package

;;;; strifebarge.asd

(asdf:defsystem #:strifebarge
  :serial t
  :depends-on (#:hunchentoot
               #:cl-who
               #:ironclad
               #:parenscript
               #:cl-css
               #:swank
               #:clsql)
  :components ((:file "package")
               (:file "util")
               (:file "model") (:file "space") (:file "board") (:file "game")
               (:file "strifebarge")))
;;;; package.lisp

(defpackage #:strifebarge
  (:use #:cl #:cl-who #:clsql #:hunchentoot #:parenscript)
  (:import-from #:swank #:find-definition-for-thing)
  (:import-from #:ironclad 
                #:encrypt-in-place #:decrypt-in-place #:make-cipher #:digest-sequence 
                #:octets-to-integer #:integer-to-octets
                #:ascii-string-to-byte-array #:byte-array-to-hex-string)
  (:shadow #:get-time))

(in-package #:strifebarge)

(defparameter *web-server* (start (make-instance 'hunchentoot:easy-acceptor :port 5050)))

And actually, now that I look at them, those clearly include things I haven't used, and may not for a while yet. I'll keep them around for the moment, but I'm leaving a mental note here that I really don't need anything past :hunchentoot and :cl-who just yet.

By the way, a significant chunk of this was quickproject-generated. I added the :import-from clauses, and some of the :file declarations, but that's pretty much it. The rest of it was created by running quickproject:make-project with the appropriate inputs. Moving right along, lets start with the meat of this thing

;;;; strifebarge.lisp

(in-package #:strifebarge)

(defparameter *game* nil)

(define-easy-handler (index :uri "/") ()
  (let ((players (list (make-player 'carrier 'cruiser 'destroyer)
                       (make-player 'carrier 'cruiser 'destroyer))))
    (echo (apply #'make-game players) (car players))))

(define-easy-handler (new-game :uri "/new-game") (player-count)
  (let* ((p-count (if player-count (parse-integer player-count) 2)) 
         (players (loop for i from 1 to p-count
                        collect (make-player 'carrier 'cruiser 'destroyer))))
    (setf *game* (apply #'make-game players))
    (redirect "/join-game")))

(define-easy-handler (join-game :uri "/join-game") ()
  (assert (and (not (null (waiting-for *game*)))
               (null (session-value :player))))
  (setf (session-value :player) (pop (waiting-for *game*)))
  (redirect "/show-game"))

(define-easy-handler (show-game :uri "/show-game") ()
  (assert (not (null (session-value :player))))
  (echo *game* (session-value :player)))

(define-easy-handler (quit-game :uri "/quit-game") ()
  (assert (not (null (session-value :player))))
  (push (waiting-for *game*) (session-value :player))
  (setf (session-value :player) nil)
  "You have quit the game")

(define-easy-handler (turn :uri "/turn") (x y)
  (assert (and (eq (car (turn-stack *game*)) (session-value :player))
               (stringp x) (stringp y)))
  (advance-turn *game*)
  (fire *game* (session-value :player) (parse-integer x) (parse-integer y))
  (redirect "/show-game"))

strifebarge contains all the HTTP handlers this project uses. I've implemented a test handler (index), which does nothing now that I'm past working up the echo methods. It's also possible to create a new-game, join or quit a game, show the current state of a game board, and take a turn[1].

I did mention that this was an 0.01, so the intense lack of usability should come as no surprise to you. Firstly, there is only one *game*, stored in the global variable of the same name. For the moment, if anyone starts a new game, the old one gets clobbered. Secondly, note that turn order is maintained through an error mechanism. In the final game, those should actually display a little note along the lines of "It's not your turn yet", rather than vomiting a stack dump[2].

Lets take a closer look at how the turn mechanism is approached. It actually starts at the join-game handler.

(define-easy-handler (join-game :uri "/join-game") ()
  (assert (and (not (null (waiting-for *game*)))
               (null (session-value :player))))
  (setf (session-value :player) (pop (waiting-for *game*)))
  (redirect "/show-game"))

The assert here makes sure of two things:

  • The game is waiting for at least one more player to join
  • You have not already joined a game

As noted, if an assert statement fails, you get an error. If they both succeed, you are assigned a player record, stored in your session, to track who you are for the duration of the game[3]. This is relevant in two ways. Firstly, the board is displayed differently based on which player is looking; we'll see more about this later, the only hint you get from this file is the call to echo in show-game.

(define-easy-handler (show-game :uri "/show-game") ()
  (assert (not (null (session-value :player))))
  (echo *game* (session-value :player)))

and secondly the player record in your session determines when it's your turn.

(define-easy-handler (turn :uri "/turn") (x y)
  (assert (and (eq (car (turn-stack *game*)) (session-value :player))
               (stringp x) (stringp y)))
  (advance-turn *game*)
  (fire *game* (session-value :player) (parse-integer x) (parse-integer y))
  (redirect "/show-game"))

Notice both that the assert in this handler makes sure that the top player on the turn-stack is the same as the player in your session, and that part of the handler body calls the method advance-turn on the current game before calling fire and re-displaying the game board. That segues nicely into

;;;; game.lisp

(in-package :strifebarge)

;;;;;;;;;;;;;;;;;;;; game creation and setup
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun make-player (&rest ship-types)
  (let ((p (make-instance 'player)))
    (setf (ships p)
          (mapcar (lambda (s) (make-instance s :player p)) ship-types))
    p))

(defun make-game (&rest players)
  (let ((board (make-board (mapcan #'ships players))))
    (make-instance 'game :board board 
                         :players players 
                         :waiting-for players 
                         :turn-stack players)))

;;;;;;;;;;;;;;;;;;;; display
;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod echo ((g game) (p player))
  (with-html-output-to-string (*standard-output* nil :prologue t :indent t)
    (:html (:body (echo (board g) p)))))

;;;;;;;;;;;;;;;;;;;; actions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod advance-turn ((g game))
  (if (cdr (turn-stack g))
      (pop (turn-stack g))
      (setf (turn-stack g) (players g))))

(defmethod fire ((g game) (p player) x y)
  (let ((result (make-instance 
                 (if (empty-space-at? (board g) x y) 'miss 'hit)
                 :player p :x x :y y)))
    (push result (history g))
    (setf (move (space-at (board g) x y)) result)
    result))

The creation and setup functions give you a pretty good idea of how players and games are represented. For now, a player is just an object that has one or more ships[4]. A game is a slightly more complex construct; it has a board as well as a collection of players, a turn-stack and list of players that haven't shown up yet[5]. We'll discuss the board a bit later, lets get into how players and the game function. Um. I mean: function.

For the moment, echoing a game just passes the buck to echoing a board for the current player. There will eventually be things other than the board, such as various stat displays, and a turn counter. The interesting stuff here is advance-turn and fire.

(defmethod advance-turn ((g game))
  (if (cdr (turn-stack g))
      (pop (turn-stack g))
      (setf (turn-stack g) (players g))))

After reading this, it should be perfectly obvious what the turn stack is, and how it enforces actions. It just starts off as a copy of the list of players participating in the game, and we pop the top record off each time a turn is passed. Once we get down to the last player in the stack, we copy out the list of players instead of poping again. That keeps the game circular.

(defmethod fire ((g game) (p player) x y)
  (let ((result (make-instance 
                 (if (empty-space-at? (board g) x y) 'miss 'hit)
                 :player p :x x :y y)))
    (push result (history g))
    (setf (move (space-at (board g) x y)) result)
    result))

fire makes a new hit or miss marker[6] and attaches it to the space ... I mean, space... at the given coordinates. It also records the move in the games history.

Again, 0.01, so neither of these functions actually deal damage to a given ship, or end the game if a player has been eliminated. The turn sequence just goes on until all the players stop playing. Note one very intentional effect of this architecture though; the game supports n players by default. It's not a two-player affair, but rather, as many as you like[7], as hinted at by the new-game handler[8].

Before we deal with the space and board files, we should probably take a look at the model. There are some non-obvious interactions, and I want to lay them bare before getting into how I put together the actual front end and hit tracking.

;;;; model.lisp

(in-package :strifebarge)

(defclass ship ()
  ((space-count :reader space-count :initarg :space-count)
   (player :reader player :initarg :player)
   (damage :accessor damage :initform 0)
   (coords :accessor coords :initarg :coords)
   (direction :accessor direction :initarg :direction)))

(defclass carrier (ship) ((space-count :initform 5)))
(defclass cruiser (ship) ((space-count :initform 3)))
(defclass destroyer (ship) ((space-count :initform 2)))

(defclass move ()
  ((player :reader player :initarg :player)
   (x :reader x :initarg :x)
   (y :reader y :initarg :y)))

(defclass hit (move) ())
(defclass miss (move) ())

(defclass player ()
  ((score :accessor score :initform 0)
   (sunken :accessor sunken :initarg :sunken)
   (ships :accessor ships :initarg :ships)))

(defclass board-space ()
  ((x :reader x :initarg :x)
   (y :reader y :initarg :y)
   (contents :accessor contents :initform nil)
   (move :accessor move :initform nil)))

(defclass board ()
  ((width :reader width :initarg :width)
   (height :reader height :initarg :height)
   (spaces :accessor spaces :initarg :spaces)))

(defclass game ()
  ((board :accessor board :initarg :board)
   (players :accessor players :initarg :players)
   (waiting-for :accessor waiting-for :initarg :waiting-for)
   (turn-stack :accessor turn-stack :initarg :turn-stack)
   (history :accessor history :initform nil)))

You probably inferred the shape of the game, player and move classes based on stuff I've already shown you. The reason that move, hit and miss are implemented like this is twofold. First, it makes echoing simple[9], and second, it will eventually let me do clever things like color-coding shot markers per player.

The new stuff here is the ship and associated classes. I've only implemented 3; a 5-space, a 3-space and a 2-space vessel, each of which just inherits from ship and sets its space-count. As you can see, they're already prepared to take damage, in addition to tracking their position, orientation and owner. Now that I really think about it, I'm not sure why I have a ship track its coordinates after being placed; it becomes completely irrelevant to the ship at that point. The space-count matters[10], but it makes no difference what specific spaces a given ship occupies and won't for a rather long while. That's definitely something I'll be removing after I finish this write-up.

The other new bits, which may help understand the rest of the files so I'll dwell on them a moment, are the board and space classes.

(defclass board-space ()
  ((x :reader x :initarg :x)
   (y :reader y :initarg :y)
   (contents :accessor contents :initform nil)
   (move :accessor move :initform nil)))

A board-space has an x and y coordinate, as well as initially empty contents and move slots. You already saw what move does; when a space is fired upon, it's marked as either a hit or a miss using a shot flag[11]. The contents are exactly what you'd expect; each occupied space carries a pointer to the ship it contains.

(defclass board ()
  ((width :reader width :initarg :width)
   (height :reader height :initarg :height)
   (spaces :accessor spaces :initarg :spaces)))

Last one, and then we can round out the methods. A board caches its width and height, as well as keeping the full spaces grid. What a grid looks like is non-obvious from just the class declaration, so this is actually the perfect segue into

;;;; board.lisp

(in-package :strifebarge)

;;;;;;;;;;;;;;;;;;;; board creation
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun empty-grid (width height)
  (loop for y from 0 to height
        collect (loop for x from 0 to width collect (make-space x y))))

(defun empty-board (width height)
  (make-instance 'board 
                 :spaces (empty-grid width height)
                 :width width
                 :height height))

;;;;;;;;;;;;;;;;;;;; board setup
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod space-at ((b board) x y) (nth x (nth y (spaces b))))

(defmethod assign-ship-spaces ((s ship) direction x y)
  (loop for i from 0 to (- (space-count s) 1)
        if (eq :vertical direction)
          collect (cons x (+ i y))
        else
          collect (cons (+ i x) y)))

(defmethod position-ship ((s ship) (b board))
  (let* ((x (random (- (width b) (space-count s))))
         (y (random (- (height b) (space-count s))))
         (direction (pick '(:vertical :horizontal)))
         (ship-spaces (assign-ship-spaces s direction x y)))
    (if (every (lambda (p) (empty-space-at? b (car p) (cdr p))) ship-spaces)
        (progn 
          (setf (coords s) ship-spaces
                (direction s) direction)
          (loop for (x . y) in ship-spaces
                do (setf (contents (space-at b x y)) s)))
        (position-ship s b))))

(defun make-board (list-of-ships)
  (let* ((width (+ 5 (* 2 (length list-of-ships))))
         (height (+ 5 (* 2 (length list-of-ships))))
         (board (empty-board width height)))
    (dolist (s list-of-ships) (position-ship s board))
    board))

;;;;;;;;;;;;;;;;;;;; display
;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod echo ((b board) (p player))
  (with-html-output (*standard-output* nil :indent t)
    (:table :id "game-board"
            (mapc (lambda (row) 
                    (htm (:tr (mapc (lambda (s) (echo s p)) row)))) 
                  (spaces b)))))

As you can see, that's the chunkiest single file in the package, and that's because it implements creating a board as well as placing ships[12]. Firstly, looking at empty-board and space-at should clear up what a board looks like. It's a list of lists of spaces[13].

The ship placement methods are worth a slightly closer look

(defmethod assign-ship-spaces ((s ship) direction x y)
  (loop for i from 0 to (- (space-count s) 1)
        if (eq :vertical direction)
          collect (cons x (+ i y))
        else
          collect (cons (+ i x) y)))

(defmethod position-ship ((s ship) (b board))
  (let* ((x (random (- (width b) (space-count s))))
         (y (random (- (height b) (space-count s))))
         (direction (pick '(:vertical :horizontal)))
         (ship-spaces (assign-ship-spaces s direction x y)))
    (if (every (lambda (p) (empty-space-at? b (car p) (cdr p))) ship-spaces)
        (progn 
          (setf (coords s) ship-spaces
                (direction s) direction)
          (loop for (space-x . space-y) in ship-spaces
                do (setf (contents (space-at b space-x space-y)) s)))
        (position-ship s b))))

The position-ship method takes a ship and a board and positions the ship on the board. It does this by randomly picking a starting x/y coordinate and direction. Those are fed into assign-ship-spaces which returns a list of (x . y) corresponding to the spaces this ship will take up[14]. Once we have that, we check whether all of the generated spaces are currently empty, and if they aren't[15], we try again. If the given spaces are clear, we[16] store those spaces in the ships' coords and the direction in direction[17] before assigning ship pointers to the appropriate spaces on the board. Tadaah! That was the most complicated piece of this game.

make-board is fairly self-explanatory; it takes a list of ships and determines width/height of the map based on how many there are, then places each ship and returns the resulting board instance. The boards' echo method should make perfect sense now that you've seen what a board is; in order to echo one, we start an HTML table and map echo over each space in each row of the board. Before we look at space, lets just zoom in on one part of position-ship. Specifically, the part that reads

...
(direction (pick '(:vertical :horizontal)))
...

pick isn't actually a Lisp primitive, but it's fairly simple to define. Here's

;;;; util.lisp

(in-package :strifebarge)

(defun pick (a-list)
  "Randomly selects an element from the given list with equal probability."
  (nth (random (length a-list)) a-list))

(defun range (a b)
  "Returns a list of numbers starting with a and ending with b inclusive."
  (loop for i from a to b collect i))

Both are fairly self-explanatory. range is a second utility function I defined for an earlier iteration of the codebase, but ended up refactoring out all calls to it. I'm still keeping it, probably more out of superstition than anything else. In fact, never mind, I'm adding one to the list of things I need to trim once I finish writing this.

Ok, all that out of the way, lets finally take a look at

;;;; space.lisp

(in-package :strifebarge)

;;;;;;;;;;;;;;;;;;;; creation and setup
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun make-space (x y) 
  (make-instance 'board-space :x x :y y))

;;;;;;;;;;;;;;;;;;;; predicates
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod empty-space? ((s board-space)) (null (contents s)))
(defmethod empty-space-at? ((b board) x y) (null (contents (space-at b x y))))

;;;;;;;;;;;;;;;;;;;; display
;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod echo ((s board-space) (p player))
  (with-html-output (*standard-output* nil :indent t)
    (:td (cond ((move s) (echo (move s) p))
               ((and (contents s) (eq (player (contents s)) p)) (str "#"))
               (t (htm (:a :href (format nil "/turn?x=~a&y=~a" (x s) (y s)) "~")))))))

(defmethod echo ((m hit) (p player))
  (with-html-output (*standard-output* nil :indent t)
    "X"))

(defmethod echo ((m miss) (p player))
  (with-html-output (*standard-output* nil :indent t)
    "O"))

I told you the hard part concluded with make-board earlier. make-space is a self-explanatory shortcut to using the raw make-instance[18]. The empty predicates are shorthand for checking whether a given space (or a space given by specified coordinates on a board) is empty.

The last mystery is solved with the echo methods here. A space is echoed as a td tag, but its contents depends on certain properties of the space. First, if this space has been fired upon, we echo its shot marker[19]. Second, if the space hasn't been fired upon, but contains a ship belonging to the current player, we echo a marker for a ship[20]. Finally, if all else fails, we output a shot link with the coordinates of the current space, and wrap it around "~" which looks sufficiently wave-like for this stage of development.

As an architectural aside, that last one is why we needed the more complex representation of spaces. I initially toyed with just having a simple 2-dimensional list of '([move] [contents]), but that would have been both more difficult to abstract from other parts of the program[21], and it would have complicated emitting the coordinate link to /turn.

So there, putting it all together, we've got a very simple[22] implementation of an HTTP-using multiplayer, turn-based, guessing/strategy game in Common Lisp with a grand total of 220 lines including comments[23]. Hopefully this step-by step has been useful to someone. If nothing else, it helped me figure out where I'm going next in a much more concrete way. I need to trim a few things, add some re-direction constructs to use in place of the assertions, get cracking on a sprite-set[24], and figure out a good way to periodically notify clients about new developments in the game[25]. That's it for the short term, once that's all done, I'll do another one of these little reflection/code-review articles.

If you feel like poking around the codebase for your own education, or for the purposes of patching, check out the github repo. I haven't actually decided what license I'm using yet, so maybe hold off on hacking on it until I get that cleared up.[26]


Footnotes

1 - [back] - Which fires a single shot on the specified space and passes the turn.

2 - [back] - Which is what failed assertions do.

3 - [back] - Incidentally, this is why I wanted to include :ironclad right out of the gate; as far as I know, Hunchentoot sessions aren't particularly spoof-resistant, so in a real game I'd want better player verification than this approach gives me. I'm assuming the final solution will take the form of IP and user-agent recording combined with a Diffie-Hellman handshake.

4 - [back] - It also has some other tracking slots, like score and how many ships they sank, but those don't get tracked quite yet.

5 - [back] - That'd be waiting-for.

6 - [back] - Depending on whether the space being fired into is empty or not, obviously.

7 - [back] - Though I probably should have the option of limiting the count through a config variable somewhere in the final.

8 - [back] - Which actually takes player-count as an input, and defaults to 2.

9 - [back] - As you'll see when we get to the space file.

10 - [back] - Or rather, will matter, once I start tracking ship damage.

11 - [back] - An instance of the move class.

12 - [back] - Which is only non-trivial because we're breaking tradition by placing all ships on the same board, necessitating both random placing and preventing ship collisions.

13 - [back] - Subject to change to arrays in the final, but I can't be bothered to optimize at this point. On the upside, defining space-at explicitly means that when I change the representation of a board, I'll only have to change that single function and the empty- functions rather than tracking down every call to nth.

14 - [back] - Taking into account this particular ships' length.

15 - [back] - Which would signal a collision with another ship.

16 - [back] - Uselessly

17 - [back] - The direction will actually be useful sooner rather than later; it will help figure out how to render a ship once I start using sprites instead of the plain grid display going on currently.

18 - [back] - This technique both saves some typing, and lets you be flexible about re-defining the representation of the object in question later. In this case, I could completely change how the game thinks of board-spaces, and all I'd really need to change is the code in this file.

19 - [back] - Currently, a hit is represented as "X" and a miss is "O".

20 - [back] - The current representation is "#" for all ships, this will eventually get complicated enough to call for another echo method specializing on ship, but that can wait until I actually get some graphics up ins.

21 - [back] - In the sense that changing a particular spaces' move or contents would have necessitated at least a little grubbing around with car and cdr.

22 - [back] - And still unplayable.

23 - [back] - And that's even before the cuts trims I said I'd make.

24 - [back] - Or try to find one.

25 - [back] - My intuition tells me that long-poll/comet won't be a very good fit for Hunchentoot's view of the world, so I'll need to figure something out.

26 - [back] - It'll definitely be an open one, I'm just not sure which, though currently leaning towards the AGPL since the point of this is a hobby-horse/educational project. In other words, definitely hold off if you're a GNU hater.