04d792fae37f

More documentation
[view raw] [browse files]
author Steve Losh <steve@stevelosh.com>
date Wed, 23 Mar 2016 21:36:38 +0000
parents ce6aa58d27da
children e854dc46c211
branches/tags (none)
files Makefile docs/01-installation.markdown docs/02-overview.markdown docs/03-reference.markdown docs/04-changelog.markdown docs/api.lisp package.lisp src/example.lisp src/ggp.lisp

Changes

--- a/Makefile	Wed Mar 23 17:37:50 2016 +0000
+++ b/Makefile	Wed Mar 23 21:36:38 2016 +0000
@@ -7,7 +7,7 @@
 # src/utils.lisp: src/make-utilities.lisp
 # 	cd src && sbcl --noinform --load make-utilities.lisp  --eval '(quit)'
 
-$(apidoc): $(sourcefiles) docs/api.lisp
+$(apidoc): $(sourcefiles) docs/api.lisp package.lisp
 	sbcl --noinform --load docs/api.lisp  --eval '(quit)'
 
 docs: docs/build/index.html
--- a/docs/01-installation.markdown	Wed Mar 23 17:37:50 2016 +0000
+++ b/docs/01-installation.markdown	Wed Mar 23 21:36:38 2016 +0000
@@ -1,2 +1,8 @@
 Installation
 ============
+
+`cl-ggp` is compatible with Quicklisp, but not *in* Quicklisp (yet?).  You can
+clone the repository into your [Quicklisp local-projects][local] directory for
+now.
+
+[local]: https://www.quicklisp.org/beta/faq.html#local-project
--- a/docs/02-overview.markdown	Wed Mar 23 17:37:50 2016 +0000
+++ b/docs/02-overview.markdown	Wed Mar 23 21:36:38 2016 +0000
@@ -4,6 +4,9 @@
 `cl-ggp` handles the GGP protocol for you.  Players are implemented as CLOS
 objects.
 
+This document assumes you know what General Game Playing is, what GDL is, and
+how the GGP community/competitions/etc work.
+
 [TOC]
 
 Basics
@@ -12,6 +15,7 @@
 You can create your own player by extending the `ggp-player` class, creating an
 object, and calling `start-player` on it to fire it up:
 
+    :::lisp
     (defclass simple-player (ggp:ggp-player)
       ())
 
@@ -21,8 +25,8 @@
 
     (ggp:start-player *player*)
 
-`ggp-player` takes `:name` and `:port` initargs.  It has a few other internal
-slots you shouldn't mess with.
+`ggp-player` takes `:name` and `:port` initargs, which do what you think they
+do.  It has a few other internal slots you shouldn't mess with.
 
 You can kill a player with `kill-player`.
 
@@ -38,7 +42,8 @@
 
 ### player-start-game
 
