# HG changeset patch # User Steve Losh # Date 1458768998 0 # Node ID 04d792fae37f8fe6bdfe7565c2c20179fbd416e3 # Parent ce6aa58d27da79399d60d767ab10485dcddc2db1 More documentation diff -r ce6aa58d27da -r 04d792fae37f Makefile --- 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 diff -r ce6aa58d27da -r 04d792fae37f docs/01-installation.markdown --- 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 diff -r ce6aa58d27da -r 04d792fae37f docs/02-overview.markdown --- 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 diff -r ce6aa58d27da -r 04d792fae37f docs/03-reference.markdown --- 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. + + + diff -r ce6aa58d27da -r 04d792fae37f docs/04-changelog.markdown --- 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. + diff -r ce6aa58d27da -r 04d792fae37f docs/api.lisp --- 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" ) diff -r ce6aa58d27da -r 04d792fae37f package.lisp --- 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. + + ")) + + diff -r ce6aa58d27da -r 04d792fae37f src/example.lisp --- 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) diff -r ce6aa58d27da -r 04d792fae37f src/ggp.lisp --- 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