All communication takes the form of messages between client and server, as plain text over a TCP socket.
All interactions are initiated by the client; a single client->server message is sent, which results in exactly one server->client message. The server never sends unsolicited messages. From the client's perspective, the protocol is effectively synchronous RPC.
The purpose of these constraints is to allow protocol participants to straightforwardly use blocking I/O.
In this specification, lines starting with >
are used to indicate
messages from client to server; lines with no prefix indicate
responses.
Each message is a JSON object, sent on exactly one line of text. Embedded newlines are not permitted. Embedded carriage returns should be ignored.
All messages have the same top-level structure:
{
"msg": "<kind>",
"data: {...}
}
Other header information (keys in the top-level object) may be added in a future protocol version. Participants must ignore unknown keys in the header.
Values of msg
are drawn from a fixed set (per protocol
version). data
is optional, but if present its value must always be
an object, even when there is only one value; this is to ease
backwards compatibility. Participants must ignore unknown keys in
data
.
The purpose of these constraints is to ease implementation in the largest number of languages and environments:
- The JSON parser only needs to be able to do String -> JSON (which is usually the first example given for using a JSON libary).
- Message dispatch is straightforward:
msg
values map to functions, all of which can be of the same type, with a single argument (data
). - Protocol extensions are likely to be mostly backwards-compatible without any real implementation effort, provided they only add new message types and keys without changing the semantics of old ones.
Players will only experience a single match per TCP socket, and their sessions are stateful. Spectators will see the data for all ongoing matches on the same socket.
All games are played on a board, which is a tiling of cells. To ease implementation of clients, they should not generally have to worry about how to tile cells.
Boards are represented as graphs. In general, clients should be able to navigate the graph without regard to the spatial positioning of cells. Instead, the server will compute the adjacency relation for the graph and provide it to clients on request:
> {"msg": "edges"}
{"resp": "edges", "data": {"edges": [["0,0", "0,1"], ["0,1", "1,1"], ["1,1","1,0"], ["1,0","0,0"], ["0,1", "0,0"], ["1,1", "0,1"], ["1,0","1,1"], ["0,0","1,0"]]]]}}
Clients can also request information regarding the type of tiling and the shape of the board, but this will probably only be needed for visualiation.
Boards used in this protocol are generally expected to be 2D and represented by planar graphs.
Clients which do not do visualisation should regard the strings identifying nodes as opaque. However, for 2D tiled boards, the names should be the column-major coordinates of each cell; that is, the cells in the first column of the board should be "0,0", "0,1", "0,2". For display purposes, 0,0 should be the graphics origin, i.e., the top left.
The board state is divided into dynamic and static state.
Static state is essentially the shape of the board (i.e., the graph). It is guaranteed not to change during a match; no guarantees are made between matches.
Dynamic state consists of the positions of the snakes, and any food on the board.
The position of each snake is specified as a list of nodes, e.g.:
["0,0", "0,1"]
The first element of the list is the head of the snake.
The position of food is specified as a list of the nodes which contain food.
Subsequent extensions may introduce additional kinds of dynamic state.
?? Food ??
?? Growth ??
?? Fun ??
?? Profit ??
Sessions start with version negotation. The client asks the server which protocol versions it supports:
> {"msg": "versions"}
{"resp": "versions", "data": {"available": [1, 2, 3]}}
and then chooses a protocol version:
> {"msg": "version", "data": {"requested_version": 1}}
{"resp": "version", "data": {"accepted_version": 1}}
The client then registers with the server, offerring a preferred name:
> {"msg": "register", "data": {"name": "sirpent-demo", "kind": "player", "secret": "pa55word"}
{"resp": "register", "data": {"name": "sirpent-demo"}}
The server may return a different name to that offered by the client; the client must then use this name (for example, the server might add a suffix to distinguish multiple versions of the same client).
Client secrets are optional. The name/secret mapping is agreed
out-of-band and maintained by the server. Clients which do not provide
a secret or which provide an incorrect secret will never be registered
with the name they request, but with some other disambiguated name,
such as sirpent-demo (charlatan)
.
If multiple clients provide the correct secret, naming behaviour is implementation-defined. Reasonable options would be to give them all the same name, or to add a numeric suffix in a format reserved for authenticated players.
The client then sends ready
messages to ask to join a match.
If no match is available, the server will send keepalive
responses
after a reasonable timeout.
> {"msg": "ready"}
{"resp": "keepalive"}
the client should immediately respond with another ready
message.
Once a match is available, the server will send a match_start
message containing the dynamic state of the board:
> {"msg": "ready"}
{"resp": "match_start", "data": {"snakes": {"sirpent-demo": {"alive": true, "pos":["0,0"]}, "sirpent-dummy":{"alive":true, "pos":["1,1"]}}, "food": ["0,1"]}}
Clients can query for the static and dynamic state of the board as they need it:
> {"msg": "edges"}
{"resp": "edges", "data": {"edges": [["0,0", "0,1"], ["0,1", "1,1"], ["1,1","1,0"], ["1,0","0,0"], ["0,1", "0,0"], ["1,1", "0,1"], ["1,0","1,1"], ["0,0","1,0"]]]]}}
> {"msg": "tiling"}
{"resp": "tiling", "data": {"kind": "square", "width":2, "height":2}
> {"msg": "cells"}
{"resp": "cells", "data": {"cells": ["0,1", "0,1"]}}
> {"msg": "match_state"}
{"resp": "match_state", "data": {"match_id": "<xxx>", "snakes": {"sirpent-demo": {"alive": true, "pos":["0,0"]}, "sirpent-dummy":{"alive":true, "pos":["1,1"]}}, "food": ["0,1"]}}
Most clients will probably just care about the edges
graph, unless
they want to implement their own tiling algorithms.
Clients then send a move
message, indicating the cell into which
they wish to move the head of their snake. In response, they will be
updated with the dynamic state of the board after all snakes have
moved:
> {"msg": "move", "data": {"next": "1,1"}}
{"resp": "match_play", "data": {"match_id": "<xxx>", "snakes": {"sirpent-demo": {"alive": true, "pos":["0,0"]}, "sirpent-dummy":{"alive":true, "pos":["1,1"]}}, "food": ["0,1"]}}
If a move is invalid, the server will send a move_error
response:
> {"msg": "move", "data": {"next": "0,0"}}
{"resp": "move_error"}
Moves continue until the match is won or lost, at which point the
server will send a match_over
message with the final dynamic state of
the board.
> {"msg": "move", "data": {"next": "0,1"}}
{"resp": "match_over", "data": {"match_id": "<xxx>", "snakes": {"sirpent-demo": {"alive": true, "pos":["0,0"]}, "sirpent-dummy":{"alive":false, "pos":["1,1"]}}, "food": ["0,1"]}}
Clients can then send an outcome
message to request the result of match.
> {"msg": "outcome"}
{"resp": "outcome", "data": {"match_id": "<xxx>", "victor": "sirpent-demo", "scores": {"sirpent-demo": 10, "sirpent-dummy": 2}}
Once a match has finished, clients can again indicate that they are ready for another match.
> {"msg": "ready"}
{"resp": "keepalive"}
Clients can also register as spectators. The expected use case for this is scoreboards and visualizers.
> {"msg": "register", "data": {"name": "sirpent-demo", "kind": "spectator"}
{"resp": "register", "data": {"name": "sirpent-demo"}}
> {"msg": "watch"}
{"resp": "match_watch", "data": {"match_id": "<xxx>", "snakes": {"sirpent-demo": {"alive": true, "pos":["0,0"]}, "sirpent-dummy":{"alive":true, "pos":["1,1"]}}, "food": ["0,1"]}}
> {"msg": "outcome"}
{"resp": "outcome", "data": {"match_id": "<xxx>", "victor": "sirpent-demo", "scores": {"sirpent-demo": 10, "sirpent-dummy": 2}}
> {"msg": "watch"}
{"resp": "keepalive"}
> {"msg": "outcome"}
{"resp": "keepalive"}
Spectators may also ask for the state of the board for a given match:
> {"msg": "edges", "data": {"match_id": "<xxx>"}}
{"resp": "edges", "data": {"edges": [["0,0", "0,1"], ["0,1", "1,1"], ["1,1","1,0"], ["1,0","0,0"], ["0,1", "0,0"], ["1,1", "0,1"], ["1,0","1,1"], ["0,0","1,0"]]]]}}
> {"msg": "tiling", "data": {"match_id": "<xxx>"}}
{"resp": "tiling", "data": {"kind": "square", "width":2, "height":2}
> {"msg": "cells", "data": {"match_id": "<xxx>"}}
{"resp": "cells", "data": {"cells": ["0,1", "0,1"]}}
> {"msg": "match_state", "data": {"match_id": "<xxx>"}}
{"resp": "match_state", "data": {"match_id": "<xxx>", "snakes": {"sirpent-demo": {"alive": true, "pos":["0,0"]}, "sirpent-dummy":{"alive":true, "pos":["1,1"]}}, "food": ["0,1"]}}
Spectators should poll both watch
and outcome
regularly. The
server should respond with a keepalive
after a short timeout if no
watch
response is available, and instantly if no outcome
response
is available.
If a client sends an incorrect move (i.e., to a cell non-adjacent to
the head, or overlapping the tail), the server responds with
move_error
:
> {"msg": "move", "data": {"next": "0,0"}}
{"resp": "move_error", "data": {"error_msg": "Invalid move"}}
The data
key of the response is optional and clients must not depend
on it. The server may include additional helpful information in
data
, if it is feeling magnanimous.
If a client sends a message which is invalid in the current session
state (e.g., sending a move after a match has finished) the server
responds with state_error
:
> {"msg": "move", "data": {"next": "0,0"}}
{"resp": "state_error", "data": {"error_msg": "Game over"}}
The data
key of the response is optional and clients must not depend
on it. The server may include additional helpful information in
data
, if it is feeling magnanimous.
If there is any other kind of error, the server responds with a
generic error
:
> {"msg": "flibbertigibbet"}
{"resp": "error", "data": {"error_msg": "wat"}}
Clients which do not send a message to the server within 10 seconds of the response will be timed out.
Clients which do not send a move
message to the server within 15
seconds of the last match_start
or match_play
message will be
timed out (to prevent extending turns by continuously asking for the
static state of the board).
The server may send error messages to clients it is about to time out. However, it is valid for the server to simply close the socket.
All messages generated by the server for a given match must have the same match id. This is not important to "player" clients (because they only handle one match at a time) but is intended to allow "spectator" clients to demux matches straightforwardly.
Match ids must be as unique as reasonably possible (any guid algorithm should be sufficient).
A player session can be in one of the following states (valid messages listed):
PRE_VERSION
versions
version
PRE_REGISTER
register
MATCH_WAIT
ready
MATCH_PLAYING
move
edges
cells
tiling
match_status
MATCH_FINISHED
outcome
match_status
ready
A spectator session can be in one of the following states (valid messages listed):
PRE_VERSION
versions
version
PRE_REGISTER
register
WATCHING
watch
outcome
edges
cells
tiling
match_status