6.9 KiB
API contract v1
Machine-readable HTTP documentation: openapi.yaml.
All JSON is UTF-8 and uses Content-Type: application/json. API responses set
Cache-Control: no-store. A request exceeding its route limit is rejected
before parsing with PAYLOAD_TOO_LARGE; malformed JSON is MALFORMED_JSON.
All numeric fields are decimal JSON integers, never strings.
Common values
| Type | Values / bound |
|---|---|
role |
player1, player2, spectator |
mode |
human, bot |
phase |
lobby, preparing, in_progress, finished, rematch_wait |
token |
exactly 32 lowercase hexadecimal characters |
gameId, version |
unsigned 32-bit integer |
x, y |
integer 0–9 |
name |
1–20 scalar values, at most 80 UTF-8 bytes |
board |
exactly 100 ASCII cells: 0 unknown/water, 1 revealed ship, 2 miss, 3 hit, 4 sunk hit |
Every state-changing request has token and gameId; they must precede any
mutation validation. A stale gameId is rejected as STALE_GAME.
Response envelope
Success:
{"ok":true,"version":17,"gameId":4}
Failure (maximum 160 encoded bytes):
{"ok":false,"code":"NOT_YOUR_TURN","message":"Сейчас ход соперника","version":17}
code is one of MALFORMED_JSON, PAYLOAD_TOO_LARGE, INVALID_NAME,
INVALID_ROLE, INVALID_MODE, INVALID_COORDINATE, UNAUTHORIZED, SESSION_INVALIDATED,
NO_PLAYER_SLOT, NO_SPECTATOR_SLOT, FORBIDDEN_ROLE, WRONG_PHASE,
NOT_YOUR_TURN, CELL_ALREADY_SHOT, STALE_GAME, or SERVER_BUSY.
message is Russian and at most 80 UTF-8 bytes.
HTTP routes
| Route | Maximum request | Response / maximum |
|---|---|---|
GET /api/info |
128 B target | public device/slot state and names, 384 B |
GET /api/health |
128 B target | diagnostics without secrets, 320 B; includes reset reason |
POST /api/session/join |
192 B body | {name,requestedRole}; token and role, 192 B |
POST /api/session/resume |
96 B body | {token}; role and state metadata, 192 B |
POST /api/session/leave |
80 B body | {token,gameId}; releases only that session |
POST /api/session/profile-reset |
80 B body | {token,gameId}; server release for local profile reset |
POST /api/game/config |
96 B body | {token,gameId,mode}; common envelope |
POST /api/game/start |
80 B body | {token,gameId}; common envelope |
POST /api/game/shot |
96 B body | {token,gameId,x,y}; common envelope |
POST /api/game/rematch |
80 B body | {token,gameId}; common envelope |
POST /api/game/abort |
80 B body | {token,gameId}; common envelope |
POST /api/game/reset |
80 B body | {token,gameId}; player-only full RAM reset |
GET /api/state?version=N |
128 B target | one role-safe state, 768 B |
GET /api/statistics |
128 B target | role and bounded match/cumulative counters |
The session token is in each POST body. For GET /api/state, it is supplied
in X-Session-Token; absence creates a spectator-safe view. It is never a URL
parameter.
For the primary “Play” action, requestedRole: "player" atomically assigns
the first available player slot (player1, then player2). This avoids a
client-side availability race; the response and every authorized state snapshot
contain the assigned concrete role.
leave and profile-reset release only the requesting session; profile data is
cleared by the browser in Milestone 022. A player leaving an active match aborts
that match and returns remaining valid players to the lobby. game/reset is
available only to an authenticated player and atomically invalidates every
session, clears match and cumulative statistics, and starts a fresh game
generation. Successful recovery responses contain resetReason (session_left,
profile_reset, or game_reset) and generation. Invalidated-token state
polling returns SESSION_INVALIDATED with the same bounded recovery metadata.
The recovery generation and gameId make delayed commands stale; retrying a
completed recovery is harmless and cannot mutate a newly registered session.
Role-safe state event
The HTTP state response and WebSocket state event use this single 768-byte
maximum schema:
{"type":"state","version":17,"gameId":4,"phase":"in_progress","mode":"human","viewer":"player1","turn":"player2","players":["Алиса","Борис"],"boards":["000...100 cells...","000...100 cells..."],"wins":[0,0],"winner":null,"statistics":[[3,2,1,1],[4,1,3,0]]}
boards[0] belongs to player 1 and boards[1] to player 2. The presenter
replaces every unauthorized unhit ship with 0. In finished, both boards may
contain 1. winner is null until finished, then player index 0 or 1.
players contains validated display names for occupied slots (an empty string for a free slot). Each compact statistics tuple is [shots,hits,misses,shipsSunk]. No other event
contains a board.
Statistics response
GET /api/statistics accepts the same optional X-Session-Token as state and
returns no board data. match uses [shots,hits,misses,shipsSunk] per side;
cumulative uses [games,wins,losses,shipsSunk,shots,hits,misses] per side.
{"ok":true,"viewer":"player1","gameId":4,"match":[[3,2,1,1],[4,1,3,0]],"cumulative":[[2,1,1,10,30,15,15],[2,1,1,8,28,14,14]]}
WebSocket
Endpoint: GET /ws; all incoming frames are text JSON, at most 192 B. The
first frame must arrive within 5 seconds:
{"type":"hello","token":"32-lowercase-hex-characters","version":17}
It receives a state snapshot. Supported inbound frames are hello (96 B),
ping (16 B), config (96 B), start (80 B), shot (96 B), rematch
(80 B), and abort (80 B). Their fields exactly match the corresponding HTTP
commands. Outbound state is at most 768 B; error uses the common 160-byte
failure envelope; pong is 16 B. Commands are idempotent when the same
gameId, version, and command payload are retried: the server returns the
current state rather than applying the action twice.