# HG changeset patch # User Steve Losh # Date 1485727639 0 # Node ID 1a4608813a730af4e8e474f593ca5417b4609988 # Parent a07961309f28eede631a7fffebbf059667c6f4be Add some documentation for the reasoner and a few missing functions diff -r a07961309f28 -r 1a4608813a73 Makefile --- a/Makefile Sun Jan 29 12:53:28 2017 +0000 +++ b/Makefile Sun Jan 29 22:07:19 2017 +0000 @@ -1,20 +1,20 @@ .PHONY: pubdocs -sourcefiles = $(shell ffind --full-path --dir src --literal .lisp) +sourcefiles = $(shell ffind --full-path --literal .lisp) docfiles = $(shell ls docs/*.markdown) -apidoc = docs/03-reference.markdown +apidocs = $(shell ls docs/*reference*.markdown) # src/utils.lisp: src/make-utilities.lisp # cd src && sbcl --noinform --load make-utilities.lisp --eval '(quit)' -$(apidoc): $(sourcefiles) docs/api.lisp package.lisp +$(apidocs): $(sourcefiles) sbcl --noinform --load docs/api.lisp --eval '(quit)' +docs/build/index.html: $(docfiles) $(apidocs) docs/title + cd docs && ~/.virtualenvs/d/bin/d + docs: docs/build/index.html -docs/build/index.html: $(docfiles) $(apidoc) docs/title - cd docs && ~/.virtualenvs/d/bin/d - pubdocs: docs hg -R ~/src/sjl.bitbucket.org pull -u rsync --delete -a ./docs/build/ ~/src/sjl.bitbucket.org/cl-ggp diff -r a07961309f28 -r 1a4608813a73 README.markdown --- a/README.markdown Sun Jan 29 12:53:28 2017 +0000 +++ b/README.markdown Sun Jan 29 22:07:19 2017 +0000 @@ -1,17 +1,24 @@ - +``` ___ __ ___ ___ ____ / __)( ) ___ / __) / __)( _ \ ( (__ / (_/\(___)( (_ \( (_ \ ) __/ \___)\____/ \___/ \___/(__) +``` -`cl-ggp` is a tiny framework for writing [GGP][] players in Common Lisp. +`cl-ggp` is a tiny framework for writing [general game players][GGP] in Common +Lisp. -It handles the GGP protocol for you but *nothing else*. In particular you'll -need to bring your own logic system to parse the games. +The `cl-ggp` system handles the GGP protocol for you and *nothing else*. If you +plan on doing your own GDL reasoning, this is all you need. + +The `cl-ggp.reasoner` system contains a simple Prolog-based reasoner using the +[Temperance][] logic programming library. It's useful as a starting point for +when writing players. [GGP]: http://www.ggp.org/ +[Temperance]: https://sjl.bitbucket.io/temperance/ * **License:** MIT/X11 -* **Documentation:** -* **Code:** -* **Issues:** +* **Documentation:** +* **Mercurial:** +* **Git:** diff -r a07961309f28 -r 1a4608813a73 docs/03-reference.markdown --- a/docs/03-reference.markdown Sun Jan 29 12:53:28 2017 +0000 +++ b/docs/03-reference.markdown Sun Jan 29 22:07:19 2017 +0000 @@ -1,4 +1,4 @@ -# API Reference +# Main API Reference The following is a list of all user-facing parts of `cl-ggp`. @@ -18,72 +18,6 @@ The base class for a GGP player. Custom players should extend this. -#### Slot `NAME` - -* Allocation: `:INSTANCE` -* Type: `STRING` -* Initarg: `:NAME` -* Initform: `"CL-GGP"` -* Reader: `PLAYER-NAME` - -The name of the player. - -#### Slot `PORT` - -* Allocation: `:INSTANCE` -* Type: `(INTEGER 0)` -* Initarg: `:PORT` -* Initform: `9999` -* Reader: `PLAYER-PORT` - -The port the HTTP server should listen on. - -#### Slot `MATCH-ROLES` - -* Allocation: `:INSTANCE` -* Type: `(OR NULL LIST)` -* Initform: `NIL` -* 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))` -* Initform: `NIL` - -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))` -* Initform: `NIL` - -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))` -* Initform: `NIL` - -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` -* Initform: `NIL` - -The ID of the current match the player is playing, or `nil` if it is waiting. **Do not touch this.** - -#### Slot `SERVER` - -* Allocation: `:INSTANCE` - -The Clack server object of the player. **Do not touch this.** Use `start-player` and `kill-player` to start/stop the server safely. - ### `KILL-PLAYER` (function) (KILL-PLAYER PLAYER) @@ -163,6 +97,12 @@ +### `READ-GDL-FROM-FILE` (function) + + (READ-GDL-FROM-FILE FILENAME) + +Read GDL from `filename` + ### `START-PLAYER` (function) (START-PLAYER PLAYER &KEY (SERVER :HUNCHENTOOT) (USE-THREAD T)) diff -r a07961309f28 -r 1a4608813a73 docs/04-changelog.markdown --- a/docs/04-changelog.markdown Sun Jan 29 12:53:28 2017 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,19 +0,0 @@ -Changelog -========= - -Here's the list of changes in each released version. - -[TOC] - -Pending -------- - -* `start-player` now takes `:server` and `:use-thread` options which it passes - along to Clack. -* Added rudimentary support for writing GDL-II players. - -v0.0.1 ------- - -Initial alpha version. Things are going to break a lot. Don't use this. - diff -r a07961309f28 -r 1a4608813a73 docs/04-reference-reasoner.markdown --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/04-reference-reasoner.markdown Sun Jan 29 22:07:19 2017 +0000 @@ -0,0 +1,68 @@ +# Reasoner API Reference + +cl-ggp includes a simple Prolog-based reasoner you can use as a starting point when writing general game players in the `cl-ggp.reasoner` system. + + [TOC] + +## Package `GGP.REASONER` + +This package contains a simple GGP reasoner. It can be useful as a starting point for writing general game players. + +### `GOAL-VALUE-FOR` (function) + + (GOAL-VALUE-FOR REASONER STATE ROLE) + +Return the goal value for `role` in `state`, or `nil` if none exists. + + Note that the GDL spec only requires that such values have meaning in terminal + states. Game authors sometimes add goal values to nonterminal states, but + this is probably not something you should rely on. + + + +### `INITIAL-STATE` (function) + + (INITIAL-STATE REASONER) + +Return the initial state of `reasoner`. + +### `LEGAL-MOVES-FOR` (function) + + (LEGAL-MOVES-FOR REASONER STATE ROLE) + +Return a list of legal moves for `role` in `state`. + + `ggp:player-select-move` must return exactly one of the items in this list. + + + +### `MAKE-REASONER` (function) + + (MAKE-REASONER RULES) + +Create and return a reasoner for the given GDL `rules`. + + `rules` should be a list of GDL rules with the symbols interned into the + appropriate packages. `ggp:player-start-game` will give you this, or you can + use `ggp:read-gdl-from-file` to get them without a player if you want to just + poke at the reasoner. + + + +### `NEXT-STATE` (function) + + (NEXT-STATE REASONER STATE MOVES) + +Compute and return the successor to `state`, assuming `moves` were made. + + `moves` should be an alist of `(role . move)` pairs, which is what + `ggp:player-update-game` will give you. + + + +### `TERMINALP` (function) + + (TERMINALP REASONER STATE) + +Return whether `state` is terminal. + diff -r a07961309f28 -r 1a4608813a73 docs/05-changelog.markdown --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/05-changelog.markdown Sun Jan 29 22:07:19 2017 +0000 @@ -0,0 +1,19 @@ +Changelog +========= + +Here's the list of changes in each released version. + +[TOC] + +Pending +------- + +* `start-player` now takes `:server` and `:use-thread` options which it passes + along to Clack. +* Added rudimentary support for writing GDL-II players. + +v0.0.1 +------ + +Initial alpha version. Things are going to break a lot. Don't use this. + diff -r a07961309f28 -r 1a4608813a73 docs/api.lisp --- a/docs/api.lisp Sun Jan 29 12:53:28 2017 +0000 +++ b/docs/api.lisp Sun Jan 29 22:07:19 2017 +0000 @@ -1,12 +1,9 @@ (ql:quickload "cl-d-api") -(defparameter *document-packages* - (list "GGP" "GGP-RULES")) - -(defparameter *output-path* - #p"docs/03-reference.markdown" ) - -(defparameter *header* +(d-api:generate-documentation + :cl-ggp + #p"docs/03-reference.markdown" + (list "GGP" "GGP-RULES") "The following is a list of all user-facing parts of `cl-ggp`. If there are backwards-incompatible changes to anything listed here, they will @@ -15,10 +12,15 @@ Anything not listed here is subject to change at any time with no warning, so don't touch it. -") +" + :title "Main API Reference") (d-api:generate-documentation - :cl-ggp - *output-path* - *document-packages* - *header*) + :cl-ggp.reasoner + #p"docs/04-reference-reasoner.markdown" + (list "GGP.REASONER") + "cl-ggp includes a simple Prolog-based reasoner you can use as a starting point when writing general game players in the `cl-ggp.reasoner` system. + + " + :title "Reasoner API Reference") + diff -r a07961309f28 -r 1a4608813a73 docs/index.markdown --- a/docs/index.markdown Sun Jan 29 12:53:28 2017 +0000 +++ b/docs/index.markdown Sun Jan 29 22:07:19 2017 +0000 @@ -1,11 +1,17 @@ -`cl-ggp` is a tiny framework for writing [GGP][] players in Common Lisp. +`cl-ggp` is a tiny framework for writing [general game players][GGP] in Common +Lisp. -It handles the GGP protocol for you but *nothing else*. In particular you'll -need to bring your own logic system to parse the games. +The `cl-ggp` system handles the GGP protocol for you and *nothing else*. If you +plan on doing your own GDL reasoning, this is all you need. + +The `cl-ggp.reasoner` system contains a simple Prolog-based reasoner using the +[Temperance][] logic programming library. It's useful as a starting point for +when writing players. [GGP]: http://www.ggp.org/ +[Temperance]: https://sjl.bitbucket.io/temperance/ * **License:** MIT/X11 -* **Documentation:** -* **Code:** -* **Issues:** +* **Documentation:** +* **Mercurial:** +* **Git:** diff -r a07961309f28 -r 1a4608813a73 gdl/buttons.gdl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gdl/buttons.gdl Sun Jan 29 22:07:19 2017 +0000 @@ -0,0 +1,85 @@ +(role robot) +(init (off p)) +(init (off q)) +(init (off r)) +(init (step 1)) +(<= (next (on p)) + (does robot a) + (true (off p))) +(<= (next (on q)) + (does robot a) + (true (on q))) +(<= (next (on r)) + (does robot a) + (true (on r))) +(<= (next (off p)) + (does robot a) + (true (on p))) +(<= (next (off q)) + (does robot a) + (true (off q))) +(<= (next (off r)) + (does robot a) + (true (off r))) +(<= (next (on p)) + (does robot b) + (true (on q))) +(<= (next (on q)) + (does robot b) + (true (on p))) +(<= (next (on r)) + (does robot b) + (true (on r))) +(<= (next (off p)) + (does robot b) + (true (off q))) +(<= (next (off q)) + (does robot b) + (true (off p))) +(<= (next (off r)) + (does robot b) + (true (off r))) +(<= (next (on p)) + (does robot c) + (true (on p))) +(<= (next (on q)) + (does robot c) + (true (on r))) +(<= (next (on r)) + (does robot c) + (true (on q))) +(<= (next (off p)) + (does robot c) + (true (off p))) +(<= (next (off q)) + (does robot c) + (true (off r))) +(<= (next (off r)) + (does robot c) + (true (off q))) +(<= (next (step ?y)) + (true (step ?x)) + (succ ?x ?y)) +(succ 1 2) +(succ 2 3) +(succ 3 4) +(succ 4 5) +(succ 5 6) +(succ 6 7) +(legal robot a) +(legal robot b) +(legal robot c) +(<= (goal robot 100) + (true (on p)) + (true (on q)) + (true (on r))) +(<= (goal robot 0) (or + (not (true (on p))) + (not (true (on q))) + (not (true (on r))))) +(<= terminal + (true (step 7))) +(<= terminal + (true (on p)) + (true (on q)) + (true (on r))) diff -r a07961309f28 -r 1a4608813a73 gdl/tictactoe.gdl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gdl/tictactoe.gdl Sun Jan 29 22:07:19 2017 +0000 @@ -0,0 +1,135 @@ +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Tictactoe +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Roles +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + (role xplayer) + (role oplayer) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Initial State +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + (init (cell 1 1 b)) + (init (cell 1 2 b)) + (init (cell 1 3 b)) + (init (cell 2 1 b)) + (init (cell 2 2 b)) + (init (cell 2 3 b)) + (init (cell 3 1 b)) + (init (cell 3 2 b)) + (init (cell 3 3 b)) + (init (control xplayer)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Dynamic Components +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; Cell + + (<= (next (cell ?m ?n x)) + (does xplayer (mark ?m ?n)) + (true (cell ?m ?n b))) + + (<= (next (cell ?m ?n o)) + (does oplayer (mark ?m ?n)) + (true (cell ?m ?n b))) + + (<= (next (cell ?m ?n ?w)) + (true (cell ?m ?n ?w)) + (distinct ?w b)) + + (<= (next (cell ?m ?n b)) + (does ?w (mark ?j ?k)) + (true (cell ?m ?n b)) + (or (distinct ?m ?j) (distinct ?n ?k))) + + (<= (next (control xplayer)) + (true (control oplayer))) + + (<= (next (control oplayer)) + (true (control xplayer))) + + + (<= (row ?m ?x) + (true (cell ?m 1 ?x)) + (true (cell ?m 2 ?x)) + (true (cell ?m 3 ?x))) + + (<= (column ?n ?x) + (true (cell 1 ?n ?x)) + (true (cell 2 ?n ?x)) + (true (cell 3 ?n ?x))) + + (<= (diagonal ?x) + (true (cell 1 1 ?x)) + (true (cell 2 2 ?x)) + (true (cell 3 3 ?x))) + + (<= (diagonal ?x) + (true (cell 1 3 ?x)) + (true (cell 2 2 ?x)) + (true (cell 3 1 ?x))) + + + (<= (line ?x) (row ?m ?x)) + (<= (line ?x) (column ?m ?x)) + (<= (line ?x) (diagonal ?x)) + + + (<= open + (true (cell ?m ?n b))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + (<= (legal ?w (mark ?x ?y)) + (true (cell ?x ?y b)) + (true (control ?w))) + + (<= (legal xplayer noop) + (true (control oplayer))) + + (<= (legal oplayer noop) + (true (control xplayer))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + (<= (goal xplayer 100) + (line x)) + + (<= (goal xplayer 50) + (not (line x)) + (not (line o)) + (not open)) + + (<= (goal xplayer 0) + (line o)) + + (<= (goal oplayer 100) + (line o)) + + (<= (goal oplayer 50) + (not (line x)) + (not (line o)) + (not open)) + + (<= (goal oplayer 0) + (line x)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + (<= terminal + (line x)) + + (<= terminal + (line o)) + + (<= terminal + (not open)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff -r a07961309f28 -r 1a4608813a73 package.reasoner.lisp --- a/package.reasoner.lisp Sun Jan 29 12:53:28 2017 +0000 +++ b/package.reasoner.lisp Sun Jan 29 22:07:19 2017 +0000 @@ -7,6 +7,6 @@ :terminalp :legal-moves-for :goal-value-for) - (:documentation "The package containing a simple GGP reasoner.")) + (:documentation "This package contains a simple GGP reasoner. It can be useful as a starting point for writing general game players.")) diff -r a07961309f28 -r 1a4608813a73 src/reasoner.lisp --- a/src/reasoner.lisp Sun Jan 29 12:53:28 2017 +0000 +++ b/src/reasoner.lisp Sun Jan 29 22:07:19 2017 +0000 @@ -5,8 +5,11 @@ (and (consp form) (eq (car form) 'ggp-rules::<=))) +(defun dedupe (things) + (remove-duplicates things :test #'equal)) + (defun normalize-state (state) - (remove-duplicates state :test #'equal)) + (dedupe state)) ;;;; Reasoner ----------------------------------------------------------------- @@ -39,6 +42,7 @@ ;; todo this rules) + (defun load-rule (rule) (if (gdl-rule-p rule) (apply #'invoke-rule t (rest rule)) @@ -51,6 +55,14 @@ (defun make-reasoner (rules) + "Create and return a reasoner for the given GDL `rules`. + + `rules` should be a list of GDL rules with the symbols interned into the + appropriate packages. `ggp:player-start-game` will give you this, or you can + use `ggp:read-gdl-from-file` to get them without a player if you want to just + poke at the reasoner. + + " (let ((reasoner (make-instance 'reasoner))) (load-rules-into-reasoner reasoner rules) reasoner)) @@ -94,11 +106,18 @@ (defun initial-state (reasoner) + "Return the initial state of `reasoner`." (normalize-state (query-for (reasoner-database reasoner) ?what (ggp-rules::init ?what)))) (defun next-state (reasoner state moves) + "Compute and return the successor to `state`, assuming `moves` were made. + + `moves` should be an alist of `(role . move)` pairs, which is what + `ggp:player-update-game` will give you. + + " (with-database (reasoner-database reasoner) (ensure-state reasoner state) (ensure-moves reasoner moves) @@ -107,12 +126,54 @@ (defun legal-moves (reasoner state) + "Return an alist of `(role . move)` for all legal moves in `state`." (with-database (reasoner-database reasoner) (ensure-state reasoner state) - (query-all t (ggp-rules::legal ?role ?action)))) + (dedupe (loop :for move :in (query-all t (ggp-rules::legal ?role ?action)) + :collect (cons (getf move '?role) + (getf move '?action)))))) (defun legal-moves-for (reasoner state role) - (loop :for move :in (legal-moves reasoner state) - :when (eq (getf move '?role) role) - :collect (getf move '?action))) + "Return a list of legal moves for `role` in `state`. + + `ggp:player-select-move` must return exactly one of the items in this list. + + " + (with-database (reasoner-database reasoner) + (ensure-state reasoner state) + (dedupe (invoke-query-for t '?action `(ggp-rules::legal ,role ?action))))) + + +(defun goal-values (reasoner state) + "Return an alist of `(role . value)` pairs of goal values for `state`. + + Note that the GDL spec only requires that such values have meaning in terminal + states. Game authors sometimes add goal values to nonterminal states, but + this is probably not something you should rely on. + " + (with-database (reasoner-database reasoner) + (ensure-state reasoner state) + (dedupe (loop :for goal :in (query-all t (ggp-rules::goal ?role ?value)) + :collect (cons (getf goal '?role) + (getf goal '?value)))))) + +(defun goal-value-for (reasoner state role) + "Return the goal value for `role` in `state`, or `nil` if none exists. + + Note that the GDL spec only requires that such values have meaning in terminal + states. Game authors sometimes add goal values to nonterminal states, but + this is probably not something you should rely on. + + " + (with-database (reasoner-database reasoner) + (ensure-state reasoner state) + (car (invoke-query-for t '?value `(ggp-rules::goal ,role ?value))))) + + +(defun terminalp (reasoner state) + "Return whether `state` is terminal." + (with-database (reasoner-database reasoner) + (ensure-state reasoner state) + (prove t ggp-rules::terminal))) +