Files
maze32/ARCHITECTURE.md
T

15 KiB

maze32 Architecture

1. Purpose

This document defines the implementation boundaries and technical contracts for the maze32 MVP described in 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

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

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

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 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:

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:

{
  "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 browser may correlate a server rejection only with its matching outstanding movement intention for presentation feedback. A blocked rejection shakes the already visible projection in that attempted direction, while rate_limited briefly darkens and blurs the maze only. Neither effect changes or derives game state, and both use bounded, resettable local timers.

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.