# API contract v1 Machine-readable HTTP documentation: [openapi.yaml](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: ```json {"ok":true,"version":17,"gameId":4} ``` Failure (maximum 160 encoded bytes): ```json {"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 | | `GET /api/network/status` | 128 B target | unauthenticated state/message; never includes SSID/password | | `GET /api/network/scan` | 128 B target | unauthenticated bounded scan state and up to 12 SSIDs | | `POST /api/network/validate` | 160 B body | unauthenticated `{ssid,password}` validation; password is never returned | | `POST /api/network/delete` | 0 B body | unauthenticated deletion of the saved profile | | `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. Network configuration is deliberately unauthenticated so it works from the open fallback AP and the regular local address. Any reachable client can change or delete the saved profile; the open AP and local HTTP do not provide password confidentiality. Network responses and diagnostics never include the password. ## Role-safe state event The HTTP state response and WebSocket `state` event use this single 768-byte maximum schema: ```json {"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. ```json {"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: ```json {"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.