From e5d82a94af2832860442b1199e794e58623471f2 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Mon, 7 Sep 2026 12:50:28 -0500 Subject: [PATCH] A first game's first turn: the step light, the goal, and a held opening Three strangers this week pressed play, looked at the board for a minute, and closed it. What they saw: a small maze under a roll-off card, a column of prose, seven strange cards, and nothing that said where they were or what to tap. Now, on a browser's first game, the squares a stride can reach glow and the rest of the maze falls into shadow, with one line hung over the wizard for the first stride. The turn banner says what winning is for the first two rounds. And when the clockwork rolls first, its opening turn waits behind the roll-off card instead of playing out unwatched, then arrives a beat at a time. The step light switches off once that first game is won, and lives in preferences for anyone who wants it back. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG --- packages/web/src/App.svelte | 34 ++- packages/web/src/Board.svelte | 43 ++++ packages/web/src/net.svelte.ts | 357 +++++++++++++++++-------------- packages/web/src/prefs.svelte.ts | 9 +- 4 files changed, 277 insertions(+), 166 deletions(-) diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 314189e..184950a 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -223,7 +223,7 @@ }); const openingRolls = $derived(net.openingRolls ?? local.openingRolls); function dismissRolls() { - net.openingRolls = null; + net.dismissRolls(); local.openingRolls = null; } /** A face-up card being peeked at from the scoresheet (view-only). */ @@ -1405,6 +1405,8 @@ const room = net.roomId; if (!room || finishedRoom === room) return; finishedRoom = room; + // The step light is for a first game; the second is played by sight. + if (prefs.finishedGames === 0) setPref("stepLight", false); setPref("finishedGames", prefs.finishedGames + 1); if (!prefs.tierChosen && prefs.botTier === "apprentice") setPref("botTier", "adept"); }); @@ -1479,7 +1481,26 @@ // answer to "how can he even see me?" when sight ran through a warp mouth. const sightTrace = $derived(view ? stackSightTrace(view) : null); + /** The step light: with no card in hand, your square and the squares a + * stride reaches are lit and the rest of the maze falls into shadow. */ + const stepLight = $derived( + !!view && !!me && prefs.stepLight && isYourTurn && !view.turn.actionsEnded && !selectedCard && + !net.spectating && !youMustRespond && view.turn.movementAllowance - view.turn.movementUsed > 0, + ); + const stepCells = $derived.by(() => { + if (!stepLight || !view || !me) return []; + return SIDES.map((side) => stepTarget(view!.board, me!.position, side)) + .filter((t) => t.kind !== "blocked") + .map((t) => cellKey(t.to)); + }); + /** The first stride of a first game gets one line on the board itself. */ + const stepCaption = $derived( + stepLight && view && me && view.turn.round === 1 && view.turn.movementUsed === 0 + ? { cell: me.position, text: "This is you — tap a lit square to walk" } + : null, + ); const litCells = $derived.by(() => { + if (view && me && !selectedDef && stepLight) return new Set([cellKey(me.position), ...stepCells]); if (!view || !selectedDef || !yourMoment) return null; if (ambushVia || ambushSpell || ambushTrigger) return null; // ambushes aim at the future if (!me) return null; @@ -1833,6 +1854,11 @@ onchange={(e) => setPref("handLeft", e.currentTarget.checked)} /> Hand at the left: your cards stand in a column beside the board (wide screens) +