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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
Eric Wagoner
2026-09-07 12:50:28 -05:00
co-authored by Claude Fable 5.1
parent 96c0c18d97
commit e5d82a94af
4 changed files with 277 additions and 166 deletions
+43
View File
@@ -33,6 +33,8 @@
markedCell = null,
markedCells = null,
litCells = null,
spotCells = [],
spotCaption = null,
markedSector = null,
ghostSlots = null,
onGhostClick,
@@ -65,6 +67,10 @@
markedCells?: { x: number; y: number }[] | null;
/** When set, squares NOT in this set dim — the targeting aid. */
litCells?: Set<string> | null;
/** The step light's squares: a pulsing rim on each. */
spotCells?: string[];
/** One line hung above a square — the first stride's cue. */
spotCaption?: { cell: { x: number; y: number }; text: string } | null;
/** Origin of a picked-up 5x5 sector: the whole sector outlines as taken. */
markedSector?: { x: number; y: number } | null;
/** Empty 5x5 slot origins a sector may land on — drawn as dashed ground
@@ -680,6 +686,23 @@
{/if}
{/each}
{/if}
<!-- the step light: the squares a stride reaches, rimmed and pulsing -->
{#each spotCells as key (key)}
{@const sx = Number(key.split(",")[0])}
{@const sy = Number(key.split(",")[1])}
<rect x={sx * CELL + 3} y={sy * CELL + 3} width={CELL - 6} height={CELL - 6} class="spot-cell" rx="5" />
{/each}
{#if spotCaption}
{@const cols = Math.max(...Object.keys(view.board.cells).map((k) => Number(k.split(",")[0]))) + 1}
{@const cw = spotCaption.text.length * 6.6 + 20}
{@const cx = Math.min(Math.max(spotCaption.cell.x * CELL + CELL / 2, cw / 2 + 4), cols * CELL - cw / 2 - 4)}
{@const above = spotCaption.cell.y > 0}
{@const cy = above ? spotCaption.cell.y * CELL - 14 : (spotCaption.cell.y + 1) * CELL + 14}
<g class="spot-caption" transform={`translate(${cx}, ${cy})`}>
<rect x={-cw / 2} y={-12} width={cw} height={24} rx="12" />
<text x="0" y="4">{spotCaption.text}</text>
</g>
{/if}
<!-- FEAR's bubble: the three-space diamond, through walls, wrapping the rim -->
{#each fearAura as key (key)}
{@const bx = Number(key.split(",")[0])}
@@ -823,6 +846,26 @@
fill: rgba(12, 9, 5, 0.55);
pointer-events: none;
}
.spot-cell {
fill: none;
stroke: #f2cf5b;
stroke-width: 3;
pointer-events: none;
animation: spot-pulse 1.3s ease-in-out infinite;
}
@keyframes spot-pulse {
0%, 100% { opacity: 0.35; }
50% { opacity: 1; }
}
.spot-caption { pointer-events: none; }
.spot-caption rect { fill: rgba(28, 22, 12, 0.92); stroke: #f2cf5b; stroke-width: 1.5; }
.spot-caption text {
fill: #f7ecc8;
font-size: 12px;
font-family: "Oswald", "Helvetica Neue", Arial, sans-serif;
letter-spacing: 0.02em;
text-anchor: middle;
}
.marked-cell {
fill: rgba(211, 133, 43, 0.18);
stroke: #d3852b;