--- 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 — it's just a bunch of lists of
+symbols — 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