feat: lock production decisions and resource budgets

This commit is contained in:
2026-08-28 22:21:36 +03:00
parent 63e33510b3
commit 4ff1718307
6 changed files with 680 additions and 15 deletions
+91
View File
@@ -0,0 +1,91 @@
# API contract v1
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` own ship, `2` miss, `3` 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`,
`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, 192 B |
| `GET /api/health` | 128 B target | diagnostics without secrets, 320 B |
| `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/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 |
| `GET /api/state?version=N` | 128 B target | one role-safe state, 512 B |
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.
## Role-safe state event
The HTTP state response and WebSocket `state` event use this single 512-byte
maximum schema:
```json
{"type":"state","version":17,"gameId":4,"phase":"in_progress","mode":"human","viewer":"player1","turn":"player2","boards":["000...100 cells...","000...100 cells..."],"wins":[0,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`. No other event contains a board.
## 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 512 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.