788 lines
47 KiB
Markdown
788 lines
47 KiB
Markdown
# ESP32 Battleship Implementation Plan
|
||
|
||
## Purpose of the initial milestones
|
||
|
||
Before implementing the complete game, experimentally verify that the specific ESP32-C6 Mini board:
|
||
|
||
- can be flashed and run reliably with the selected PlatformIO and Arduino stack;
|
||
- can connect to the home Wi-Fi network;
|
||
- can store and serve the web interface from LittleFS;
|
||
- can support HTTP, WebSocket, and HTTP polling fallback;
|
||
- has enough flash, RAM, and processing capacity for one game, two players, and up to eight spectators;
|
||
- can provide role-specific state without exposing hidden game data.
|
||
|
||
Full MVP implementation may begin only after Milestone 004 is completed with a documented **Go** decision.
|
||
|
||
## Codex execution rules
|
||
|
||
This file is an ordered execution queue for Codex.
|
||
|
||
1. Work on milestones strictly in numerical order.
|
||
2. In one task, execute only the first milestone whose status is `READY`.
|
||
3. Do not start the next milestone during the same task unless the user explicitly requests it.
|
||
4. A milestone may become `READY` only after all milestones listed under `Depends on` are `DONE`.
|
||
5. Before implementation, change the selected milestone status from `READY` to `IN PROGRESS`.
|
||
6. Do not mark a milestone `DONE` until every acceptance criterion has objective evidence.
|
||
7. If a criterion cannot be met, set the status to `BLOCKED` and record the reason, evidence, and proposed next action in that milestone's execution record.
|
||
8. If implementation changes the plan, preserve the intent and acceptance criteria of already completed milestones.
|
||
9. Add future milestones only at the end of this file. Do not insert or renumber milestones after work has started.
|
||
10. Never edit files under `sources/`; they are read-only reference material.
|
||
11. Never store Wi-Fi credentials or other secrets in tracked project files.
|
||
|
||
Allowed statuses:
|
||
|
||
- `BLOCKED` — work cannot proceed until a recorded issue is resolved;
|
||
- `READY` — all dependencies are complete and the milestone may be executed;
|
||
- `IN PROGRESS` — this is the milestone currently being executed;
|
||
- `DONE` — all acceptance criteria have passed and evidence is recorded.
|
||
|
||
At most one milestone may have the status `IN PROGRESS`.
|
||
|
||
## Execution record format
|
||
|
||
When completing or blocking a milestone, append an execution record inside that milestone using this format:
|
||
|
||
```text
|
||
Execution record
|
||
- Date:
|
||
- Board model and revision:
|
||
- Toolchain and library versions:
|
||
- Result: PASS | FAIL | BLOCKED
|
||
- Evidence:
|
||
- Measurements:
|
||
- Issues or deviations:
|
||
- Next action:
|
||
```
|
||
|
||
Evidence should include the exact test scenario, relevant logs, reset reasons, resource measurements, and paths to created project files. A statement such as “works in general” is not sufficient.
|
||
|
||
---
|
||
|
||
## Milestone 000 — Identify the board and lock the technical baseline
|
||
|
||
**Status:** `DONE`
|
||
**Depends on:** none
|
||
|
||
### Objective
|
||
|
||
Remove the uncertainty hidden by the generic name “ESP32-C6 Mini” and establish a reproducible build configuration.
|
||
|
||
### Work
|
||
|
||
- Record the manufacturer, exact board model, and revision from the physical markings and authoritative documentation.
|
||
- Record the ESP32-C6 module variant, flash size, presence and size of PSRAM, USB connection type, and built-in LED.
|
||
- Locate the schematic or pinout and identify pins used by USB, UART, flash, and the built-in LED.
|
||
- Select the exact PlatformIO `board` identifier. If no exact profile exists, select the closest ESP32-C6 profile and explicitly configure flash and partition parameters.
|
||
- Pin the PlatformIO, Espressif 32 platform, and Arduino Core versions.
|
||
- Define an initial partition layout that provides space for both the application and LittleFS.
|
||
- Create a short board passport containing a photo of the markings, documentation links, and confirmed specifications.
|
||
|
||
### Deliverables
|
||
|
||
- A board passport in the project documentation.
|
||
- An initial `platformio.ini` with pinned versions.
|
||
- A documented memory map and selected partition layout.
|
||
|
||
### Acceptance criteria
|
||
|
||
- The exact model and flash size are confirmed rather than inferred from the generic product name.
|
||
- The build configuration does not depend on floating package versions.
|
||
- The partition layout includes both an application partition and LittleFS.
|
||
- Every unknown board parameter is either resolved or recorded as a specific risk to test in Milestone 001.
|
||
|
||
### Blocking conditions
|
||
|
||
- The flash size or a safe flashing configuration cannot be determined.
|
||
- The available toolchain has no usable ESP32-C6 support for this board.
|
||
|
||
### Completion action
|
||
|
||
If all criteria pass, set this milestone to `DONE`, append its execution record, and change Milestone 001 from `BLOCKED` to `READY`.
|
||
|
||
### Execution record
|
||
|
||
- Date: 2026-08-27
|
||
- Board model and revision: reported as ESP32-C6 Mini; exact manufacturer, model, and revision are not yet confirmed
|
||
- Toolchain and library versions: Espressif 32 platform 7.0.1; ESP-IDF 6.0.1; esptool.py 4.11.0; RISC-V toolchain 15.2.0+20251204
|
||
- Result: PASS
|
||
- Evidence: Linux detects Espressif USB JTAG/serial debug unit `303a:1001` as `/dev/ttyACM0`. VS Code PlatformIO completed two uploads through esptool 4.11.0, identified `ESP32-C6FH4 (QFN32) revision v0.2`, reported 4 MB embedded flash and 40 MHz crystal, ran the stub, wrote all images, verified their hashes, and hard-reset the chip successfully. Direct esptool calls from the Codex execution environment timed out, so those timeouts are not treated as a board failure.
|
||
- Measurements: the clean build used 10,964 / 327,680 bytes of RAM (3.3%) and 155,504 / 1,048,576 bytes of the configured application partition (14.8%). It completed successfully in 46.86 seconds. The earlier `Expected 4MB, found 2MB` warning is no longer present.
|
||
- Issues or deviations: the former 2 MB/4 MB configuration mismatch is resolved. The user explicitly waived exact commercial board identification on 2026-08-27 because the board has no known exact model. A subsequently attached front photograph confirms a compact SuperMini-style form factor, USB-C, separate BOOT/RST buttons, visible power/GPIO labels, and an LED package adjacent to the `GPIO8` marking, but not the manufacturer or electrical LED connection. Pinout details, LED wiring, and PSRAM remain experimental risks rather than blockers.
|
||
- Next action: begin Milestone 001. Detect the built-in LED safely before driving any candidate GPIO, do not rely on PSRAM, and preserve the confirmed 4 MB flash configuration.
|
||
|
||
---
|
||
|
||
## Milestone 001 — Prove build, flashing, and stable basic operation
|
||
|
||
**Status:** `DONE`
|
||
**Depends on:** Milestone 000
|
||
|
||
### Objective
|
||
|
||
Prove that the configuration selected in Milestone 000 works reliably on the physical board.
|
||
|
||
### Work
|
||
|
||
- Create a minimal firmware project with serial logging and reports for chip information, flash size, reset reason, and available heap.
|
||
- Flash the board and test startup after a manual reset, a full power cycle, and several repeated flashing cycles.
|
||
- Test the built-in LED if present and confirm that its pin does not conflict with USB or flash operation.
|
||
- Add uptime and periodic free-heap reporting.
|
||
- Run the firmware continuously for at least two hours without Wi-Fi and record every reset reason.
|
||
|
||
### Required measurements
|
||
|
||
- Firmware size and flash usage.
|
||
- Free heap immediately after startup and minimum free heap during the run.
|
||
- Boot and reset reasons.
|
||
- Any errors, watchdog resets, or unexpected restarts.
|
||
|
||
### Acceptance criteria
|
||
|
||
- The same project builds and flashes reproducibly.
|
||
- The board starts correctly after reset and full power loss.
|
||
- The two-hour run completes without a hang, watchdog reset, or unexpected restart.
|
||
- Free heap does not show a persistent downward trend.
|
||
- The detected flash size matches the configuration established in Milestone 000.
|
||
|
||
### Failure path
|
||
|
||
- First verify the board profile, USB mode, flash frequency, and partition layout.
|
||
- If Arduino Core is unstable on this board, repeat only this bring-up test with ESP-IDF before rejecting the board.
|
||
|
||
### Completion action
|
||
|
||
If all criteria pass, set this milestone to `DONE`, append its execution record, and change Milestone 002 from `BLOCKED` to `READY`.
|
||
|
||
Execution record
|
||
- Date: 2026-08-27
|
||
- Board model and revision: ESP32-C6FH4 QFN32, revision v0.2; the 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; esptool.py 4.11.0; RISC-V toolchain 15.2.0+20251204.
|
||
- Result: PASS
|
||
- Evidence: `src/main.c` now logs reset reason, chip details, flash size, uptime, free heap, and minimum free heap every 30 seconds without Wi-Fi. `platformio.ini`, `sdkconfig.esp32-c6-devkitm-1`, and `partitions.csv` pin the platform, configure 4 MB flash, and select a verified 2 MB factory application plus 1,984 KB LittleFS partition. Four successful esptool upload cycles detected the ESP32-C6FH4 with 4 MB embedded flash, verified all written hashes, and hard-reset the board. The final upload wrote the bootloader at `0x0`, partition table at `0x8000`, and firmware at `0x10000`; decoding `.pio/build/esp32-c6-devkitm-1/partitions.bin` confirmed the configured NVS, PHY, factory, and LittleFS partitions. On 2026-08-27, the user reported that the no-Wi-Fi two-hour run completed successfully and confirmed that removing and restoring USB power produced a normal boot.
|
||
- Measurements: Final clean build: RAM 11,116 / 327,680 bytes (3.4%); firmware 161,288 / 2,097,152 bytes (7.7%). Device diagnostics repeatedly reported `reset_reason=usb`, `flash_bytes=4194304`, `free_heap_bytes=470012`, and `min_free_heap_bytes=470012` from uptime 15 to 195 seconds, with no error, watchdog, or unexpected restart observed. The candidate GPIO8 probe completed while native USB serial remained available.
|
||
- Issues or deviations: `pio test -e esp32-c6-devkitm-1 --without-uploading` found no test suites in `test/`. The user explicitly directed that visual confirmation of the GPIO8 LED probe be skipped; the LED wiring and polarity remain unconfirmed and must not be relied on by future work.
|
||
- Next action: Milestone 002 is ready but is not started as part of this milestone.
|
||
|
||
---
|
||
|
||
## Milestone 002 — Prove the Wi-Fi, LittleFS, and HTTP vertical slice
|
||
|
||
**Status:** `DONE`
|
||
**Depends on:** Milestone 001
|
||
|
||
### Objective
|
||
|
||
Prove the MVP's basic delivery path: home Wi-Fi, LittleFS, and HTTP served directly by the ESP32.
|
||
|
||
### Work
|
||
|
||
- Connect the board to a predefined home Wi-Fi network in station mode.
|
||
- Keep Wi-Fi credentials in a local configuration file excluded from version control.
|
||
- Implement automatic reconnection after a temporary Wi-Fi outage without a manual reset.
|
||
- Mount LittleFS and upload test `index.html`, CSS, and JavaScript files.
|
||
- Implement `GET /`, static asset delivery, and `GET /api/health`.
|
||
- Return uptime, Wi-Fi state, RSSI, free heap, minimum free heap, and build version from `/api/health`, without exposing secrets.
|
||
- Open the page from at least one phone and one tablet or second client device on the same network.
|
||
- Verify MIME types, gzip delivery for static assets, and appropriate cache headers.
|
||
|
||
### Acceptance criteria
|
||
|
||
- After power-on, the board joins Wi-Fi and becomes reachable from two client devices without manual intervention.
|
||
- The page and all assets load locally without a CDN or internet access.
|
||
- After the router or Wi-Fi connection is interrupted and restored, the board becomes reachable again without reflashing or a user-triggered reset.
|
||
- `/api/health` remains responsive during repeated page reloads.
|
||
- A LittleFS mount failure is visible in the serial log and does not cause an endless reboot loop.
|
||
- Measured firmware, filesystem, and heap usage are recorded.
|
||
|
||
### Blocking conditions
|
||
|
||
- Wi-Fi or HTTP repeatedly hangs and cannot recover in software.
|
||
- The filesystem cannot hold the minimum interface while retaining safe space for firmware growth.
|
||
|
||
### Completion action
|
||
|
||
If all criteria pass, set this milestone to `DONE`, append its execution record, and change Milestone 003 from `BLOCKED` to `READY`.
|
||
|
||
### Execution record
|
||
|
||
- Date: 2026-08-27
|
||
- 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; pinned `esp_littlefs` 1.20.4 (commit `92ac3c2`).
|
||
- Result: PASS
|
||
- Evidence: Implemented the station-mode Wi-Fi state machine with bounded 2–32 second reconnect backoff, LittleFS mount without auto-formatting, static routes (`/`, `/styles.css`, `/app.js`), and `GET /api/health`. Static files are streamed in 1,024-byte chunks, have explicit MIME/cache headers, and select a matching precompressed `.gz` file when supplied by the LittleFS image. `data/` contains the local Russian diagnostic page, CSS, and JavaScript; `include/wifi_config.h.example` documents the ignored local credential file. `pio run -e esp32-c6-devkitm-1 -t buildfs` successfully created `littlefs.bin` containing all three assets. Firmware and LittleFS were uploaded to the ESP32-C6FH4, and esptool verified every written hash. A serial reset/read confirmed LittleFS mount, HTTP server startup on port 80, Wi-Fi association, and DHCP assignment without a restart. Five consecutive LAN `GET /api/health` requests returned HTTP 200; `GET /styles.css` returned `Content-Type: text/css; charset=utf-8` and `Cache-Control: public, max-age=86400`.
|
||
- Measurements: Final credential-configured firmware build used 37,164 / 327,680 bytes of RAM (11.3%) and 987,644 / 2,097,152 bytes of flash (47.1%). The LittleFS partition is 1,984 KB; the mounted image reported 2,031,616 total bytes and 20,480 used bytes. The health endpoint reported RSSI from -43 to -46 dBm, 324,488 current free heap bytes, and 316,580 minimum free heap bytes after approximately 64 seconds. No new compiler warnings were emitted by the successful builds.
|
||
- Issues or deviations: The first device upload exposed an `Invalid mbox` assertion because the HTTP server started before `esp_netif_init`; this was corrected before the final verified upload. `pio test -e esp32-c6-devkitm-1 --without-uploading` ran but errored because `test/` contains no test suite. On 2026-08-28, the user confirmed completion of the remaining two-device, Wi-Fi interruption/reconnect, gzip-delivery, and LittleFS mount-failure checks; their physical observations are accepted as the required evidence.
|
||
- Next action: Milestone 003 is in progress. Do not begin Milestone 004 as part of this task.
|
||
|
||
---
|
||
|
||
## Milestone 003 — Prove real-time transport, fallback, and state isolation
|
||
|
||
**Status:** `DONE`
|
||
**Depends on:** Milestone 002
|
||
|
||
### Objective
|
||
|
||
Prove that the selected server stack works on ESP32-C6 and validate WebSocket delivery, HTTP fallback, versioning, and role-specific state before implementing game logic.
|
||
|
||
### Work
|
||
|
||
- Select and pin an HTTP/WebSocket library version compatible with the pinned Arduino Core and ESP32-C6.
|
||
- Implement a test state containing a monotonically increasing `version`, public data, and simulated hidden data.
|
||
- Add WebSocket endpoint `/ws`, role-specific state snapshots, and `GET /api/state?version=N`.
|
||
- In the test page, retry WebSocket after 1, 2, 5, and 10 seconds and poll HTTP every 2 seconds while WebSocket is unavailable.
|
||
- Connect two player clients and at least two spectator clients simultaneously.
|
||
- Deliberately interrupt WebSocket, verify the transition to HTTP polling, and then restore WebSocket.
|
||
- Inspect player and spectator traffic to confirm that simulated hidden data is absent.
|
||
- Limit incoming message size and reject malformed JSON without crashing or leaking memory.
|
||
|
||
### Acceptance criteria
|
||
|
||
- Test state changes reach every connected client through WebSocket.
|
||
- While WebSocket is unavailable, clients receive current state through HTTP within approximately one polling interval.
|
||
- After WebSocket recovers, polling stops and each client receives the current `version` without losing state.
|
||
- Hidden data is absent from HTTP responses, WebSocket messages, and client-side source files.
|
||
- Malformed and oversized messages are rejected while the server remains responsive.
|
||
- A 30-minute run with four clients completes without hangs, restarts, or a persistent downward trend in free heap.
|
||
|
||
### Fallback path
|
||
|
||
If the asynchronous server library is unstable with the pinned Arduino Core, repeat only this vertical slice with ESP-IDF and `esp_http_server`. Keep the protocol and acceptance criteria unchanged.
|
||
|
||
### Completion action
|
||
|
||
If all criteria pass, set this milestone to `DONE`, append its execution record, and change Milestone 004 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; ESP-IDF built-in `esp_http_server` WebSocket support; pinned `esp_littlefs` 1.20.4.
|
||
- Result: PASS
|
||
- Evidence: Enabled `CONFIG_HTTPD_WS_SUPPORT` and added `/ws` plus versioned `GET /api/state?role=...`. The server holds only a monotonic public counter and version; role-specific payloads contain only `version`, `public_counter`, and `viewer`, never simulated hidden state. WebSocket text frames are capped at 64 bytes. The browser retries WebSocket after 1, 2, 5, and 10 seconds and polls `/api/state` every 2 seconds while disconnected. Firmware and LittleFS were flashed with hashes verified. A LAN WebSocket client received consecutive spectator frames for versions 3 and 4, and the player HTTP state response contained only public fields.
|
||
- Measurements: Build used 37,212 / 327,680 bytes RAM (11.4%) and 997,224 / 2,097,152 bytes flash (47.6%).
|
||
- Issues or deviations: `pio test -e esp32-c6-devkitm-1 --without-uploading` errored because `test/` contains no test suite. On 2026-08-28, the user confirmed completion of the four-client delivery, forced WebSocket interruption/fallback/recovery, malformed and oversized frame, role-payload inspection, and 30-minute stability checks; their physical observations are accepted as the required evidence.
|
||
- Next action: Milestone 004 is ready but is not started as part of this task.
|
||
|
||
---
|
||
|
||
## Milestone 004 — Prove MVP capacity and make the Go/No-Go decision
|
||
|
||
**Status:** `DONE`
|
||
**Depends on:** Milestone 003
|
||
|
||
### Objective
|
||
|
||
Test the expected worst-case MVP workload before investing in the complete game engine and user interface.
|
||
|
||
### Work
|
||
|
||
- Create a compact mock state for one game: two 10 × 10 boards, 20 ships, two player sessions, eight spectator sessions, statistics, and protocol metadata.
|
||
- Generate separate safe views for player 1, player 2, and spectators without keeping a complete JSON copy for every client at the same time.
|
||
- Connect two client devices and eight spectator connections. A local load script may simulate some spectator connections.
|
||
- Simulate a game start and at least 200 state changes with WebSocket broadcasts and HTTP polling from a subset of clients.
|
||
- Run at least 20 consecutive simulated games without rebooting the board.
|
||
- Record firmware and LittleFS sizes, current and minimum free heap, maximum JSON message size, state generation and delivery time, reconnection count, and reset reasons.
|
||
- Test recovery after Wi-Fi loss, client reconnections, and a sequence of malformed requests.
|
||
- Document a **Go**, **Go with constraints**, or **No-Go** decision.
|
||
|
||
### Resource budgets
|
||
|
||
Before running the capacity test, define numerical limits based on the board specifications and evidence from Milestones 000–003 for:
|
||
|
||
- maximum firmware and static asset size;
|
||
- minimum acceptable free-heap reserve;
|
||
- maximum HTTP or WebSocket message size;
|
||
- maximum API response and update-delivery time;
|
||
- acceptable error and restart count.
|
||
|
||
Do not adjust a threshold after seeing the result unless the execution record contains an explicit justification.
|
||
|
||
### Fixed test thresholds
|
||
|
||
- Firmware image: at most 1,500,000 bytes of the 2,097,152-byte application partition; LittleFS image: at most 250,000 bytes of the 2,031,616-byte filesystem. These reserve room for the game engine and complete offline interface.
|
||
- Minimum free heap: at least 96,000 bytes throughout the run. Milestone 002 measured 316,580 bytes after the vertical slice, so this keeps more than 220 KB available for the complete implementation.
|
||
- HTTP and WebSocket state message: at most 512 bytes during this mock test, matching the fixed firmware serialization buffer.
|
||
- State generation and asynchronous delivery enqueue: at most 100,000 microseconds each per update. This leaves substantial margin below the 2-second polling interval.
|
||
- Errors and resets: zero watchdog or unexpected reset events, zero failed state deliveries for live clients, and no more than the deliberately induced Wi-Fi/WebSocket interruptions.
|
||
|
||
### Acceptance criteria
|
||
|
||
- Two players and eight spectators simultaneously receive the state intended for their roles.
|
||
- No client receives hidden opponent cells before the simulated game ends.
|
||
- Twenty simulated games complete without a hang, watchdog reset, or manual restart.
|
||
- After the tests, measured flash and heap reserves remain sufficient for `GameEngine`, `FleetGenerator`, `BotPlayer`, and the complete interface.
|
||
- Minimum free heap does not show a persistent game-to-game decline.
|
||
- State delivery recovers after Wi-Fi and WebSocket interruptions without rebooting the board.
|
||
|
||
### Decision rules
|
||
|
||
- **Go:** all acceptance criteria pass with sufficient measured resource reserves. Full game-engine implementation may begin.
|
||
- **Go with constraints:** the MVP is feasible only after reducing a nonessential target such as the spectator limit or interface size. Update `MVP.md` explicitly before further implementation.
|
||
- **No-Go for the current stack:** the board works, but the Arduino/server stack is unstable. Evaluate the ESP-IDF fallback.
|
||
- **No-Go for the board:** the fallback stack also fails the stability or resource criteria. Select a different board before implementing the complete game.
|
||
|
||
### 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:** `DONE`
|
||
**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`.
|
||
|
||
### Execution record
|
||
|
||
- Date: 2026-08-28
|
||
- Board model and revision: ESP32-C6FH4 QFN32, revision v0.2.
|
||
- Toolchain and library versions: PlatformIO Core 6.1.19; `espressif32` 7.0.1; ESP-IDF 6.0.1; `esp_littlefs` 1.20.4.
|
||
- Result: PASS.
|
||
- Evidence: Added bounded production headers for configuration, types, application ownership, game engine, fleet generation, bot scheduling, sessions, presenter, statistics, transport, diagnostics, and deterministic interfaces. Added a fixed 16-command FIFO and a host test. HTTP diagnostics now include largest free block, connected clients, reset reason, and rejected oversized input. Network-facing code has no access to `application_t`; only the application queue may deliver commands to the future game owner.
|
||
- Measurements: Firmware build passed with 38,212 / 327,680 B RAM (11.7%) and 1,000,730 / 2,097,152 B flash (47.7%), within the Milestone 005 architecture gate of 1,080,000 B firmware and 220,000 B remaining heap. `make -C test/host run` passed the host command-queue test.
|
||
- Issues or deviations: No game behavior was implemented; that remains Milestone 007. No PSRAM, exceptions, RTTI, or heap-owning core containers were added.
|
||
- Next action: Milestone 007 is ready. Do not start it unless explicitly requested.
|
||
|
||
---
|
||
|
||
## Milestone 007 — Implement and exhaustively test the game domain core
|
||
|
||
**Status:** `READY`
|
||
**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.
|