Files
maze32/ARCHITECTURE.md
T

442 lines
16 KiB
Markdown

# maze32 Architecture
## 1. Purpose
This document defines the implementation boundaries and technical contracts for
the maze32 MVP described in [MVP.md](MVP.md). It explains how the firmware,
browser application, network transport, game simulation, and persistent
configuration fit together.
Product behavior belongs in `MVP.md`. Implementation sequencing and milestone
evidence belong in `PLANS.md`.
## 2. Architectural drivers
The design is constrained by the following facts:
- One ESP32-C6FH4 hosts the entire application.
- The board has 4 MB Flash and must not require PSRAM.
- Two players and up to four observers connect through browsers.
- HTTP serves static assets and configuration endpoints.
- WebSocket carries live lobby, control, and match traffic.
- The ESP is the only authoritative source of game state.
- Player clients must not receive information hidden by fog of war.
- The same firmware supports normal Wi-Fi station operation and fallback
access-point provisioning.
- The MVP has no OTA partition and no requirement to recover an active match
after device reboot.
## 3. System context
```mermaid
flowchart LR
P1["Player 1 browser"] -->|"HTTP + WebSocket"| ESP["ESP32-C6 maze32 server"]
P2["Player 2 browser"] -->|"HTTP + WebSocket"| ESP
O["Up to 4 observer browsers"] -->|"HTTP + WebSocket"| ESP
ESP --> NVS["NVS: Wi-Fi and device settings"]
ESP --> LFS["LittleFS: compressed web assets"]
ESP --> WIFI["Existing Wi-Fi network"]
ESP -. "fallback" .-> AP["Provisioning access point"]
```
The browser is a presentation and input client. It may animate confirmed state,
but it must not decide whether an action succeeded.
## 4. Firmware component boundaries
### 4.1 Platform layer
Owns ESP-IDF startup, logging, monotonic time, random seed acquisition, reboot
behavior, and error reporting. Code above this layer must not depend on an
unverified GPIO, LED, or PSRAM.
### 4.2 Persistent configuration
Owns NVS access for:
- Wi-Fi SSID and credentials;
- provisioning state;
- tunable device and gameplay settings that are intentionally persisted;
- schema version for safe future migration.
Session tokens, lobby membership, maze state, and active matches live in RAM
and are cleared on reboot.
### 4.3 Network manager
Implements this boot policy:
1. Load saved station credentials from NVS.
2. Attempt to connect in station mode.
3. If credentials are missing or the connection attempt fails, start the
fallback access point and provisioning interface.
4. Accept new network settings, save them to NVS, and retry station mode.
The exact connection timeout and access-point naming convention are tunable
implementation settings. Network reconfiguration must not silently continue an
active match.
### 4.4 HTTP and static asset service
Serves:
- the player/lobby application;
- the observer view;
- the fallback provisioning interface;
- minimal health and version information needed for diagnostics.
Production web assets are stored in LittleFS and should be precompressed during
the build. The asset compression script is not part of the current baseline and
must be introduced and tested in its own implementation milestone.
### 4.5 WebSocket gateway
Owns connections, role assignment, temporary session tokens, message parsing,
schema validation, rate limiting, reconnect attachment, and outgoing queues.
It translates accepted client messages into commands for the authoritative
game service. It never mutates match state directly.
### 4.6 Lobby service
Owns:
- two player slots;
- up to four observer slots;
- unique display-name validation;
- optional humorous name suggestions;
- identification of Player 1 as lobby owner;
- readiness and start authorization;
- reconnect reservations for player sessions.
Only Player 1 may start, and only when both player slots are connected.
### 4.7 Maze generator and validator
Generates a configurable rectangular maze and all initial placements. Generation
is complete only after the validator confirms:
- all required cells and objects are reachable;
- 180-degree rotational symmetry;
- symmetric player-relative starts, keys, exits, compasses, landmarks, and
vision artifacts;
- cycles and alternative routes;
- multiple connections between the conceptual halves;
- keys outside the initial owner views;
- equivalent placement and route metrics for both players;
- no placement collision that violates object rules.
Generation uses a server-owned seed recorded with the in-memory match for
diagnostics and reproducible tests. Clients do not generate the maze.
Search difficulty cannot be proven solely by geometric distance. The generator
therefore exposes route and branching metrics, while the final requirement that
key discovery be at least as challenging as subsequent exit discovery is
validated through gameplay telemetry and playtesting.
### 4.8 Authoritative game service
Owns the complete mutable match state:
- maze and generated-object state;
- player positions and connection states;
- inventories and ground keys;
- opened exits;
- active vision effects;
- countdown, pause, and result state;
- monotonic timestamps and accepted-action sequence numbers.
All commands are processed serially through one logical command queue. This
prevents concurrent WebSocket callbacks from producing inconsistent key,
collision, or victory results.
### 4.9 View projector
Builds role-specific snapshots and events from authoritative state:
- Player 1 receives only Player 1's allowed state.
- Player 2 receives only Player 2's allowed state.
- Observers receive the complete match view.
The projector is the security boundary for fog of war. Hidden cells and objects
must be omitted from serialized player payloads, not sent with a client-side
visibility flag.
## 5. Runtime state machines
### 5.1 Device lifecycle
```mermaid
stateDiagram-v2
[*] --> Booting
Booting --> StationConnecting: saved credentials
Booting --> ProvisioningAP: no credentials
StationConnecting --> Serving: connected
StationConnecting --> ProvisioningAP: connection failed
ProvisioningAP --> StationConnecting: settings saved
Serving --> [*]: reboot
```
### 5.2 Match lifecycle
```mermaid
stateDiagram-v2
[*] --> Lobby
Lobby --> Countdown: owner starts with 2 players
Countdown --> Running: countdown completes
Running --> Paused: player disconnects
Paused --> Running: player reconnects within 60 seconds
Paused --> Finished: reconnect timeout; draw
Running --> FinishPending: first outward exit
FinishPending --> Finished: 200 ms result window closes
Finished --> Lobby: return to lobby
```
During `Paused`, movement commands are rejected and move-counted effects do not
advance. During `FinishPending`, the first exit is retained while the server
waits long enough to determine whether the second exit creates a draw.
## 6. Authoritative command processing
Every gameplay command follows the same pipeline:
1. Authenticate the session token and resolve its role.
2. Verify that the role is allowed to issue the command.
3. Validate the match state and client command schema.
4. Enforce the server-side movement cooldown or other action constraints.
5. Apply the command to authoritative state.
6. Resolve cell entry, artifact activation, pickup, player meeting, key return,
exit opening, outward exit, and result transitions in a documented order.
7. Increment the authoritative sequence number.
8. Project and send the resulting role-specific state updates.
Observer sessions never reach mutation steps.
## 7. Game-state model
The implementation should favor fixed-size or bounded containers. Required
conceptual records include:
- `MazeConfig`: width, height, movement cooldown, object-density settings, and
balance thresholds;
- `Maze`: compact generated cell topology plus landmark identifiers;
- `MatchTopology`: fixed-size server-opened passage overlay for one match;
- `PlayerState`: slot, display name, session token reference, position,
inventory, vision effect, connection state, and last accepted move time;
- `KeyState`: owner and either a ground position or carrier;
- `ExitState`: owner, position, and open state;
- `CompassState`: owner association and position;
- `VisionArtifactState`: position and consumed state;
- `MatchState`: lifecycle, seed, sequence, timers, players, objects, and result;
- `ConnectionState`: WebSocket handle, role, session association, and bounded
outgoing queue.
The maze representation must be sized from configured dimensions with checked
arithmetic. The default 1,024-cell map must not require a per-cell heap
allocation.
## 8. Visibility projection
For a player at `(px, py)` with radius `r`, a cell `(x, y)` is visible when:
```text
max(abs(x - px), abs(y - py)) <= r
```
The normal radius is three and the boosted radius is six. Map boundaries clip
the square. Walls do not occlude cells.
The projected player view contains only:
- visible maze cells and landmarks;
- visible ground objects;
- the player's own position, inventory, effect counter, and exit/compass hint
state;
- the opponent only when the opponent is currently visible, including the
identity of a carried key;
- public lobby, pause, countdown, and result information.
Previously visible map state is not retained by the server for client display.
## 9. WebSocket protocol contract
Messages use a versioned envelope. The initial logical shape is:
```json
{
"v": 1,
"type": "message_type",
"seq": 42,
"requestId": "optional-client-id",
"payload": {}
}
```
Protocol requirements:
- unknown versions or message types are rejected safely;
- malformed payloads never reach game logic;
- client request identifiers support acknowledgement without granting
authority;
- server sequence numbers let clients discard stale updates;
- reconnect produces a fresh role-appropriate snapshot;
- payload and queue sizes are bounded;
- slow observers may lose intermediate updates and receive a newer snapshot,
but must not block player command processing.
Initial client-to-server message families are:
- session creation or restoration;
- role request and display-name selection;
- lobby start;
- move intent;
- drop-key intent;
- heartbeat.
Initial server-to-client message families are:
- session and role result;
- lobby snapshot;
- countdown;
- player-view snapshot or update;
- observer snapshot or update;
- action acknowledgement or rejection;
- pause/reconnect status;
- match result;
- protocol error.
Exact field schemas belong beside the implementation and require protocol tests.
## 10. Browser application
The browser application has four presentation modes:
1. network provisioning;
2. name selection and lobby;
3. player match view;
4. observer full-map view.
Player controls support keyboard movement, avatar drag, touch swipes, an
explicit four-button directional pad, and an explicit drop-key action. Every
movement input uses the same bounded latest-intent path and sends only a
cardinal intention; the browser does not predict a movement result. The UI
renders only server-provided visible state. The observer view supports fitting,
panning, or zooming the complete `32 x 32` map without changing server
authority.
The browser renders Player 1 as yellow and Player 2 as blue. The matching key
and exit use the owner's color as a secondary identity signal, while distinct
SVG silhouettes and open/closed treatments identify object type and state. This
presentation mapping is local only and does not alter authoritative game or
projection data.
`docs/UI_STYLE.md` is the canonical browser presentation contract. The player
layout supports a minimum `412 x 916` CSS-pixel viewport. Observer mode requires
at least `1024 x 768` CSS pixels and presents a large-screen requirement message
below that threshold. Responsive layout changes presentation only; they never
change projection content, command validation, or server authority.
The ESP stores each player's bounded perseverance streak and a fixed-size
opened-passage overlay. It alone expires streaks at 3,000 ms, opens both passage
bits, and projects changed passages only in cells already visible to that role.
The browser may correlate a matching server-approved impact stage with an
already visible bush wall; it never derives or opens topology locally.
Browser hedge rendering derives a bounded canonical edge list solely from the
current role projection. Its deterministic local SVG selection is presentation
only: it cannot create maze edges, alter hit testing, retain hidden cells, or
change authoritative movement and visibility. A player-only breakthrough
animation may begin only when a newer projection contains both a newly opened
visible edge and an adjacent server-approved self-cell move across that exact
edge; it is bounded, replaceable by a newer projection, and never delays the
received state.
Player projections are translated only within their received cells so the
authoritative `self` cell aligns with the responsive local focal point. That
point is centered horizontally and 30% down the usable portrait map region;
compact landscape uses a left-biased unobstructed region. It changes no server
projection, visibility, or hidden-state handling.
All application assets are local and precompressed. Browser startup and play
must not depend on internet access, third-party CSS, remote fonts, icons,
analytics, or content services. The complete compressed web payload is limited
to 1.5 MiB within the unchanged 1,984 KiB LittleFS partition.
A temporary session token is stored in browser-local storage so page refresh
can reattach to the existing slot. It is not a permanent account credential and
does not need to survive an ESP reboot.
## 11. Memory, storage, and concurrency
The locked Flash layout is defined in `partitions.csv`:
- 24 KiB NVS;
- 4 KiB PHY data;
- 2 MiB factory application;
- 1,984 KiB LittleFS.
The design must work without PSRAM. Memory rules are:
- no per-cell heap allocation;
- bounded WebSocket connections and outgoing queues;
- reuse serialized buffers where practical;
- prefer compact maze and object representations;
- do not retain separate full maze copies for each player;
- compute or serialize role projections from authoritative state;
- measure free heap and largest free block under the six-client acceptance
load.
`CONFIG_LWIP_MAX_SOCKETS=16` is an initial ceiling, not proof that six browser
clients fit in RAM. Hardware load evidence is required before the relevant
milestone can be completed.
## 12. Failure handling
- Invalid or rate-limited client actions receive a rejection and do not mutate
state.
- A player WebSocket loss pauses the match and starts a 60-second monotonic
reconnect deadline.
- Session restoration within the deadline resumes the same player state.
- Deadline expiry ends the match in a draw.
- Observer loss has no gameplay effect.
- A device reboot clears the active lobby and match.
- Failure to mount required production web assets is a startup error exposed in
logs and health state.
- A maze that fails validation is discarded and regenerated with a bounded
retry policy; exhausting that policy returns to the lobby with a visible
error rather than starting an invalid match.
## 13. Security and trust boundaries
The MVP is intended for a trusted local network, but the server still enforces:
- server-side role authorization;
- observer read-only behavior;
- unique player slots;
- bounded input lengths and message sizes;
- movement and message rate limits;
- no hidden-state disclosure to player payloads;
- no acceptance of client positions, timers, inventory, or results;
- restricted network-credential handling through the provisioning flow.
Password-protected accounts, internet exposure, TLS termination, and remote
matchmaking are outside MVP scope.
## 14. Verification strategy
Verification is layered:
- host-side unit tests for symmetry, reachability, visibility, movement, items,
collision resolution, timers, and state transitions;
- deterministic generator tests over many seeds;
- protocol tests for malformed messages, authorization, stale sequence handling,
and reconnect snapshots;
- browser tests for keyboard, swipe, drop-key, lobby, player, and observer flows;
- firmware build and partition-size checks;
- on-device tests for Wi-Fi fallback, NVS persistence, six simultaneous
WebSocket clients, disconnect recovery, heap use, and representative match
duration.
No milestone is complete merely because code exists. Completion requires the
evidence specified for that milestone in `PLANS.md`.