Skip to content

Instantly share code, notes, and snippets.

@qlkzy

qlkzy/sirpent.md Secret

Last active January 13, 2017 22:01
Show Gist options
  • Save qlkzy/c62fb75c1d7b11d86b48ffe3ea66ed01 to your computer and use it in GitHub Desktop.
Save qlkzy/c62fb75c1d7b11d86b48ffe3ea66ed01 to your computer and use it in GitHub Desktop.

Protocol

General Considerations

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.

Board structure

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.

Dynamic and Static State

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.

Gameplay

?? Food ??

?? Growth ??

?? Fun ??

?? Profit ??

Session structure

Version negotiation

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}}

Name registration

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.

Starting matches

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"]}}

Board state

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.

Playing matches

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"}

Spectating

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.

Errors

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"}}

Timeouts

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.

Match IDs

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).

Protocol States

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment