- Lay out possible errors (on advisory basis) and possible causes of death.
- Specify resolution to "In that case those players despite dying also win."
By David Morris and Michael Mokrysz.
All communication takes the form of messages between client and server, as plain text over a TCP socket.
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 as delimited by
\n
. Newlines embedded in the JSON are are not permitted. Carriage returns
should be handled so as to allow \r\n
delimiters.
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
).
Player sessions are stateful and will experience a single game at a time, with each TCP socket experiencing a sequence of games. Spectators will see the data for all ongoing games on their TCP socket.
All games are played on a grid which is a tiling of cells. Clients can implement the grids, or ease implementation by retrieving a graph representation.
Clients are be told the current grid type during the initial handshake:
Server: {"msg": "welcome", "data": {…, "grid": {"kind": "hexagon", "data": {radius": 15}}}}
If clients wish to be sent a graph representation they must ask now. It will be sent as an adjacency relation.
Client: {"msg": "describe_grid"}
Server: {"msg": "grid_graph", "data": {"edges": [[{"x": 0, "y": 0}, {"x": 0, "y": 1}], [{"x": 0, "y": 1}, {"x": 1, "y": 1}], [{"x": 1, "y": 1},{"x": 1, "y": 0}], [{"x": 1, "y": 0}, {"x": 0, "y": 0}], [{"x": 0, "y": 1}, {"x": 0, "y": 0}], [{"x": 1, "y": 1}, {"x": 0, "y": 1}], [{"x": 1, "y": 0},"1,1"], [{"x": 0, "y": 0},{"x": 1, "y": 0}]]]]}}
The cells will take varying representations depending upon the underlying grid type but all will be dictionaries from strings to 64-bit signed integers.
In general, clients should be able to navigate the graph without regard to the spatial positioning of cells. But it is recommended clients implement the underlying grid systems. A contextless graph has implications such as requiring pathfinding for all movement planning.
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 TCP socket; no guarantees are made between sockets.
{"game": {"grid": {"radius": 25}, "players": ["46bit", "46bit_"], "uuid": "bb117ad4-d26b-49ac-8cd1-2d30572e6f41"}, "game_id": "bb117ad4-d26b-49ac-8cd1-2d30572e6f41"}
Dynamic state consists of the positions of the snakes and any food on the board:
{"turn": {"casualties": {}, "eaten":{}, "food": [{"x": -24, "y": 3}], "snakes": {"46bit": {"segments": [{"x": -6, "y": -17}]}, "46bit_": {"segments": [{"x": 11,"y": -1}]}}, "turn_number": 0}, "game_id": "bb117ad4-d26b-49ac-8cd1-2d30572e6f41"}
turn.casualties
should give a dictionary from player names to causes of death.
This must only consist of players who died in the previous turn. If clients wish
to track death information they should aggregate this information themselves.
turn.eaten
should give a dictionary from player names to vectors where food
was consumed in the previous turn.
turn.food
must be a list of cells containing food. turn.snakes
must be a
dictionary from living player names to living snakes.
Each snake is specified as a dictionary with a list of cells, e.g.:
{"segments": [{"x": 0, "y": 0}, {"x": 0, "y": 1}]}
The first element of the segments list is the head of the snake.
Subsequent extensions may introduce additional kinds of dynamic state.
?? Food ??
?? Growth ??
?? Fun ??
?? Profit ??
Sessions start with version negotation. The server tells the client which protocol version and sirpent version it is running. Clients may close the socket if they don't support that or else attempt to continue.
Server: {"msg": "version", "data": {"protocol": "0.3", "sirpent": "0.2.0"}}
The client then registers with the server, offering a preferred name:
Client: {"msg": "register", "data": {"desired_name": "46bit", "kind": "player"}}
Names cannot contain a literal \n
but may be arbitrary valid unicode.
The server replies with a welcome message.
Server: {"msg": "welcome", "data": {"grid": {"radius": 25}, "name": "46bit", "timeout": {"nanos": 0,"secs": 5}}}
It may offer 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 connections from the same client).
As discussed it will inform of a particular grid being used. Clients may be timed out if an response takes longer than allowed and those details should be communicated here.
If a client does not want to continue it should close the socket.
As discussed above the client can ask for an adjacency matrix of the grid being used. Unless the client needs this it should proceed to the next stage.
Client: {"msg": "describe_grid"}
Server: {"msg": "grid_graph", "data": {"edges": [[{"x": 0, "y": 0}, {"x": 0, "y": 1}], [{"x": 0, "y": 1}, {"x": 1, "y": 1}], [{"x": 1, "y": 1},{"x": 1, "y": 0}], [{"x": 1, "y": 0}, {"x": 0, "y": 0}], [{"x": 0, "y": 1}, {"x": 0, "y": 0}], [{"x": 1, "y": 1}, {"x": 0, "y": 1}], [{"x": 1, "y": 0},"1,1"], [{"x": 0, "y": 0},{"x": 1, "y": 0}]]]]}}
The client then sends a ready
message to indicate they are ready to join a
game.
Client: {"msg": "ready"}
The server will decide when to play a new game. It can choose its own criteria although generally this would be based upon having a sensible number of players connected.
Once a new game is started, the server will send a game_start
message
describing the static state of the game. This will be sent to all participating
clients, both players and spectators:
Server: {"msg": "game_start", "data": {"game": {"grid": {"radius": 25}, "players": ["46bit", "46bit_"], "id": "bb117ad4-d26b-49ac-8cd1-2d30572e6f41"}, "game_id": "bb117ad4-d26b-49ac-8cd1-2d30572e6f41"}}
Each game has a UUID. In game_start
this is awkwardly included twice to be
consistent with where other within-game messages expect it in data.game_id
.
Immediately after game_start
or moves being made, the server will send a
turn
message. This contains the current dynamic state of the game. It will
be sent to all participating clients, both players and spectators:
Server: {"msg": "turn", "data": {"turn": {"casualties": {"46bit_": …}, "eaten":{}, "food": [{"x": -24, "y": 3}], "snakes": {"46bit": {"segments": [{"x": -6, "y": -17}]}, "46bit_": {"segments": [{"x": 11,"y": -1}]}}, "turn_number": 0}, "game_id": "bb117ad4-d26b-49ac-8cd1-2d30572e6f41"}}
Turn numbers must be zero-indexed and incremented each turn.
If clients wish to track death and eating information they should aggregate this information themselves.
Living players must then send a move
message indicating the direction in
which they wish to move the head of their snake:
Client: {"msg": "move", "data": {"direction": "north"}}
If describe_grid
is implemented a player must be able to specify the cell the
head should move to instead. This is useful if working with a graph
representation:
Client: {"msg": "move", "data": {"next": {"x": 5, "y": 6}}}
Clients must only send one of these representations. They must be valid.
The server will compute the resulting turn. It will determine which snakes have collided with each other, with the edge of the grid, or have errored in some fashion (e.g., no move received inside the timeout).
Before the next turn
message each newly dead player will receive a died
message giving their cause of death:
Server: {"msg": "died", "data": {"cause_of_death": …, "game_id": "bb117ad4-d26b-49ac-8cd1-2d30572e6f41"}}
The receipt of a died
message should not result in a terminated TCP socket and
should not stop information on the ongoing game. Dead players should be provided
with the game action similarly to spectators. They should continue to receive
turn
messages but must not send move
messages.
If a client does not support this then it should close the TCP socket upon death and immediately reconnect to wait for the next game. Servers should have a pause between games sufficient to allow reconnections to happen.
The server chooses victory criteria. Generally this is when only one player is left standing or when all players die. The latter case could happen when N remaining snakes die in the same turn. In that case those players (despite dying) also win.
Winning players will be sent a won
message:
Server: {"msg": "won", "data": {"game_id": "bb117ad4-d26b-49ac-8cd1-2d30572e6f41"}}
All participating clients, both players and spectators, will then receive a
game_over
message. This also contains the final state.
Server: {"msg": "game_over", "data": {"winners": ["46bit", "Taneb"], "turn": {"casualties": {"Taneb": …}, "eaten": {}, "food": [{"x": -24, "y": 3}], "snakes": {"46bit": {"segments": [{"x": -6, "y": -17}]}, "46bit_": {"segments": [{"x": 11,"y": -1}]}}, "turn_number": 100}, "game_id": "bb117ad4-d26b-49ac-8cd1-2d30572e6f41"}}
Clients can also register as spectators. The expected use case for this is scoreboards and visualizers. Spectators must send no messages after the initial handshake.
Server: {"msg": "version", "data": {"protocol": "0.3", "sirpent": "0.2.0"}}
Client: {"msg": "register", "data": {"desired_name": "visualiser", "kind": "spectator"}}
Server: {"msg": "welcome", "data": {"grid": {"radius": 25}, "name": "spectator", "timeout": {"nanos": 0,"secs": 5}}}
Client: {"msg": "ready"}
Server: {"msg": "game_start", "data": {"game": {"grid": {"radius": 25}, "players": ["46bit", "46bit_"], "uuid": "bb117ad4-d26b-49ac-8cd1-2d30572e6f41"}, "game_id": "bb117ad4-d26b-49ac-8cd1-2d30572e6f41"}}
Server: {"msg": "turn", "data": {"turn": {"casualties": {}, "eaten":{}, "food": [{"x": -24, "y": 3}], "snakes": {"46bit": {"segments": [{"x": -6, "y": -17}]}, "46bit_": {"segments": [{"x": 11,"y": -1}]}}, "turn_number": 0}, "game_id": "bb117ad4-d26b-49ac-8cd1-2d30572e6f41"}}
[…]
Server: {"msg": "game_over", "data": {"winners": ["46bit", "Taneb"], "turn": {"casualties": {"Taneb": …}, "eaten": {}, "food": [{"x": -24, "y": 3}], "snakes": {"46bit": {"segments": [{"x": -6, "y": -17}]}, "46bit_": {"segments": [{"x": 11,"y": -1}]}}, "turn_number": 100}, "game_id": "bb117ad4-d26b-49ac-8cd1-2d30572e6f41"}}
Died and won messages must not be relayed to spectators. Spectators should infer
deaths from the data.turn.casualties
field on each turn
or game_over
message. Similarly winners should be inferred from the data.winners
field on
each game_over
message.
Servers may play multiple games simultaneously. Spectators may be sent multiple
games at once. In this case the data.game_id
field can be used to determine
which game a message refers to.
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": {"direction": "atotallyinvaliddirection"}}
{"resp": "move_error", "data": {"error_msg": "Invalid move"}}
If a client sends a message which is invalid in the current session
state (e.g., sending a move after a game has finished) the server
responds with state_error
:
> {"msg": "move", "data": {"next": "0,0"}}
{"resp": "state_error", "data": {"error_msg": "Game over"}}
If there is any other kind of error, the server responds with a
generic error
:
> {"msg": "flibbertigibbet"}
{"resp": "error", "data": {"error_msg": "wat"}}
For all errors 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.
Servers may give clients a fixed time to send messages. If a client fails to respond quick enough the connection may be closed. If closing the connection for this an error message indicating such should be sent.
In the case of player moves timing out the server should handle this without terminating the client, killing them for the missing move but retaining their connection as for any other dead player.
All messages generated by the server for a given game must have the same game
id stored in data.game_id
. This is not important to Player clients
(because they only handle one game at a time) but is intended to allow
Spectator clients to demux games straightforwardly.
Game ids must be as unique as reasonably possible (any guid algorithm should be sufficient). Servers may use a random (V4) UUID as a sensible approach.
A player session can be in one of the following states (valid messages listed):
PRE_VERSION
version
PRE_REGISTER
register
PRE_WELCOME
welcome
PRE_READY
describe_grid
ready
READY_WAIT
game_start
GAME_PLAYING
turn
TURN_MOVING
move
TURN_DONE
died
won
game_over
A spectator session can be in one of the following states (valid messages listed):
PRE_VERSION
version
PRE_REGISTER
register
PRE_WELCOME
welcome
PRE_READY
describe_grid
ready
READY_WAIT
game_start
GAME_PLAYING
turn
game_over