Architecture¶
The Enchanted Board’s game client is a static browser application with no framework or frontend runtime package. index.html loads the generated styles/app.css bundle and starts src/app.js as a native ECMAScript module. A separate, standard-library Python service persists optional player feedback in SQLite; it never participates in chess rules or game state.
Architecture at a glance¶
The runtime boundaries. Chess decisions stay in the browser; only optional feedback crosses the application edge. Documentation has its own build and hosting path.¶
The golden path is the complete chess loop: input reaches the coordinator, the
canonical ChessEngine evaluates or changes the position, and a snapshot is
projected into the board. The green path is deliberately separate: feedback is
plain text sent for authoritative server validation and never enters game state.
The blue documentation path neither deploys to nor executes inside the game
runtime.
Runtime data flow¶
src/app.jscreates oneChessEngineand collects references to the interface elements.Setup choices determine local or computer mode, player color, search depth, and board orientation.
A pointer, keyboard, hint, undo, or computer action asks the engine for legal candidates; promotion choice is resolved before a candidate advances.
For a move, the coordinator locks interaction, captures the game-generation token, and uses the stable pre-move board as scene geometry.
A normal move runs grounded travel; a cinematic capture instead runs an in-place battle. Reduced-motion mode bypasses either scene, while skip and material resize call the same finish cleanup.
After the scene handle resolves, a generation guard rejects stale work. Only then does
ChessEngine.makeMove()revalidate and commit the move.The coordinator reads the new engine snapshot and status, renders 64 keyed grid controls with structural SVG pieces, then updates the rails, command panel, chronicle, captured-piece courts, announcements, and any endgame scene.
The footer feedback form validates locally, then posts plain-text feedback to the same-origin
/api/feedbackendpoint. Caddy routes only that path to the private feedback service.
The chess engine remains the source of truth. Animation modules receive rendered elements and move metadata; they do not decide whether a move is legal.
Module boundaries¶
Client ownership and dependency direction. The coordinator connects small modules while the rules core remains independent of the DOM, CSS, and animation system.¶
The diagram reads from authority to adaptation rather than from visual depth. Solid lines are coordinator calls; dashed lines are data or finishable scene handles returned to the coordinator. Choreography can consume a legal move record, but it cannot create one or mutate the engine.
Game rules: src/chess.js¶
ChessEngine owns the board array, active color, castling rights, en-passant target, clocks, and move history. Public moves use algebraic square names while the internal board uses indexes 0 through 63, from a8 to h1.
Legal move generation follows this sequence:
Generate piece-specific pseudo-legal moves.
Apply each candidate to a reversible snapshot.
Reject candidates that leave the moving king attacked.
Restore the snapshot and expose cloned move records to callers.
The computer player uses material and positional scoring with alpha-beta search. The interface maps the three difficulty choices to depths one through three even though the engine defensively clamps direct API requests to a safe range.
analyzeBestMove({ depth: 3 }) runs a deterministic, non-mutating search for hints. It
returns the root move, score and perspective, an annotated legal principal variation,
and verifiable facts such as captures, check, castling, center occupation, or a
knight/bishop leaving its home rank. src/hints.js formats those facts into plain
text without inventing threats or strategic motives.
Application coordinator: src/app.js¶
The coordinator owns transient interface state such as the selected and focused squares, orientation, active animation, preferences, and setup choices. It is responsible for:
rendering board controls and status copy;
coordinating human and computer turns;
opening setup, rules, promotion, and feedback dialogs;
keeping pointer and keyboard behavior consistent;
starting and finishing arrival, travel, combat, and endgame scenes;
restoring focus and updating the live region after asynchronous actions.
Generation counters and active-animation handles prevent delayed callbacks from an earlier game from mutating a newly reset position.
Hint analysis yields one paint before its synchronous depth-three search, locks the board during that search, and checks both the generation and original FEN before presenting its result. Hints are stored as structured transient state. The keyed renderer adds distinct, direct-child FROM and TO markers without replacing the hinted piece node. Selecting the hinted source preserves both markers and the persistent oracle; a different selection, move attempt, cancellation, reset, or undo clears them.
Input: src/board/input.js¶
The board is transformed in 3D, so a DOM target alone is not always enough to identify the intended square. The input helper prioritizes a piece’s owning square, supports delegated SVG children, fills sub-pixel perspective seams, and expands selected or legal destinations to at least a 44-pixel screen-space target. When expanded targets overlap, the closest visual center wins.
Pieces: src/pieces/renderer.js¶
The piece renderer returns SVG markup with stable role, articulation, weapon, and contact-point hooks consumed by CSS and combat choreography. Keep these hooks backward compatible or update their contract tests with the dependent motion code.
Movement and combat¶
src/board/motion.js implements role-specific locomotion and move cleanup. src/board/battle.js owns combat profiles, phase timing, actor rigs, weapon-contact geometry, effects, the matchup display, and skip cleanup.
Both APIs return active-scene handles that the coordinator can finish early. Cleanup must leave board ownership, focus, and the engine position consistent whether an animation completes, is skipped, or is interrupted by resize or a new game.
Move transaction lifecycle¶
Travel and combat are pre-commit scenes. Completion, skip, or resize cleanup reaches the same generation guard; only ChessEngine.makeMove() can commit the position afterward.¶
This ordering is intentional. The scene runs over stable pre-move squares, then
its { promise, finish } handle resolves. app.js compares the captured game
generation before calling engine.makeMove(), so a reset makes an old animation
harmless even if its callback eventually completes. Reduced-motion mode bypasses
the scene and reaches the same guard immediately. After a successful commit, the
coordinator reads the new engine snapshot and status to update the board, rails,
chronicle, announcements, and any endgame scene.
Endgame: src/endgame-choreography.js¶
describeEndgame is a pure function that converts engine status and play mode into outcome copy and placement. startEndgameChoreography applies DOM state, effects, timing, announcements, and skip behavior. Keeping the description pure makes outcome rules testable without a browser DOM.
Feedback client: src/feedback.js¶
The feedback module normalizes and validates the category and 3–2,000-character message, submits the strict JSON payload, and owns the dialog’s pending, cancellation, error, success, and focus-restoration lifecycle. The hidden website field is an abuse-detection honeypot. Browser normalization improves the interaction but is not a security boundary; the server repeats every check.
The client accepts any successful HTTP response without rendering response content. Failed requests leave the draft in place, duplicate submits are ignored while a request is pending, and closing the dialog aborts the request.
Feedback service: feedback/¶
feedback.server exposes only POST /api/feedback and GET /healthz. It requires exact same-origin request metadata from a trusted reverse proxy, applies a strict three-field schema, performs authoritative plain-text sanitization, and stores accepted feedback with parameterized SQLite statements. It uses WAL mode, bounded storage, rate windows, a keyed client identifier, and a 24-hour keyed duplicate digest. Client and duplicate keys live only in short-lived abuse-control tables; feedback rows do not contain an IP address.
feedback.manage is a host-only CLI for counts, sanitized JSON Lines exports, full integrity checks, retention pruning, and consistent online backups. There is no public feedback read API. The HMAC key and database live in separate persistent mounts. The full data model and operational commands remain in the private repository guides.
Styling and visual layers¶
Stylesheet sources live in styles/, are concatenated in the order declared by scripts/build-css.mjs, and are checked in as styles/app.css. Base layout and battle rules come first; later files refine responsive behavior, structural pieces, board depth, combat depth, control placement, endgame presentation, and the isolated feedback dialog. Run npm run build:css after changing a source stylesheet, and inspect later layers before increasing specificity.
The board’s 3D appearance is produced with CSS transforms and inline SVG, not WebGL. Animation code should prefer transforms and opacity so movement remains composited and avoids repeated layout work.
Accessibility and motion¶
The board is an ARIA grid with roving keyboard focus.
Status changes are announced through an atomic live region.
Setup, rules, promotion, and feedback use native dialogs; feedback restores its footer trigger after close.
Selected and legal destinations retain enlarged pointer targets in viewport space.
Reduced-motion preference bypasses arrival, travel, and battle scenes, and settles endgame effects without extended choreography.
Escape provides a consistent way to leave or finish transient scenes.
Any new asynchronous sequence should preserve those same focus, announcement, reduced-motion, and cancellation guarantees.
Safe extension points¶
Add a chess rule in
src/chess.js, then cover legal generation, execution, notation, and undo intests/chess.test.js.Add or reshape a piece through the structural renderer while preserving role and weapon-contact hooks.
Add movement or combat behavior in its focused module instead of coupling it to engine internals.
Add interface state in the coordinator, but derive position facts from
ChessEnginerather than duplicating board state.Add visual rules at the narrowest appropriate stylesheet layer and review both 3D and flat layouts at desktop and phone sizes.
Change the feedback request schema only as an atomic client, service, database constraint, Caddy, documentation, and test update.