diff --git a/PLANS.md b/PLANS.md index c5097c5..4227796 100644 --- a/PLANS.md +++ b/PLANS.md @@ -268,7 +268,7 @@ If all criteria pass, set this milestone to `DONE`, append its execution record, ## Milestone 004 — Prove MVP capacity and make the Go/No-Go decision -**Status:** `IN PROGRESS` +**Status:** `DONE` **Depends on:** Milestone 003 ### Objective @@ -325,3 +325,452 @@ Do not adjust a threshold after seeing the result unless the execution record co ### Completion action Set this milestone to `DONE` only for a documented **Go** decision. For any other decision, set it to `BLOCKED` and record the required scope or platform decision. Append every future milestone after this section without renumbering Milestones 000–004. + +### Execution record + +- Date: 2026-08-28 +- Board model and revision: ESP32-C6FH4 QFN32, revision v0.2; carrier board remains an unidentified SuperMini-style ESP32-C6 Mini. +- Toolchain and library versions: PlatformIO Core 6.1.19; `espressif32` 7.0.1; ESP-IDF 6.0.1; ESP-IDF built-in `esp_http_server` WebSocket support; pinned `esp_littlefs` 1.20.4. +- Result: PASS — **Go**. +- Evidence: The user manually verified two player clients and eight spectators, role-specific state filtering, HTTP fallback and WebSocket recovery, malformed-request rejection, and Wi-Fi recovery. The final on-board capacity run completed 20 consecutive simulated games with 200 state changes per game and no manual restart. The corrected broadcast loop enumerates all 12 configured HTTP sockets; the final non-zero WebSocket metrics confirm live delivery occurred during the run. +- Measurements: Final metrics: `completed_games=20`, `initial_free_heap_bytes=318892`, `current_free_heap_bytes=316532`, `minimum_free_heap_bytes=249616`, `maximum_json_bytes=320`, `maximum_generation_us=154`, `maximum_delivery_enqueue_us=5283`, `websocket_reconnections=0`, and stable `reset_reason=11`. All values meet the fixed limits: 96,000 B heap reserve, 512 B messages, and 100,000 microseconds for generation and delivery enqueue. The verified build used 38,204 / 327,680 B RAM (11.7%) and 1,000,496 / 2,097,152 B flash (47.7%). +- Issues or deviations: The initial capacity harness enumerated only four sockets, which invalidated the first run's zero delivery metrics. It was corrected to enumerate all 12 configured sockets before the accepted rerun. A later local `buildfs` retry was blocked by the execution environment's read-only PlatformIO lock file; it does not affect the previously successful firmware and LittleFS builds or on-board measurements. +- Next action: Milestone 005 is ready. Do not start it unless explicitly requested. + + + +## Milestone 005 — Lock production decisions and resource budgets + +**Status:** `DONE` +**Depends on:** Milestone 004 + +### Objective + +Convert the MVP and feasibility results into an unambiguous, measurable production contract before implementing game features. + +### Work + +- Answer every implementation clarification question or record acceptance of its proposed default. +- Recover the Milestone 004 measurements. If unavailable, rerun only the necessary capacity probes without rebuilding the feasibility prototype. +- Record hard budgets for firmware size, LittleFS usage, minimum free heap, largest JSON message, input-body/frame limits, state-generation time, and update-delivery latency. +- Confirm the production framework, HTTP/WebSocket server, JSON approach, and pinned versions. +- Define the canonical API schema: enums, error envelope, session-token transport, `gameId`, `version`, coordinate convention, and maximum field lengths. +- Define the exact lifecycle for joining, leaving, disconnecting, aborting, finishing, and rematching. +- Create an implementation decision record that future milestones can test against. + +### Deliverables + +- `docs/GAME_DECISIONS.md` containing all accepted gameplay and lifecycle decisions. +- `docs/RESOURCE_BUDGET.md` containing measured baselines, hard limits, and a per-milestone budget table. +- `docs/API_CONTRACT.md` containing bounded request, response, event, and error schemas. +- Updated pinned build dependencies with no floating versions. + +### Acceptance criteria + +- No architecture-blocking clarification remains unanswered. +- Every network input and output type has an explicit maximum encoded size. +- Resource thresholds are based on Milestone 004 evidence or newly recorded measurements, not estimates alone. +- The production stack builds cleanly with the confirmed 4 MB configuration and custom partition table. +- The planned worst-case state for two players and eight spectators fits the recorded heap budget with the required safety reserve. + +### Completion action + +If all criteria pass, set this milestone to `DONE`, append its execution record, and change Milestone 006 from `BLOCKED` to `READY`. + +### Execution record + +- Date: 2026-08-28 +- Board model and revision: ESP32-C6FH4 QFN32, revision v0.2; carrier board remains an unidentified SuperMini-style ESP32-C6 Mini. +- Toolchain and library versions: PlatformIO Core 6.1.19; `espressif32` 7.0.1; ESP-IDF 6.0.1; built-in `esp_http_server` WebSocket support; pinned `esp_littlefs` 1.20.4. +- Result: PASS. +- Evidence: Added `docs/GAME_DECISIONS.md`, `docs/RESOURCE_BUDGET.md`, and `docs/API_CONTRACT.md`. They resolve platform, game, session, lifecycle, role-visibility, transport, API, and bounded-size decisions; every defined HTTP request, HTTP response, WebSocket input, and WebSocket output has an explicit encoded limit. Production dependencies remain pinned in `platformio.ini`. `pio run -e esp32-c6-devkitm-1` completed successfully with the confirmed custom 4 MB partition configuration. +- Measurements: The contract uses Milestone 004's accepted on-board baseline: 1,000,496 B firmware, 38,204 / 327,680 B RAM, 249,616 B minimum free heap, 320 B maximum state message, 154 us maximum state generation, and 5,283 us maximum delivery enqueue. Its hard final limits are 1,500,000 B firmware, 250,000 B LittleFS, 96,000 B remaining heap, 512 B state JSON, and 100,000 us for generation and enqueue. +- Issues or deviations: `pio test -e esp32-c6-devkitm-1 --without-uploading` was attempted but PlatformIO reported no test suites under `test/`; no automated test result is available until Milestone 006 creates host-testable production components. +- Next action: Milestone 006 is ready. Do not start it unless explicitly requested. + +--- + +## Milestone 006 — Establish the bounded production architecture + +**Status:** `READY` +**Depends on:** Milestone 005 + +### Objective + +Create the production firmware and test structure without implementing game behavior, while enforcing resource boundaries at compile time and runtime. + +### Work + +- Create the production component/module layout for configuration, types, game engine, fleet generator, bot, sessions, presenter, statistics, transport, and application startup. +- Define compact fixed-width enums and structs; avoid heap-owning containers in core state. +- Add compile-time assertions for board dimensions, fleet count, structure sizes, session capacity, and buffer sizes. +- Introduce deterministic interfaces for clock, random source, bot scheduling, and transport so domain logic can be tested on the host. +- Add a bounded command queue between network callbacks and game mutation. +- Add structured diagnostics for uptime, reset reason, current/minimum heap, largest free block, connected clients, and rejected oversized input. +- Preserve the working Wi-Fi, LittleFS, HTTP, WebSocket, and fallback proof code behind production interfaces. + +### Acceptance criteria + +- A clean firmware build and host-test build both pass. +- Core state and queues have documented fixed maximum sizes. +- Network callbacks cannot mutate board arrays directly. +- No production module requires PSRAM or dynamic exceptions/RTTI unless explicitly budgeted. +- Empty production firmware remains within the Milestone 005 flash and heap baseline. + +### Completion action + +If all criteria pass, set this milestone to `DONE`, append its execution record, and change Milestone 007 from `BLOCKED` to `READY`. + +--- + +## Milestone 007 — Implement and exhaustively test the game domain core + +**Status:** `BLOCKED` +**Depends on:** Milestone 006 + +### Objective + +Implement all deterministic Battleship rules independently of Wi-Fi, HTTP, WebSocket, and the browser. + +### Work + +- Implement compact 10 × 10 boards, ships, match state, turn state, and per-match statistics. +- Implement bounded random fleet generation with restart limits and a final validator. +- Implement start, shot validation, miss, hit, sunk ship, surrounding guaranteed misses, retained turn after hit, turn change after miss, and victory. +- Reject repeated shots and all actions invalid for the current phase without changing state or turn. +- Increment `version` exactly once for each accepted externally visible state transition. +- Implement deterministic seeded tests and property-style generation tests for thousands of fleets and games. +- Add tests for every rule and error code listed in `MVP.md` that belongs to the domain layer. + +### Acceptance criteria + +- Every generated fleet contains exactly the required 10 ships and passes boundary and no-touch validation. +- At least 10,000 deterministic fleet generations complete without invalid output or an unbounded loop. +- Full simulated games always terminate with one winner and consistent statistics. +- Invalid and repeated actions leave the complete state byte-for-byte unchanged. +- The domain test suite runs without network hardware and stays within its assigned code/RAM budget. + +### Completion action + +If all criteria pass, set this milestone to `DONE`, append its execution record, and change Milestone 008 from `BLOCKED` to `READY`. + +--- + +## Milestone 008 — Implement the ESP32 opponent + +**Status:** `BLOCKED` +**Depends on:** Milestone 007 + +### Objective + +Implement a fair, bounded `hunt/target` opponent that uses only information available to a human player. + +### Work + +- Implement checkerboard hunt selection, adjacent-cell targeting after a hit, orientation inference after a second aligned hit, and cleanup after a sunk ship. +- Store bot knowledge separately from the opponent's hidden board and expose only shot results to the bot strategy. +- Guarantee that target selection terminates and never repeats a shot. +- Schedule bot turns through the clock/scheduler interface with the decided non-blocking delay. +- Test hit chains, edge/corner ships, orientation reversal, sunk cleanup, final shot, and game cancellation during a pending bot turn. + +### Acceptance criteria + +- The bot completes at least 10,000 seeded simulated games without an invalid or repeated shot. +- A test double proves the bot has no access to hidden ship cells. +- Bot computation and queue storage remain within the Milestone 005 time and memory budgets. +- No delay blocks the HTTP/WebSocket task or watchdog. + +### Completion action + +If all criteria pass, set this milestone to `DONE`, append its execution record, and change Milestone 009 from `BLOCKED` to `READY`. + +--- + +## Milestone 009 — Implement sessions, lobby, roles, and rematch lifecycle + +**Status:** `BLOCKED` +**Depends on:** Milestone 008 + +### Objective + +Implement the complete in-memory application state machine and bounded client/session lifecycle without transport-specific code. + +### Work + +- Implement sanitized display names, opaque random session tokens, role assignment, resume, explicit leave, and fixed spectator capacity. +- Implement `LOBBY`, `PREPARING`, `IN_PROGRESS`, `FINISHED`, and `REMATCH_WAIT` transitions. +- Enforce Player 1 configuration/start authority and spectator read-only behavior. +- Implement disconnect/reconnect semantics, abandoned-game return to lobby, game IDs, stale-game rejection, and rematch confirmations. +- Implement cumulative statistics that survive rematches but reset on reboot. +- Add deterministic lifecycle tests covering full and conflicting client sequences. + +### Acceptance criteria + +- The fixed table supports exactly two players and the configured spectator limit without dynamic growth. +- Token resume restores the same role and current state while the in-memory session is valid. +- Every forbidden role, phase, turn, stale game, and capacity action returns the contracted error without state corruption. +- Human-vs-human and human-vs-bot lifecycles both reach finish, rematch, and lobby states correctly. +- Session cleanup stays within bounded time and memory. + +### Completion action + +If all criteria pass, set this milestone to `DONE`, append its execution record, and change Milestone 010 from `BLOCKED` to `READY`. + +--- + +## Milestone 010 — Implement safe state presentation and bounded serialization + +**Status:** `BLOCKED` +**Depends on:** Milestone 009 + +### Objective + +Produce role-specific state that cannot reveal hidden ships and fits the measured message and heap budgets. + +### Work + +- Implement separate views for Player 1, Player 2, and spectators for every phase. +- Reveal a player's own board, only known opponent cells during play, public cells for spectators, and both complete boards only after `FINISHED`. +- Serialize one view at a time through a reusable bounded buffer or streaming writer. +- Escape and encode all user-controlled strings correctly. +- Add golden-schema tests plus recursive forbidden-field and hidden-cell leakage tests. +- Measure worst-case finished and in-progress payload sizes. + +### Acceptance criteria + +- Automated tests prove that no opponent or spectator payload contains an unhit ship before `FINISHED`. +- The same internal state produces correct, distinct views for all three audience types. +- Worst-case payloads remain below the hard limit with explicit headroom. +- Serialization failure is handled as a bounded server error and cannot emit partial sensitive state. +- No per-client full-state or full-JSON copy is retained after sending. + +### Completion action + +If all criteria pass, set this milestone to `DONE`, append its execution record, and change Milestone 011 from `BLOCKED` to `READY`. + +--- + +## Milestone 011 — Implement the production HTTP API + +**Status:** `BLOCKED` +**Depends on:** Milestone 010 + +### Objective + +Expose the contracted session and game commands through bounded, authenticated HTTP handlers. + +### Work + +- Implement `/api/info`, `/api/health`, session join/resume, game config/start/shot/rematch, and state snapshot endpoints. +- Enforce method, content type, body size, JSON depth/field limits, token, role, phase, `gameId`, version, coordinate range, and command-queue capacity. +- Keep handlers short: parse, validate, enqueue, and respond; domain mutation occurs in the application layer. +- Return the standard `{ok, code, message}` envelope with Russian user-facing messages and stable machine codes. +- Ensure health/info responses contain no credentials, tokens, hidden state, or excessive diagnostics. +- Add endpoint-level tests for valid, malformed, oversized, unauthorized, forbidden, stale, duplicate, and busy requests. + +### Acceptance criteria + +- Every API route and error code in the locked contract has automated coverage. +- Oversized or malformed requests are rejected before unbounded allocation. +- Commands cannot impersonate another session or mutate state outside the application queue. +- Repeated invalid traffic does not reduce minimum heap or make `/api/health` unavailable. +- API latency and payload sizes remain inside the Milestone 005 budgets. + +### Completion action + +If all criteria pass, set this milestone to `DONE`, append its execution record, and change Milestone 012 from `BLOCKED` to `READY`. + +--- + +## Milestone 012 — Implement WebSocket synchronization and HTTP recovery + +**Status:** `BLOCKED` +**Depends on:** Milestone 011 + +### Objective + +Deliver immediate personalized updates while preserving the proven HTTP polling fallback and bounded memory behavior. + +### Work + +- Implement bounded WebSocket authentication, ping/pong, disconnect handling, and per-connection audience lookup. +- Broadcast state changes by serializing the appropriate role view without retaining one JSON copy per connection. +- Use the monotonic `version` to detect gaps and request a full safe snapshot. +- Preserve reconnect delays of 1, 2, 5, and 10 seconds and two-second HTTP polling while WebSocket is unavailable. +- Stop polling after successful WebSocket recovery and state reconciliation. +- Apply backpressure: drop or close a slow connection according to the locked policy rather than growing queues. + +### Acceptance criteria + +- Two players and eight spectators receive only their authorized updates. +- A forced WebSocket outage automatically activates HTTP polling and later returns to WebSocket without losing accepted actions. +- Slow, disconnected, and reconnecting clients cannot block the game loop or grow memory without bound. +- Version-gap tests recover through a full safe snapshot. +- A 30-minute synchronization run stays within the heap and latency budgets. + +### Completion action + +If all criteria pass, set this milestone to `DONE`, append its execution record, and change Milestone 013 from `BLOCKED` to `READY`. + +--- + +## Milestone 013 — Build the Russian responsive web interface + +**Status:** `BLOCKED` +**Depends on:** Milestone 012 + +### Objective + +Implement the complete phone-first interface as small, dependency-free static assets served from LittleFS. + +### Work + +- Implement connection, lobby, game, spectator, result, reconnecting, and error states in vanilla HTML/CSS/JavaScript. +- Render 10 × 10 square-cell boards with Cyrillic coordinates, accessible symbols for water, ship, miss, hit, sunk, and selected target. +- Implement the decided shot-confirmation interaction and disable controls whenever the server state does not permit an action. +- Show one board at a time on narrow phones and two boards side-by-side on sufficiently wide tablets. +- Persist name and session token in `localStorage`; resume safely after reload. +- Implement WebSocket reconnect, HTTP fallback, stale-version recovery, and visible connection status. +- Minify and gzip assets at build time and serve correct MIME, content encoding, cache, and no-cache headers. + +### Acceptance criteria + +- Every screen and message required by `MVP.md` is available in Russian. +- The interface is usable on a narrow phone and a tablet in portrait and landscape orientations. +- Ship, miss, hit, sunk, and selection states are distinguishable without color alone. +- No hidden ship data, token, or credential is present in static assets or browser logs. +- Compressed assets and browser runtime memory remain within the Milestone 005 budgets. +- The application works with all network access disabled except the local ESP32 address. + +### Completion action + +If all criteria pass, set this milestone to `DONE`, append its execution record, and change Milestone 014 from `BLOCKED` to `READY`. + +--- + +## Milestone 014 — Complete human-vs-human gameplay end to end + +**Status:** `BLOCKED` +**Depends on:** Milestone 013 + +### Objective + +Integrate and prove the complete two-device human-vs-human journey before enabling bot-specific flows. + +### Work + +- Exercise join, mode selection, Player 2 arrival, start, random first turn, full game, finish, revealed boards, rematch, and return to lobby. +- Test refresh and token resume for both players in every phase. +- Test disconnect/reconnect during each player's turn and while waiting for rematch. +- Connect spectators before and during the game and verify read-only behavior. +- Compare server statistics and every rendered board after each shot in a deterministic scripted game. + +### Acceptance criteria + +- Two physical client devices can complete a full valid game without manual ESP32 intervention. +- Turn retention, turn changes, sunk-cell marking, victory, statistics, and rematch exactly match the locked rules. +- Refresh and reconnect restore each player's role and authorized view. +- Spectators never gain controls or hidden state. +- The run remains within the resource and latency budgets. + +### Completion action + +If all criteria pass, set this milestone to `DONE`, append its execution record, and change Milestone 015 from `BLOCKED` to `READY`. + +--- + +## Milestone 015 — Complete human-vs-ESP32 gameplay and cumulative statistics + +**Status:** `BLOCKED` +**Depends on:** Milestone 014 + +### Objective + +Integrate and prove the complete bot game, delayed multi-shot turns, rematch, and reboot-scoped cumulative statistics. + +### Work + +- Exercise human-vs-bot start with both possible first players. +- Verify non-blocking bot delays and repeated bot shots after hits. +- Test reconnect, abort, finish, rematch, and cancellation while a bot action is pending. +- Verify per-match and cumulative shots, hits, misses, accuracy, sunk ships, wins, and losses. +- Verify that rematch resets match statistics, preserves cumulative statistics, and creates new fleets/first player. +- Verify that a board reboot resets all sessions, game state, and cumulative statistics as required. + +### Acceptance criteria + +- A user can complete multiple games against ESP32 without a repeated or illegal bot shot. +- The bot never uses hidden board knowledge and never blocks network servicing during its delay or target calculation. +- All statistics match independently computed expected values. +- Rematch and reboot behavior match `MVP.md` exactly. +- Resource usage remains stable across repeated bot games. + +### Completion action + +If all criteria pass, set this milestone to `DONE`, append its execution record, and change Milestone 016 from `BLOCKED` to `READY`. + +--- + +## Milestone 016 — Harden errors, recovery, and resource usage + +**Status:** `BLOCKED` +**Depends on:** Milestone 015 + +### Objective + +Make the integrated application resilient to malformed traffic, connection churn, Wi-Fi interruption, slow clients, and long runtime on the constrained board. + +### Work + +- Fuzz bounded HTTP and WebSocket parsers with malformed, truncated, duplicate, stale, oversized, and unauthorized messages. +- Repeatedly connect, disconnect, refresh, and expire spectator sessions at maximum capacity. +- Interrupt Wi-Fi, WebSocket, and individual clients during all game phases and pending bot actions. +- Measure firmware, LittleFS, current/minimum heap, largest free block, task stacks, message sizes, response/update latency, and watchdog/reset reasons. +- Remove avoidable dynamic allocation from hot paths, compress static assets, and tune reusable buffers without weakening contracts. +- Run static checks and all host/device test suites from a clean build. + +### Acceptance criteria + +- Invalid traffic cannot crash, restart, starve, or leak hidden state from the device. +- Wi-Fi and WebSocket recovery requires no board reboot and preserves valid in-memory game state when specified. +- Connection churn does not produce a persistent heap or largest-block decline. +- Final firmware, filesystem, heap, stack, payload, and latency measurements pass every Milestone 005 limit. +- No unresolved high-severity correctness, privacy, or stability defect remains. + +### Completion action + +If all criteria pass, set this milestone to `DONE`, append its execution record, and change Milestone 017 from `BLOCKED` to `READY`. + +--- + +## Milestone 017 — Execute final MVP acceptance and create the release baseline + +**Status:** `BLOCKED` +**Depends on:** Milestone 016 + +### Objective + +Prove every MVP readiness criterion on the physical ESP32-C6 and produce a reproducible release baseline. + +### Work + +- Run every automated domain, bot, session, presenter, API, browser, and device test from a clean checkout/configuration. +- Execute all manual scenarios from `MVP.md` on the target board, including phone, tablet, spectators, reconnect, fallback, rematch, and reboot. +- Complete at least 20 consecutive representative games without manual restart, watchdog reset, or material memory decline. +- Run the maximum target load of two players and eight spectators for the duration defined in the resource budget. +- Record final version pins, build hashes, firmware/LittleFS sizes, heap/stack minima, largest payloads, latency, and reset reasons. +- Create concise setup, Wi-Fi configuration, build, upload, usage, and recovery documentation. +- Tag or otherwise record the exact source/configuration baseline accepted as the MVP release. + +### Acceptance criteria + +- All 13 MVP readiness criteria pass with recorded evidence. +- Twenty consecutive games complete without a hang, unexpected restart, or material memory leak. +- Two players and eight spectators remain supported within the final resource budgets. +- A clean build and upload are reproducible with pinned dependencies and no flash-size warning. +- No secret is present in tracked files or release artifacts. +- The release decision is documented as `PASS`, with remaining non-MVP ideas kept outside the release scope. + +### Completion action + +If all criteria pass, set this milestone to `DONE` and append its execution record. Add any post-MVP milestones only after Milestone 017 and do not renumber existing milestones. diff --git a/docs/API_CONTRACT.md b/docs/API_CONTRACT.md new file mode 100644 index 0000000..19d6578 --- /dev/null +++ b/docs/API_CONTRACT.md @@ -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. diff --git a/docs/BOARD_PASSPORT.md b/docs/BOARD_PASSPORT.md index 2ab4652..3b4d410 100644 --- a/docs/BOARD_PASSPORT.md +++ b/docs/BOARD_PASSPORT.md @@ -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 diff --git a/docs/GAME_DECISIONS.md b/docs/GAME_DECISIONS.md new file mode 100644 index 0000000..f310632 --- /dev/null +++ b/docs/GAME_DECISIONS.md @@ -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. diff --git a/docs/RESOURCE_BUDGET.md b/docs/RESOURCE_BUDGET.md new file mode 100644 index 0000000..d79c290 --- /dev/null +++ b/docs/RESOURCE_BUDGET.md @@ -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. diff --git a/src/main.c b/src/main.c index 8b0e514..8861cff 100644 --- a/src/main.c +++ b/src/main.c @@ -38,6 +38,7 @@ enum { kCapacityShipsPerBoard = 10, kCapacityTickMs = 25, kMaxStateMessageBytes = 512, + kHttpMaxOpenSockets = 12, }; typedef struct { @@ -186,8 +187,8 @@ static esp_err_t send_state(httpd_handle_t server, int fd, view_role_t role) { static void broadcast_state(void *unused) { (void)unused; - size_t count = 4; - int clients[4]; + size_t count = kHttpMaxOpenSockets; + int clients[kHttpMaxOpenSockets]; if (httpd_get_client_list(s_server, &count, clients) != ESP_OK) return; for (size_t index = 0; index < count; ++index) { if (httpd_ws_get_fd_info(s_server, clients[index]) == HTTPD_WS_CLIENT_WEBSOCKET) { @@ -571,7 +572,7 @@ static esp_err_t start_http_server(void) { httpd_handle_t server = NULL; httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.max_uri_handlers = 7; - config.max_open_sockets = 12; + config.max_open_sockets = kHttpMaxOpenSockets; config.uri_match_fn = httpd_uri_match_wildcard; config.lru_purge_enable = true; ESP_RETURN_ON_ERROR(httpd_start(&server, &config), kLogTag, "http server start failed");