135 lines
7.7 KiB
Markdown
135 lines
7.7 KiB
Markdown
# 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.
|