1a4608813a73

Add some documentation for the reasoner and a few missing functions
[view raw] [browse files]
author Steve Losh <steve@stevelosh.com>
date Sun, 29 Jan 2017 22:07:19 +0000
parents a07961309f28
children e33f59e97ecb
branches/tags (none)
files Makefile README.markdown docs/03-reference.markdown docs/04-changelog.markdown docs/04-reference-reasoner.markdown docs/05-changelog.markdown docs/api.lisp docs/index.markdown gdl/buttons.gdl gdl/tictactoe.gdl package.reasoner.lisp src/reasoner.lisp

Changes

--- 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
--- 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:** <http://sjl.bitbucket.org/cl-ggp/>
-* **Code:** <http://bitbucket.org/sjl/cl-ggp/>
-* **Issues:** <http://bitbucket.org/sjl/cl-ggp/issues/>
+* **Documentation:** <https://sjl.bitbucket.io/cl-ggp/>
+* **Mercurial:** <https://bitbucket.org/sjl/cl-ggp/>
+* **Git:** <https://github.com/sjl/cl-ggp/>
--- 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))
--- 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.
-
--- /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.
+
--- /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.
+
--- 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")
+
--- 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:** <http://sjl.bitbucket.org/cl-ggp/>
-* **Code:** <http://bitbucket.org/sjl/cl-ggp/>
-* **Issues:** <http://bitbucket.org/sjl/cl-ggp/issues/>
+* **Documentation:** <https://sjl.bitbucket.io/cl-ggp/>
+* **Mercurial:** <https://bitbucket.org/sjl/cl-ggp/>
+* **Git:** <https://github.com/sjl/cl-ggp/>
--- /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)))
--- /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))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
--- 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."))
 
 
--- 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)))
+