Configurable game setup: persistence, DSL, interpreter, and Play flow (#113–#120) - #125
Open
jordanwallwork wants to merge 12 commits into
Open
Configurable game setup: persistence, DSL, interpreter, and Play flow (#113–#120)#125jordanwallwork wants to merge 12 commits into
jordanwallwork wants to merge 12 commits into
Conversation
Implements #113 and #114 (parent map #98). #113 — GameSetup persistence: - GameSetup entity (Id, ProjectId, Name, Document jsonb, IsValid) stored as an opaque raw-JSON document, following the SpreadsheetDataSource string-to-jsonb pattern so it works across Npgsql and the in-memory test provider. - AddGameSetup migration (cascade FK to Project, index on ProjectId). - GameSetupSummaryDto / GameSetupDto with JsonElement passthrough, Create/Update request DTOs. - GameSetupService with project-scoped CRUD + authorization, and five endpoints at /projects/{projectId}/setups. Service registered and endpoints mapped. - 14 service unit tests. #114 — setup DSL types + static validation ($lib/gamerunner): - types.ts: GameSetup document, SetupOption, ZoneBlueprint, ZoneReference, Condition/ValueExpr, the eight verbs/Action union, SetupNode kinds, SetupValidationError and the closed SetupRunError union, plus a version field and CURRENT_SETUP_VERSION ceiling. Runtime interpreter is a separate later ticket (#108) and is intentionally absent. - validate.ts: validateGameSetup(doc, projectContext) covering structure, referential integrity (component/blueprint/option ids, seat-context) and condition type-checking. Flat {docPath, message} errors, no flow analysis (per #110). - 48 validator tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HsqGnGTkY8y87pBDBEJmf8
Rework the setup DSL blueprint from a single zone into a named bundle of
zones, and add pure blueprint instantiation for the game runner.
- Blueprint is now {id, scope, displayName, zones}: a bundle of zones that
share one scope (table|seat|edge), replacing #114's single-zone
ZoneBlueprint. Each Zone carries {id, role, name, geometry, faceVisibility,
presence}; geometry reuses the tabletop ZoneType + Rect vocabulary.
- ZoneReference gains a `role` so the setup program addresses a specific zone
within a bundle (the (seat, role) addressing of decision #104).
- placeZone(blueprint, index) and instantiateBlueprint(blueprint, playerCount)
produce concrete PlacedZones: table once, seat once per seat, edge once per
edge (N edges for N seats). Seat zones carry their owner seatIndex (#103);
world placement (the ring layout) is left to a downstream, swappable strategy
(#102), keeping instantiation a pure function.
- validateGameSetup extends to the nested model: unique blueprint ids and
displayNames, unique zone ids/roles within a blueprint, geometry checks, and
referential integrity of (blueprint, role) zone references.
- Tests cover instantiation at 1/2/4/8 players and validator missing/duplicate
blueprint and zone-role checks.
No EF migration: per decision #106 the whole document (blueprints included)
lives in the opaque GameSetup.Document jsonb column, so the blueprints field is
a JSON-only change with no schema impact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HsqGnGTkY8y87pBDBEJmf8
Implement computeViewState, a pure function that projects the shared omniscient TabletopState into the masked copy a single viewer is allowed to see, per the #103 ownership/visibility model: - Two orthogonal per-zone axes reused from the gamerunner vocabulary: faceVisibility (all|owner|others|none, "others" = Hanabi) redacts card identity where faces are hidden while keeping presence/back; presence (hidden-from-non-owners) removes the whole zone, its piles and cards from non-owners' views. - Zone owner = its seatIndex; table/edge zones are unowned. Omniscient sees everything. The function never mutates its input. Visibility-metadata seam: the live tabletop Zone type deliberately does not carry ownership/visibility fields (nothing wires blueprint #115 PlacedZones into a live state yet), so computeViewState takes a separate VisibilityMap keyed by zone id. This keeps it pure and testable and maps one-to-one onto PlacedZone. Zones absent from the map are fully public and unowned, so existing freeform solo play is unaffected. A thin viewController.svelte.ts rune wrapper holds only the chosen viewer (seat index or omniscient) and derives the masked view, so switching seats changes the view without touching state. Tests cover 2/4/8 player configs, all four faceVisibility values, purity and the freeform default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HsqGnGTkY8y87pBDBEJmf8
Implement the pure, deterministic setup interpreter that walks a statically-valid GameSetup AST and builds a tabletop TabletopState from a blank table, emitting a linear SetupStep trace for animated replay (#107). - runSetup(doc, {playerCount, options, seed, templates}) returns a result union: { ok:true, state, visibility, trace } or { ok:false, error }. A failed run applies nothing; runtime failures surface as the closed-union SetupRunError (insufficient-cards, zone-not-on-table, card-field-miss, internal) via fail-fast. - All eight verbs: placeSeats/placeZone reuse #115 instantiateBlueprint/ placeZone; shuffle/roll reuse the existing tabletop operations (shufflePile, rollPile); place/deal/move/flip build state directly. - Seeded mulberry32 PRNG + counter ids threaded through an explicit RunContext, so same seed + options + playerCount is byte-identical. - Ownership/visibility rides alongside as a #116 VisibilityMap (the tabletop Zone carries no ownership fields), feeding computeViewState. - Condition algebra (all/any/not over typed comparisons of playerCount/ option/count/cardField) wired into WhenNode and forEachSeat fan-out. - Export placedZoneId from instantiate as the single source of truth for placed-zone ids so reference resolution can't drift from placement. - Parametric tests at 1/2/4/8 players covering final state, ownership and determinism. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HsqGnGTkY8y87pBDBEJmf8
#115 stamps blueprint zones per seat/edge but copies blueprint-local geometry verbatim, deferring world positioning to a swappable strategy. This adds that strategy: a pure, deterministic seat-ring geometry module. - seatRing.ts: `SeatLayoutStrategy` seam (#102) with a `radial` first implementation. Seats are evenly spaced on a circle (seat 0 at the bottom, nearest the viewer), edges on a tighter concentric ring between adjacent seats. Auto-fit radius is derived from the seat blueprint's bounding boxes so adjacent panels never overlap (bounding-circle / chord math); an optional `ringRadius` override (#111) only ever widens it. Components keep true physical scale (#109) — `positionRingZones` translates each seat/edge group so its bounding-box centre lands on the ring point, never scaling; table zones pass through unmoved. - interpreter.ts: `runPlaceSeats` / `runPlaceZone` now route stamped zones through `positionRingZones` using a ring computed once and cached on the run context; `PlaceSeatsAction.ringRadius` feeds the override. - Metadata (seatIndex, edgeIndex, role, visibility) and input order are preserved through positioning. Tests cover 1/2/4/8-player angles, even spacing, auto-fit non-overlap, override widening, edge placement, and metadata/order preservation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HsqGnGTkY8y87pBDBEJmf8
Introduce the first screen of the setup Play flow: a dialog listing all GameSetups for the current project, loaded from the setups API. Each row shows the setup name and a Valid/Invalid badge; invalid setups are greyed out and non-selectable. Selecting a valid setup reports it via an onSelect callback and closes the dialog — execution (count/options + interpreter) is deferred to #120, which plugs into that seam. Adds the frontend GameSetupSummary type and gameSetupsApi.list client (there was none yet), wired through the type/api barrels, plus a Play button in the tabletop toolbar that opens the picker. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HsqGnGTkY8y87pBDBEJmf8
Extend #119's setup picker into the full Play flow: pick a setup, configure a bounded player count and typed options, validate, then run the interpreter and load the result into the live tabletop. - gameSetupsApi.get(projectId, setupId) fetches the full setup document - PlaySetupDialog orchestrates load -> validation gate (#114) -> run (#117), decomposed into PlayerCountStepper, SetupOptionInputs and SetupErrorPanel - Pure lib/play helpers (choices/storage/run): default+clamp choices, localStorage per-setup memory, projectContext, wipe-needed and seed logic - store.loadSetupRun applies the whole run as one undo entry and stores the visibility map alongside for #116/#124 - Confirm wipe only when the table is non-empty; failed runs apply nothing; "Open in editor" left as a seam for #123 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HsqGnGTkY8y87pBDBEJmf8
) Wire #116's pure visibility masking into the live tabletop render and add the toolbar control that #116 deferred, so a solo designer can view the table omniscient (default) or through a single seat's eyes. - Instantiate the #116 viewController at the tabletop root, fed by store state, store.visibility (#120) and a seat count derived from the run's visibility map. Expose renderState on the tabletop context: the live state when omniscient (identity — freeform play byte-for-byte unchanged, no cloning), or the masked projection for the selected seat. - Funnel renderState through the render tree (TabletopSurface, ZoneRenderer, PileFace) so hidden zones/piles/cards drop out and face-hidden cards show their back (CardFace/FlipCard honour faceHidden). Interaction keeps operating on the live store.state; switching never touches saved state or undo history. - Add SeatSwitcher toolbar control, shown only when the run has seat zones: a dropdown listing Omniscient plus seats 1..N. Defaults to omniscient. - Add seatSwitcher.ts pure helpers (seatCountFromVisibility, hasSeatZones) with unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HsqGnGTkY8y87pBDBEJmf8
Play a computed setup run back as a skippable, step-by-step build-up instead of dropping the final table on instantly (decision #107). - Add a per-verb animation registry (all 8 verbs) that decides each step's hold time; shuffle reuses the existing riffle timing, the rest hold for a tunable ~500ms beat. Extensible seam for bespoke animations. - Add planReplay: a pure progressive reveal from the run's trace whose last frame equals the final state, so a full replay and a mid-flight skip land on the identical committed table. - Drive playback from the store: frames render through a new replayState while the live table stays untouched, so the single closing commit keeps the whole run as ONE undo entry. - Lock input during replay (interaction + keyboard guards, a screen-space overlay hosting the Skip button, disabled toolbar controls). - Wire seed handling into the Play dialog: Play mints a new seed; "Replay same deal" re-runs the last seed deterministically, persisted per setup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HsqGnGTkY8y87pBDBEJmf8
Create the setup-editor page as a new project sub-route (.../setups/[setupId]/edit) with a view-switch shell — a "Setup script" tab (fully implemented here) and a "Blueprints" tab left as an obvious stub for #122 to fill. Both views share one loaded GameSetup document and one Save button, per decision #111. The Setup-script view renders the DSL AST as editable prose: one sentence per node with dropdown/number/text slots that edit the AST in place (lossless round-trip, #104). when/forEachSeat render as collapsible nested step lists; conditions and value expressions are editable recursively. Steps can be added (any verb or block), deleted, reordered (up/down and drag within a list), and inserted between. Live validation runs validateGameSetup on every edit; failing slots get a wavy-red underline and each node shows its docPath+message beneath it. AST manipulation lives in pure, unit-tested helpers (gamerunner/edit.ts: create/insert/append/delete/move/changeVerb/setSlotValue, computeIsValid, zoneOptions) — the seam a future Blockly escape hatch (#101/#98) would target instead of the DOM. Adds gameSetupsApi.update (PUT) which persists the document plus the client-computed isValid (#110). Wires the Play dialog's "Open in editor" seam to navigate to the new route. Simplifications noted: seat/edge selectors are auto-assigned by context (each / current) rather than index-pickable; deal's count-as-expression is shown as a reset-to-number chip; drag reorder is within-list only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HsqGnGTkY8y87pBDBEJmf8
) Replace the #123 "Blueprints" tab stub with a real blueprint editor that shares the setup-editor page's single document and Save (decision #111). The view lists blueprints grouped by scope (seat/table/edge) with add/duplicate/ delete, edits each zone's name/role/geometry and #103 visibility preset, and previews the blueprint on a single-seat canvas with the #118 auto-fit ring. Blueprint/zone mutations live as pure, unit-tested helpers in a new `blueprintEdit.ts` (sibling to #123's `edit.ts`): add/delete/duplicate blueprint (fresh ids + unique displayName), add/delete zone, apply visibility preset, and dangling-reference detection over the setup program. All mutate the shared `$state` doc in place so the existing validity badge + Save just work — no second save path. Deleting a blueprint that setup steps still reference surfaces a warning dialog listing the dangling doc paths; delete stays allowed (#110/#111 delete-anytime) and live validation flags the leftover references. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HsqGnGTkY8y87pBDBEJmf8
The setup picker (#119) only browsed existing setups and the editor (#123) edits by id, so there was no way to actually create a setup — the picker's empty state was a dead end. Add the missing seam: - gameSetupsApi.create() → POST /projects/{id}/setups - emptyGameSetup() factory: a fresh, valid, empty document - "New setup" button in the picker creates a blank setup and opens it in the editor; per-row "Edit" opens any setup (so invalid ones are no longer unreachable dead ends) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HsqGnGTkY8y87pBDBEJmf8
|
This was referenced Jul 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Implements the first tranche of the Configurable game setup wayfinder map (#98): server persistence, the setup DSL + validator, the deterministic interpreter, seat-ring layout, and the end-to-end Play flow that runs a setup onto the tabletop.
Each commit maps to one issue and was implemented + reviewed test-first.
Commits / issues
c9cbe6cGameSetupentity (opaquejsonbdocument), migration, DTOs, project-scoped CRUD API at/projects/{projectId}/setups, service + 14 testsc9cbe6c$lib/gamerunnerDSL AST types +validateGameSetup(structure / referential / type-checking),SetupRunErrorunion, 48 testsfaec87fBlueprint = {id, scope, displayName, zones[]}+Zonegeometry/visibility; pureplaceZone/instantiateBlueprint(table/seat/edge fan-out); validator extended149947bcomputeViewState(state, viewer, visibilityMap) → MaskedState(faceVisibility + presence masking, omniscient);viewController.svelte.tsseat switcher2f16e0erunSetup(doc, {playerCount, options, seed, templates}) → {ok, state, visibility, trace} | {ok:false, error}; all 8 verbs, mulberry32 seeded RNG, fail-fast errors, replay trace387f505SeatLayoutStrategy(radial shipped); auto-fit radius from blueprint boxes + widen-only override; wired into the interpreterc22ba1bgameSetupsApi.list+SetupPickerDialog(validity badges, greyed invalid) + tabletop toolbar "Play" buttonbd05c1avalidateGameSetupgate,runSetup→ populate table as one undo entry, wipe-confirm, error UXDesign notes
GameSetup.Documentjsonb column (per decision Decide persistence and data model for game setup config #106), so Blueprint model: zone templates, schema, instantiation #115/Core interpreter: runSetup() function, 8 verbs, trace, error handling #117/Seat ring geometry: placeSeats, radial layout, auto-fit radius #118/Play dialog and execution: count/options input, launch setup #120 are TS-only.visibilitymap is produced by the interpreter and stored on the tabletop store but has no live UI consumer yet (Seat switcher and omniscient mode for solo testing #124); "Open in editor" in the Play dialog is a callback stub (editor is Setup editor UI: sentence-builder graphical editor #123); animated replay consumes the already-capturedtrace(Animated replay: animation registry, playback, undo support #121). These are intentional, not gaps.Verification
dotnet buildclean;dotnet test739 passed.npm run check0 errors; frontend suite 913 passed. The only failing tests (2) are pre-existing and unrelated —settings/page.server.test.tsfails identically onmaster./code-review(two-axis) run on the GameSetup persistence: entity, migration, CRUD API #113/DSL types and validation: GameSetup schema, validator function #114 base; nits applied.Not verified
The UI (#119/#120 dialogs and a real setup run) has not been clicked through in a running app — worth eyeballing the Play button/dialog styling against the dark tabletop chrome, and a full picker → configure → run → undo cycle.
🤖 Generated with Claude Code