-    (defmethod player-start-game ((player YOUR-PLAYER) rules role start-clock play-clock)
+    :::lisp
+    (defmethod player-start-game ((player YOUR-PLAYER) rules role timeout)
       ...)
 
 This is called when a new game starts.
@@ -48,36 +53,161 @@
 
 `role` is a symbol representing which role you've been assigned.
 
-`start-clock` is 
-
-`play-clock` is 
+`timeout` is the timestamp that the response to the server is due by, in
+internal-real time units (more on this later).
 
 ### player-update-game
 
+    :::lisp
     (defmethod player-update-game ((player YOUR-PLAYER) moves)
       ...)
 
-This is called once per turn, to update the game state with the moves each
-player selected.
+This is called once per turn, to allow you to update the game state with the
+moves each player selected.
 
-`moves` is a list of the moves made by all players.
+`moves` will be an association list of `(role . move)` conses representing the
+moves made by each player last turn.
 
 ### player-select-move
 
-    (defmethod player-select-move ((player YOUR-PLAYER))
+    :::lisp
+    (defmethod player-select-move ((player YOUR-PLAYER) timeout)
       ...)
 
-This is called once per turn.  It should return the move your player wants to
+This is called once per turn.  It needs to return the move your player wants to
 do.  All players **must** implement this function.
 
+`timeout` is the timestamp that the response to the server is due by, in
+internal-real time units (more on this later).
+
 ### player-stop-game
 
+    :::lisp
     (defmethod player-stop-game ((player YOUR-PLAYER))
       ...)
 
 This is called when the game is stopped.  You can use it for things like tearing
 down any extra data structures you've made, suggesting a GC to your Lisp, etc.
 
+Timeouts
+--------
+
+The GGP protocol specifies time limits for players.
+
+When the initial game description is sent, players have a limited amount of time
+for "metagaming" where they might process the rules, build alternate
+representations (e.g. a propnet), start searching the game's DAG, etc.
+
+Once the initial "metagaming" phase is over, the players must each choose a move
+in every round, and there is a time limit on how long it takes them to respond.
+
+`cl-ggp` mostly handles the annoying work of calculating the time your methods
+have available for work, but there are a few caveats.
+
+First: the `timestamp` arguments your methods get are timestamps of
+internal-real time.  If you're not familiar with how interal time works in
+Common Lisp, you should fix that.  Read up on [get-internal-real-time][] and
+[internal-time-units-per-second][].
+
+So you need to finish responding to the request by the internal-real timestamp
+given.  This brings us to the second caveat: "finishing responding" includes
+returning back up the call stack and sending the HTTP response back to the game
+server.  It's probably wise to bake a bit of breathing room into your player and
+not use *all* the given time in `timeout`, but `cl-ggp` doesn't try to decide
+how much time to reserve.  You should decide that based on things like:
+
+* Your ping to the GGP server.
+* How likely it is for your Lisp process to get descheduled by your OS, and how
+  long it might take to start running again.
+* Worst-case duration of a GC pause right before sending the response.
+* How brave you're feeling today.
+
+In a nutshell: when `(get-internal-real-time)` returns the number given to you
+in `timeout`, your message better have already reached the server.
+
+[get-internal-real-time]: http://www.lispworks.com/documentation/HyperSpec/Body/f_get_in.htm#get-internal-real-time
+[internal-time-units-per-second]: http://www.lispworks.com/documentation/HyperSpec/Body/v_intern.htm#internal-time-units-per-second
+
+Symbols
+-------
+
+The other tricky part about `cl-ggp` is how it handles symbols.
+
+Game descriptions are written in GDL, a fragment of which might look like this:
+
+    (role x)
+    (role o)
+    (init (control x))
+
+    (<= (legal ?role (mark ?row ?col ?role))
+      (control ?role)
+      (is-blank ?row ?col))
+
+This is obviously pretty close to Lisp &mdash; it's just a bunch of lists of
+symbols &mdash; so reading it in is almost trivial.  The main question is which
+package the symbols get interned into.
+
+`cl-ggp` interns all GDL symbols into a separate package called `GGP-RULES` to
+prevent polluting other packages.  It also clears this package between matches
+(except for a few special symbols that survive the clearing) to prevent building
+up mountains of garbage symbols from building up over time, especially when GDL
+scrambing is enabled on the server.
+
+This means that when your player's methods get symbols in their input (i.e. in
+the `rules`, `role`, and `moves` arguments) those symbols will be interned in
+`GGP-RULES`.  When your player returns a move to make from `player-select-move`,
+any symbols inside it must be interned in `GGP-RULES` for things to work
+correctly.
+
+This is kind of shitty, and the author is aware of that.  Suggestions for less
+shitty alternatives that still feel vaguely lispy are welcome.
+
 Example Player
 --------------
 
+`cl-ggp` is pretty bare bones, so it's tough to show an example player on its
+own without bringing in a logic library.  But we can at least sketch out
+a stupid little player that just returns the same move all the time, regardless
+of whether it's valid or not, just to show the end-to-end process of creating
+a player.
+
+First we'll define the player class and implement the required
+`player-select-move` method for it:
+
+    :::lisp
+    (defclass simple-player (ggp:ggp-player)
+      ())
+
+    (defmethod ggp:player-select-move ((player simple-player) timeout)
+      'ggp-rules::wait)
+
+Our player doesn't store any state of its own, so it doesn't need any extra
+slots.  Notice how `player-select-move` returns a symbol from the `GGP-RULES`
+package as discussed above.
+
+The move our stupid player always returns is `WAIT`.  If the game supports that
+move we'll make it every time, otherwise the game server will reject it as
+invalid and just choose a random move for us.
+
+Now we can actually create a player:
+
+    :::lisp
+    (defvar *player*
+      (make-instance 'simple-player
+                     :name "SimplePlayer"
+                     :port 5000))
+
+And fire it up:
+
+    :::lisp
+    (ggp:start-player *player*)
+
+Now we can play a few games with it.  We'll probably lose every time unless
+we're playing an unscrambled game of [Don't Press the Button][dptb].
+
+Once we're done we can kill it to free up the port:
+
+    :::lisp
+    (ggp:kill-player *player*)
+
+[dptb]: https://bitbucket.org/snippets/sjl/erRjL
--- a/docs/03-reference.markdown	Wed Mar 23 17:37:50 2016 +0000
+++ b/docs/03-reference.markdown	Wed Mar 23 21:36:38 2016 +0000
@@ -12,6 +12,8 @@
 
 ## Package GGP
 
+The main GGP package.
+
 ### GGP-PLAYER (class)
 
 The base class for a GGP player.  Custom players should extend this.
@@ -32,6 +34,35 @@
 
 The port the HTTP server should listen on.
 
+#### Slot MATCH-ROLES
+
+* Allocation: INSTANCE
+* Type: `(OR NULL LIST)`
+* Reader: `PLAYER-MATCH-ROLES`
+
+A list of the roles for the current match.  Feel free to read and use this if you like.  **Do not modify this.**
+
+#### Slot START-CLOCK
+
+* Allocation: INSTANCE
+* Type: `(OR NULL (INTEGER 1))`
+
+The start clock for the current game.  **Do not touch this.**  Use the `timeout` value passed to your methods instead.
+
+#### Slot PLAY-CLOCK
+
+* Allocation: INSTANCE
+* Type: `(OR NULL (INTEGER 1))`
+
+The play clock for the current game.  **Do not touch this.**  Use the `timeout` value passed to your methods instead.
+
+#### Slot MESSAGE-START
+
+* Allocation: INSTANCE
+* Type: `(OR NULL (INTEGER 0))`
+
+The (internal-real) timestamp of when the current GGP message was received.  **Do not touch this.**  Use the `timeout` value passed to your methods instead.
+
 #### Slot CURRENT-MATCH
 
 * Allocation: INSTANCE
@@ -57,24 +88,35 @@
 
 ### PLAYER-SELECT-MOVE (generic function)
 
-    (PLAYER-SELECT-MOVE PLAYER)
+    (PLAYER-SELECT-MOVE PLAYER TIMEOUT)
 
 Called when it's time for the player to select a move to play.
 
   Must return a list/symbol of the GDL move to play.  Note that any symbols in
-  the move should be ones that are interned in the `GGP` package.  The author is
-  aware that this sucks and welcomes suggestions on how to make it less awful.
+  the move should be ones that are interned in the `GGP-RULES` package.  The
+  author is aware that this sucks and welcomes suggestions on how to make it
+  less awful.
+
+  `timeout` is the timestamp that the response to the server is due by, in
+  internal-real time units.  Basically: when `(get-internal-real-time)` returns
+  this number, your message better have reached the server.
 
   
 
 ### PLAYER-START-GAME (generic function)
 
-    (PLAYER-START-GAME PLAYER RULES ROLE START-CLOCK PLAY-CLOCK)
+    (PLAYER-START-GAME PLAYER RULES ROLE TIMEOUT)
 
 Called when the game is started.
 
   `rules` is a list of lists/symbols representing the GDL description of the
-  game.  Note that all symbols are interned in the `GGP` package.
+  game.  Note that all symbols are interned in the `GGP-RULES` package.
+
+  `role` is a symbol representing the role of the player in this game.
+
+  `timeout` is the timestamp that the response to the server is due by, in
+  internal-real time units.  Basically: when `(get-internal-real-time)` returns
+  this number, your message better have reached the server.
 
   
 
@@ -93,9 +135,10 @@
 
     (PLAYER-UPDATE-GAME PLAYER MOVES)
 
-Called after all player have made their moves.
+Called after all players have made their moves.
 
-  `moves` will be a list of moves made by the players.
+  `moves` will be a list of `(role . move)` conses representing moves made by
+  each player last turn.
 
   
 
@@ -105,3 +148,17 @@
 
 Start the HTTP server for the given player.
 
+## Package GGP-RULES
+
+Symbol storage package.
+
+  The GGP-RULES package is used to hold all the symbols in the GDL game
+  descriptions, as well as some special symbols in the GGP protocol.  It is
+  cleared between game runs to avoid a buildup of garbage symbols (especially
+  when GDL scrambling is turned on), though certain special symbols are allowed
+  to survive the clearing.
+
+  This is ugly.  I'm sorry.  I'm open to suggestions on better ways to do this.
+
+  
+
--- a/docs/04-changelog.markdown	Wed Mar 23 17:37:50 2016 +0000
+++ b/docs/04-changelog.markdown	Wed Mar 23 21:36:38 2016 +0000
@@ -1,2 +1,12 @@
 Changelog
 =========
+
+Here's the list of changes in each released version.
+
+[TOC]
+
+v0.0.1
+------
+
+Initial alpha version.  Things are going to break a lot.  Don't use this.
+
--- a/docs/api.lisp	Wed Mar 23 17:37:50 2016 +0000
+++ b/docs/api.lisp	Wed Mar 23 21:36:38 2016 +0000
@@ -1,7 +1,7 @@
 (ql:quickload "cl-d-api")
 
 (defparameter *document-packages*
-  (list "GGP"))
+  (list "GGP" "GGP-RULES"))
 
 (defparameter *output-path*
   #p"docs/03-reference.markdown" )
--- a/package.lisp	Wed Mar 23 17:37:50 2016 +0000
+++ b/package.lisp	Wed Mar 23 21:36:38 2016 +0000
@@ -13,6 +13,24 @@
     :player-port
 
     :start-player
-    :kill-player
-    ))
+    :kill-player)
+  (:documentation "The main GGP package.")
+  )
+
+(defpackage #:ggp-rules
+  (:use #:cl)
+  (:import-from #:cl #:nil) ; fuckin lol
+  (:documentation
+   "Symbol storage package.
 
+  The GGP-RULES package is used to hold all the symbols in the GDL game
+  descriptions, as well as some special symbols in the GGP protocol.  It is
+  cleared between game runs to avoid a buildup of garbage symbols (especially
+  when GDL scrambling is turned on), though certain special symbols are allowed
+  to survive the clearing.
+
+  This is ugly.  I'm sorry.  I'm open to suggestions on better ways to do this.
+
+  "))
+
+
--- a/src/example.lisp	Wed Mar 23 17:37:50 2016 +0000
+++ b/src/example.lisp	Wed Mar 23 21:36:38 2016 +0000
@@ -3,8 +3,10 @@
 (defclass simple-player (ggp:ggp-player)
   ())
 
-(defmethod ggp:player-select-move ((player simple-player))
-  'wait)
+(defmethod ggp:player-select-move ((player simple-player) timeout)
+  (format t "Selecting move (timeout ~A)~%" timeout)
+  'ggp-rules::wait)
+
 
 (defvar *player* nil)
 
@@ -16,4 +18,3 @@
 (ggp:start-player *player*)
 (ggp:kill-player *player*)
 
-(setf (slot-value *player* 'ggp::current-match) nil)
--- a/src/ggp.lisp	Wed Mar 23 17:37:50 2016 +0000
+++ b/src/ggp.lisp	Wed Mar 23 21:36:38 2016 +0000
@@ -4,8 +4,29 @@
 (defparameter *debug*
   t)
 
-(defparameter *ggp-package*
-  (find-package :ggp))
+(defparameter *rules-package*
+  (find-package :ggp-rules))
+
+(defparameter *constant-rules-symbols*
+  '(nil
+    ggp-rules::info
+    ggp-rules::play
+    ggp-rules::stop
+    ggp-rules::start
+
+    ggp-rules::name
+    ggp-rules::status
+    ggp-rules::busy
+    ggp-rules::available
+    ggp-rules::species
+    ggp-rules::alien
+
+    ggp-rules::ready
+    ggp-rules::done
+    ggp-rules::what
+
+    ggp-rules::role
+    ))
 
 
 ;;;; GGP Player
@@ -22,6 +43,23 @@
      :reader player-port
      :type (integer 0)
      :documentation "The port the HTTP server should listen on.")
+   (match-roles
+     :type (or null list)
+     :initform nil
+     :reader player-match-roles
+     :documentation "A list of the roles for the current match.  Feel free to read and use this if you like.  **Do not modify this.**")
+   (start-clock
+     :type (or null (integer 1))
+     :initform nil
+     :documentation "The start clock for the current game.  **Do not touch this.**  Use the `timeout` value passed to your methods instead.")
+   (play-clock
+     :type (or null (integer 1))
+     :initform nil
+     :documentation "The play clock for the current game.  **Do not touch this.**  Use the `timeout` value passed to your methods instead.")
+   (message-start
+     :type (or null (integer 0))
+     :initform nil
+     :documentation "The (internal-real) timestamp of when the current GGP message was received.  **Do not touch this.**  Use the `timeout` value passed to your methods instead.")
    (current-match
      :initform nil
      :documentation "The ID of the current match the player is playing, or `nil` if it is waiting.  **Do not touch this.**")
@@ -30,27 +68,39 @@
   (:documentation "The base class for a GGP player.  Custom players should extend this."))
 
 
-(defgeneric player-start-game (player rules role start-clock play-clock)
+(defgeneric player-start-game (player rules role timeout)
   (:documentation "Called when the game is started.
 
   `rules` is a list of lists/symbols representing the GDL description of the
-  game.  Note that all symbols are interned in the `GGP` package.
+  game.  Note that all symbols are interned in the `GGP-RULES` package.
+
+  `role` is a symbol representing the role of the player in this game.
+
+  `timeout` is the timestamp that the response to the server is due by, in
+  internal-real time units.  Basically: when `(get-internal-real-time)` returns
+  this number, your message better have reached the server.
 
   "))
 
 (defgeneric player-update-game (player moves)
-  (:documentation "Called after all player have made their moves.
+  (:documentation "Called after all players have made their moves.
 
-  `moves` will be a list of moves made by the players.
+  `moves` will be a list of `(role . move)` conses representing moves made by
+  each player last turn.
 
   "))
 
-(defgeneric player-select-move (player)
+(defgeneric player-select-move (player timeout)
   (:documentation "Called when it's time for the player to select a move to play.
 
   Must return a list/symbol of the GDL move to play.  Note that any symbols in
-  the move should be ones that are interned in the `GGP` package.  The author is
-  aware that this sucks and welcomes suggestions on how to make it less awful.
+  the move should be ones that are interned in the `GGP-RULES` package.  The
+  author is aware that this sucks and welcomes suggestions on how to make it
+  less awful.
+
+  `timeout` is the timestamp that the response to the server is due by, in
+  internal-real time units.  Basically: when `(get-internal-real-time)` returns
+  this number, your message better have reached the server.
 
   "))
 
@@ -63,13 +113,13 @@
   "))
 
 
-(defmethod player-start-game ((player ggp-player) rules role start-clock play-clock)
+(defmethod player-start-game ((player ggp-player) rules role timeout)
   nil)
 
 (defmethod player-update-game ((player ggp-player) moves)
   nil)
 
-(defmethod player-select-move ((player ggp-player))
+(defmethod player-select-move ((player ggp-player) timeout)
   (error "Required method player-select-move is not implemented for ~A" player))
 
 (defmethod player-stop-game ((player ggp-player))
@@ -80,13 +130,35 @@
 (defun safe-read-from-string (s)
   ;; what could go wrong
   (let ((*read-eval* nil)
-        (*package* *ggp-package*))
+        (*package* *rules-package*))
     (read-from-string s)))
 
 (defun render-to-string (e)
-  (let ((*package* *ggp-package*))
+  (let ((*package* *rules-package*))
     (format nil "~A" e)))
 
+(defun calculate-timeout (player clock)
+  "Calculate the timestamp (in internal units) that we must return by."
+  (+ (slot-value player 'message-start)
+     (* clock internal-time-units-per-second)))
+
+(defun clear-rules-package ()
+  (do-symbols (symbol *rules-package*) ; JESUS TAKE THE WHEEL
+    (when (not (member symbol *constant-rules-symbols*))
+      (unintern symbol *rules-package*))))
+
+(defun find-roles (rules)
+  (mapcar #'second
+          (remove-if-not #'(lambda (rule)
+                            (and (consp rule)
+                                 (eql 'ggp-rules::role (first rule))))
+                         rules)))
+
+(defun zip-moves (player moves)
+  (mapcar #'cons ; lol ggp
+          (slot-value player 'match-roles)
+          moves))
+
 
 ;;;; Clack Horseshit
 (defun l (&rest args)
@@ -109,70 +181,83 @@
 
 ;;;; GGP Protocol
 (defun handle-info (player)
-  `((name ,(slot-value player 'name))
-    (status ,(if (slot-value player 'current-match) 'busy 'available))
-    (species alien)))
+  `((ggp-rules::name ,(slot-value player 'name))
+    (ggp-rules::status ,(if (slot-value player 'current-match)
+                          'ggp-rules::busy
+                          'ggp-rules::available))
+    (ggp-rules::species ggp-rules::alien)))
 
 (defun handle-start (player match-id role rules start-clock play-clock)
-  (declare (ignore play-clock))
   (setf (slot-value player 'current-match) match-id)
+  (setf (slot-value player 'start-clock) start-clock)
+  (setf (slot-value player 'play-clock) play-clock)
+  (setf (slot-value player 'match-roles) (find-roles rules))
   (l "Starting match ~S as ~S~%" match-id role)
-  (player-start-game player rules role start-clock play-clock)
-  'ready)
+  (player-start-game player rules role (calculate-timeout player start-clock))
+  'ggp-rules::ready)
 
 (defun handle-play (player match-id moves)
+  (declare (ignore match-id))
   (l "Handling play request with moves ~S~%" moves)
-  (player-update-game player moves)
-  (player-select-move player))
+  (player-update-game player (zip-moves player moves))
+  (player-select-move player
+                      (calculate-timeout player (slot-value player 'play-clock))))
 
 (defun handle-stop (player match-id moves)
   (l "Handling stop request for ~S~%" match-id)
+  (player-update-game player (zip-moves player moves))
   (player-stop-game player)
   (setf (slot-value player 'current-match) nil)
-  'done)
+  (setf (slot-value player 'match-roles) nil)
+  (clear-rules-package)
+  'ggp-rules::done)
 
 
 (defun route (player request)
   "Route the request to the appropriate player function."
   (match request
-    (`(info)
+    (`(ggp-rules::info)
      (handle-info player))
 
-    (`(play ,match-id ,moves)
+    (`(ggp-rules::play ,match-id ,moves)
      (handle-play player match-id moves))
 
-    (`(stop ,match-id ,moves)
+    (`(ggp-rules::stop ,match-id ,moves)
      (handle-stop player match-id moves))
 
-    (`(start ,match-id ,role ,rules ,start-clock ,play-clock)
+    (`(ggp-rules::start ,match-id ,role ,rules ,start-clock ,play-clock)
      (handle-start player match-id role rules start-clock play-clock))
 
     (unknown-request
       (l "UNKNOWN REQUEST: ~S~%~%" unknown-request)
-      'what)))
+      'ggp-rules::what)))
 
 
 ;;;; Boilerplate
 (defun should-log-p (request)
   (match request
-    (`(info) nil)
+    (`(ggp-rules::info) nil)
     (_ t)))
 
 (defun app (player env)
-  (let* ((body (get-body env))
-         (request (safe-read-from-string body))
-         (should-log (should-log-p request)))
-    (when should-log
-      (l "~%~%Got a request ====================================~%")
-      (l "~S~%" request)
-      (l "==================================================~%"))
-    (let* ((response (route player request))
-           (rendered-response (render-to-string response)))
-      (when should-log
-        (l "==================================================~%")
-        (l "Responding with:~%~A~%" rendered-response)
-        (l "==================================================~%"))
-      (resp rendered-response))))
+  (setf (slot-value player 'message-start) (get-internal-real-time))
+  (unwind-protect
+      (let* ((body (get-body env))
+             (request (safe-read-from-string body))
+             (should-log (should-log-p request)))
+        (when should-log
+          (l "~%~%Got a request ====================================~%")
+          (l "~S~%" body)
+          (l "~A~%" (render-to-string request))
+          (l "==================================================~%"))
+        (let* ((response (route player request))
+               (rendered-response (render-to-string response)))
+          (when should-log
+            (l "==================================================~%")
+            (l "Responding with:~%~A~%" rendered-response)
+            (l "==================================================~%"))
+          (resp rendered-response)))
+    (setf (slot-value player 'message-start) nil)))
 
 
 ;;;; Spinup/spindown