Files
battleship/docs/API_CONTRACT.md
T
2026-08-30 23:42:26 +03:00

6.8 KiB
Raw Blame History

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 09
name 120 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.

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.