This document assumes you know what General Game Playing is, what GDL is, and how the GGP community/competitions/etc work.
The cl-ggp
system handles the GGP network protocol and game flow for you.
Players are implemented as CLOS objects.
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:
(defclass simple-player (ggp:ggp-player) ()) (defvar *player* (make-instance 'simple-player :name "SimplePlayer" :port 4000)) (ggp:start-player *player*)
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
.
cl-ggp
defines four generic methods that are called on players at various
points in each game. You can provide method definitions for some or all of
these to let your player do whatever it needs to do.
At a minimum you must implement player-select-move
. The others are
optional and will default to doing nothing.
(defmethod ggp:player-start-game ((player YOUR-PLAYER) rules role timeout) ...)
This is called when a new game starts.
rules
is the GDL rules of the game, parsed into Lisp lists/symbols. You'll
probably want to feed this into a logic library.
role
is a symbol representing which role you've been assigned.
timeout
is the timestamp that the response to the server is due by, in
internal-real time units (more on this later).
(defmethod ggp:player-update-game ((player YOUR-PLAYER) moves) ...)
This is called once per turn, to allow you to update the game state with the moves each player selected.
moves
will be an association list of (role . move)
conses representing the
moves made by each player last turn.
(defmethod ggp:player-select-move ((player YOUR-PLAYER) timeout) ...)
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).
(defmethod ggp: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.
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:
In a nutshell: when (get-internal-real-time)
returns the number given to you
in timeout
, your message better have already reached the server.
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
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.
The cl-ggp.reasoner
system contains a Prolog-based GDL reasoner based on the
Temperance logic programming library.
It's useful as a starting point if you just want to get a bot up and running quickly. If you want more speed or control over the reasoning process you'll probably want to replace it with your own implementation.
You can make a reasoner for a set of GDL rules (e.g. the rules given to you by
player-start-game
) with (make-reasoner rules)
.
Once you've got a reasoner you can ask it for the initial state of the game with
(initial-state reasoner)
. This will give you back a state object — it's
currently just a list, but this may change in the future, so you should just
treat it as an opaque object.
Once you've got a state and a set of moves you can compute the next state with
(next-state reasoner current-state moves)
.
States can be queried for their terminality, goal values, and legal moves with
terminalp
, goal-values-for
, and legal-moves-for
respectively.
See the Reasoner API Reference for more details.
Let's create a small example player that uses the reasoner to play any GGP game legally. We'll start by creating a class for the player, with three slots to keep track of the data we need to play a game:
(defclass random-player (ggp:ggp-player) ((role :accessor p-role) (current-state :accessor p-current-state) (reasoner :accessor p-reasoner)))
Now we can implement player-start-game
. We'll store the role we've been
assigned, and create a reasoner so we can compute our legal moves later. We'll
ignore the deadline because we're not doing any extensive processing:
(defmethod ggp:player-start-game ((player random-player) rules role deadline) (declare (ignore deadline)) (setf (p-role player) role (p-reasoner player) (ggp.reasoner:make-reasoner rules)))
Next we'll implement player-update-game
, which will compute the current state
of the game and store it in the current-state
slot:
(defmethod ggp:player-update-game ((player random-player) moves) (setf (p-current-state player) (if (null moves) (ggp.reasoner:initial-state (p-reasoner player)) (ggp.reasoner:next-state (p-reasoner player) (p-current-state player) moves))))
If moves
is null we ask the reasoner for the initial state, otherwise we
compute the next state from the current one and the moves that were made.
Now we can implement player-select-move
. We'll just ask the reasoner for all
our legal moves and choose one at random. If we wanted to make a smarter
player, this is where we would search the game tree to find a good move:
(defmethod ggp:player-select-move ((player random-player) deadline) (declare (ignore deadline)) (let ((moves (ggp.reasoner:legal-moves-for (p-reasoner player) (p-current-state player) (p-role player)))) (nth (random (length moves)) moves)))
Finally we can implement player-stop-game
. We'll just clear out the player's
slots so the data can be garbage collected:
(defmethod ggp:player-stop-game ((player random-player)) (setf (p-current-state player) nil (p-reasoner player) nil (p-role player) nil))
Now we can make an instance of our player and start it up!
(defvar *random-player* (make-instance 'random-player :name "RandomPlayer" :port 4000)) (ggp:start-player *random-player*)