411 lines
14 KiB
Markdown
411 lines
14 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 cell topology plus landmark identifiers;
|
|
- `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 8,192-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, touch swipes, and an explicit
|
|
drop-key action. The UI renders only server-provided visible state. The observer
|
|
view supports fitting, panning, or zooming the complete `128 x 64` map without
|
|
changing server authority.
|
|
|
|
`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.
|
|
|
|
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`.
|