feat: lock production decisions and resource budgets
This commit is contained in:
@@ -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 0–9 |
|
||||
| `name` | 1–20 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.
|
||||
+11
-11
@@ -33,17 +33,17 @@ carrier PCB model or its LED and pin routing.
|
||||
|
||||
## Current repository baseline (not hardware confirmation)
|
||||
|
||||
| Item | Observed value | Status |
|
||||
|---|---|---|
|
||||
| PlatformIO Core | 6.1.19 | Installed locally |
|
||||
| PlatformIO platform | `platformio/espressif32` 7.0.1 | Installed locally; `platformio.ini` is not version-pinned |
|
||||
| Framework | ESP-IDF 6.0.1 | Current project setting conflicts with the Arduino-first MVP baseline |
|
||||
| Arduino Core | Not installed | Cannot be pinned and verified against this board yet |
|
||||
| PlatformIO board | `esp32-c6-devkitm-1` | Unverified candidate, not evidence of the physical model |
|
||||
| Board-profile flash size | 4 MB | Unverified; profile metadata only |
|
||||
| Detected physical flash size | 4 MB | Confirmed by `esptool.py flash_id` |
|
||||
| Generated SDK flash size | 2 MB | Conflicts with the detected hardware and board profile |
|
||||
| Generated partition table | Single application, no LittleFS | Does not meet Milestone 000 |
|
||||
| Item | Observed value | Status |
|
||||
| ---------------------------- | ------------------------------- | --------------------------------------------------------------------- |
|
||||
| PlatformIO Core | 6.1.19 | Installed locally |
|
||||
| PlatformIO platform | `platformio/espressif32` 7.0.1 | Installed locally; `platformio.ini` is not version-pinned |
|
||||
| Framework | ESP-IDF 6.0.1 | Current project setting conflicts with the Arduino-first MVP baseline |
|
||||
| Arduino Core | Not installed | Cannot be pinned and verified against this board yet |
|
||||
| PlatformIO board | `esp32-c6-devkitm-1` | Unverified candidate, not evidence of the physical model |
|
||||
| Board-profile flash size | 4 MB | Unverified; profile metadata only |
|
||||
| Detected physical flash size | 4 MB | Confirmed by `esptool.py flash_id` |
|
||||
| Generated SDK flash size | 2 MB | Conflicts with the detected hardware and board profile |
|
||||
| Generated partition table | Single application, no LittleFS | Does not meet Milestone 000 |
|
||||
|
||||
The current configuration is not yet a reproducible baseline: its generated
|
||||
SDK configuration must be changed from 2 MB to the confirmed 4 MB and its
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# Production decisions
|
||||
|
||||
This document is the binding implementation contract for the MVP. It resolves
|
||||
the choices left open in `MVP.md`; future code must not widen these limits
|
||||
without updating this document and `RESOURCE_BUDGET.md`.
|
||||
|
||||
## Platform and ownership
|
||||
|
||||
- Target: ESP32-C6FH4 (4 MB flash, no PSRAM assumed), custom 2 MiB app and
|
||||
1,984 KiB LittleFS partitions.
|
||||
- Stack: PlatformIO Core 6.1.19, `espressif32` 7.0.1, ESP-IDF 6.0.1,
|
||||
built-in `esp_http_server` WebSocket support, and `esp_littlefs` 1.20.4.
|
||||
- The ESP32 is authoritative. HTTP and WebSocket callbacks only validate and
|
||||
enqueue commands; one application task owns game, session, and statistics
|
||||
mutation.
|
||||
- Internal coordinates are unsigned `x` and `y` in `[0, 9]`. The browser
|
||||
renders Russian column labels; it never sends them.
|
||||
|
||||
## Fixed game rules
|
||||
|
||||
- Board size is 10 by 10. Each side has ships of lengths `4, 3, 3, 2, 2, 2,
|
||||
1, 1, 1, 1`, placed randomly by the server without touching, including
|
||||
diagonally.
|
||||
- A hit retains the turn. A miss changes it. Sinking a ship marks its
|
||||
surrounding cells as misses. A repeated shot is rejected without state
|
||||
change. First turn and each fleet are independently random.
|
||||
- Modes are `HUMAN_VS_HUMAN` and `HUMAN_VS_BOT`. The bot is `ESP32`; it uses
|
||||
only previously visible shot results and acts after a bounded 500–900 ms
|
||||
server timer.
|
||||
- Phases are `LOBBY`, `PREPARING`, `IN_PROGRESS`, `FINISHED`, and
|
||||
`REMATCH_WAIT`. Every accepted state change increments `version`.
|
||||
|
||||
## Sessions and capacity
|
||||
|
||||
- Roles are `PLAYER_1`, `PLAYER_2`, and `SPECTATOR`; capacity is two players
|
||||
and eight spectators. New clients become spectators when player slots are
|
||||
occupied, subject to the spectator limit.
|
||||
- Display names are 1–20 Unicode scalar values after removing controls and
|
||||
markup. The server stores a bounded UTF-8 encoding of at most 80 bytes and
|
||||
escapes it before HTML rendering.
|
||||
- A session token is 16 cryptographically random bytes, transported as 32
|
||||
lowercase hexadecimal characters. It is opaque, never logged, and remains
|
||||
valid only until board reboot or explicit slot release.
|
||||
- Active player sessions survive disconnects. Spectator disconnects free their
|
||||
slot immediately. A player may resume only with the same token; a resumed
|
||||
session receives a complete role-safe snapshot.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
1. `join` assigns player 1 if vacant and requested, then player 2 only in the
|
||||
two-player mode; otherwise it assigns spectator. `resume` never changes a
|
||||
role.
|
||||
2. Player 1 may configure a mode only in `LOBBY`. Switching to bot reserves
|
||||
player 2 as `ESP32`; switching back requires no human player 2 conflict.
|
||||
3. Player 1 may start only when player 2 is present in human mode, or bot mode
|
||||
is selected. `PREPARING` is internal and advances atomically to
|
||||
`IN_PROGRESS` after both fleets validate.
|
||||
4. A player disconnect does not abort a game. The game waits for that player;
|
||||
the bot continues only when it is the bot's turn. Player 1 may issue the
|
||||
explicit `abort` command while a human opponent is disconnected, returning
|
||||
to `LOBBY` and incrementing `version`.
|
||||
5. Destroying all ten opponent ships transitions to `FINISHED`, records match
|
||||
and cumulative statistics, reveals both boards, and rejects shots.
|
||||
6. In `FINISHED`, a player may confirm `rematch`. Human mode requires both
|
||||
player confirmations; bot mode requires player 1 only. The server enters
|
||||
`REMATCH_WAIT` until the required confirmations exist, then creates a new
|
||||
game ID and returns to `PREPARING`. Names, roles, and cumulative statistics
|
||||
remain; per-match statistics reset.
|
||||
7. Board reboot clears sessions, game, and all statistics. Wi-Fi loss does not
|
||||
mutate them; clients use HTTP polling while WebSocket reconnects.
|
||||
|
||||
## Visibility and transport
|
||||
|
||||
- Player 1 receives its complete board and only known opponent shot results;
|
||||
player 2 is symmetric. Spectators see known shot results from both boards.
|
||||
Unhit ships are replaced by `0` in every unauthorized view.
|
||||
- `FINISHED` is the sole phase that exposes full boards to every role.
|
||||
- HTTP commands include the token in the JSON body. WebSocket authentication
|
||||
is the first `hello` message; the token is never placed in a URL or log.
|
||||
- WebSocket reconnect delay is 1, 2, 5, then 10 seconds. HTTP polls a complete
|
||||
snapshot every two seconds only while WebSocket is unavailable. Any skipped
|
||||
version triggers a full snapshot request.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Resource budget
|
||||
|
||||
## Measured feasibility baseline
|
||||
|
||||
Milestone 004's accepted ESP32-C6 run completed 20 games of 200 updates with
|
||||
two players and eight spectators. Its measured values were 1,000,496 B flash,
|
||||
38,204 B RAM, 249,616 B minimum free heap, 320 B largest state message, 154 us
|
||||
maximum generation time, and 5,283 us maximum asynchronous delivery-enqueue
|
||||
time. The application partition is 2,097,152 B and LittleFS is 2,031,616 B.
|
||||
|
||||
## Hard production limits
|
||||
|
||||
| Resource | Limit | Basis |
|
||||
| ---------------------------- | ----------: | -------------------------------------------------------------------------- |
|
||||
| Firmware image | 1,500,000 B | M004 fixed threshold, leaving 597,152 B app-partition reserve |
|
||||
| LittleFS image | 250,000 B | M004 fixed threshold, leaving 1,781,616 B filesystem reserve |
|
||||
| Minimum free heap | 96,000 B | M004 fixed threshold; observed minimum was 249,616 B |
|
||||
| Largest role-safe state JSON | 512 B | Tested fixed serialization cap; production schema is compact strings |
|
||||
| HTTP JSON request body | 192 B | Largest defined command (`join`) fits within this bound |
|
||||
| WebSocket incoming frame | 192 B | `hello` is the largest defined incoming frame |
|
||||
| HTTP request target/query | 128 B | `/api/state?version=4294967295` is below this bound |
|
||||
| State generation | 100,000 us | M004 fixed threshold; observed maximum was 154 us |
|
||||
| State delivery enqueue | 100,000 us | M004 fixed threshold; observed maximum was 5,283 us |
|
||||
| Pending application commands | 16 | Fixed bounded queue; excess requests return `SERVER_BUSY` |
|
||||
| Sessions | 10 | Two players and eight spectators |
|
||||
| Open HTTP sockets | 12 | Ten clients plus two polling/transport headroom; LWIP is configured for 16 |
|
||||
|
||||
No handler may allocate a per-client complete JSON state. It serializes one
|
||||
bounded snapshot at a time into the 512-byte transport buffer.
|
||||
|
||||
## Per-milestone budget gates
|
||||
|
||||
| Milestone | Firmware ceiling | LittleFS ceiling | Heap floor | Required check |
|
||||
|---|---:|---:|---:|---|
|
||||
| 006 architecture | 1,080,000 B | 250,000 B | 220,000 B | clean build and host tests |
|
||||
| 007 game core | 1,180,000 B | 250,000 B | 190,000 B | fleet and rules tests |
|
||||
| 008 sessions/API | 1,300,000 B | 250,000 B | 150,000 B | role filtering and malformed input |
|
||||
| 009 transport/UI | 1,420,000 B | 250,000 B | 115,000 B | two players/eight spectators |
|
||||
| MVP completion | 1,500,000 B | 250,000 B | 96,000 B | repeated on-board game test |
|
||||
|
||||
Each gate is a maximum permitted consumption or minimum required remaining
|
||||
heap. A missed gate blocks the next milestone pending a documented decision.
|
||||
Reference in New Issue
Block a user