From ee630276b34f5160f6916ee59b4c88f537460ba0 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Sat, 10 Oct 2026 18:29:49 -0400 Subject: [PATCH] Easier to make a move, and plainer words around the game From feedback on the BoardGameGeek thread. The hands are framed and labelled as yours, so they no longer read as sitting under your opponent's columns. A move can be typed: the first letter goes to the left hand, the second to the right, K stabs, a dash rests, Backspace takes one back and Enter seals. The chosen pair, what it will do, any warning and the button now sit in one row that stays in view. A clap with one hand is warned of before the turn is sealed. The button says what the player does: Seal the turn. The chronicle reads oldest first and scrolls to the newest line. Headings, tabs and notes lose the label-and-colon construction and the abstract headings; the chronicle's imagery stays. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0141G6xqLeNRYEtviLWSB5Up --- src/lib/components/Board.svelte | 155 ++++++++++++++++++++++----- src/lib/components/Chronicle.svelte | 16 ++- src/lib/components/Hall.svelte | 4 +- src/lib/components/HandPicker.svelte | 10 +- src/lib/components/MobileBar.svelte | 2 +- src/lib/components/Tally.svelte | 2 +- src/lib/game/duel.svelte.ts | 29 +++++ src/lib/game/duel.test.ts | 25 +++++ src/routes/guide/+page.svelte | 12 +-- 9 files changed, 213 insertions(+), 42 deletions(-) diff --git a/src/lib/components/Board.svelte b/src/lib/components/Board.svelte index ec94fce..add8844 100644 --- a/src/lib/components/Board.svelte +++ b/src/lib/components/Board.svelte @@ -11,7 +11,7 @@ import { castKey, listText, tokensText, type Duel } from '$lib/game/duel.svelte'; import { seatColor } from '$lib/game/colors'; import { GESTURE_SHORT, glyph } from '$lib/game/glyphs'; - import { MAGIC_GESTURES } from '$lib/game/spells'; + import { MAGIC_GESTURES, type Gesture } from '$lib/game/spells'; let { duel }: { duel: Duel } = $props(); @@ -20,8 +20,37 @@ const won = $derived(outcome?.winner === duel.me); /** Past three wizards the rail lists the others on a line each. */ const crowded = $derived(duel.others.length >= 3); + /** The button's word for what the player is about to do. */ + const sealLabel = $derived( + duel.moved ? 'Waiting' : duel.sending ? 'Sending' : duel.surrendering ? 'Surrender' : duel.timeStopped ? 'Act in the stopped moment' : 'Seal the turn' + ); + + /** Letters typed anywhere on the board fill the hands, left then right; Enter seals the turn. */ + const KEYS: Record = { f: 'F', p: 'P', s: 'S', w: 'W', d: 'D', c: 'C', k: '>', '>': '>', '-': '-', '.': '-', ' ': '-' }; + function onKey(e: KeyboardEvent) { + if (e.metaKey || e.ctrlKey || e.altKey) return; + const el = e.target as HTMLElement | null; + if (el && (el.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(el.tagName))) return; + if (outcome || duel.out || duel.moved || duel.spectating) return; + if (e.key === 'Enter') { + if (duel.ready) { + e.preventDefault(); + duel.reveal(); + } + return; + } + if (e.key === 'Backspace') { + e.preventDefault(); + duel.untype(); + return; + } + const g = KEYS[e.key.toLowerCase()]; + if (g && duel.typeGesture(g)) e.preventDefault(); + } + +
@@ -54,7 +83,7 @@

Time stands still. Take your extra turn.

{duel.foe.name} cannot move, see this, or resist anything you do. Last turn's enchantments do not bind you.

{:else} -

Turn {duel.turnNumber}: write your gestures{#if duel.hasted}, twice{/if}

+

Your gestures for turn {duel.turnNumber}{#if duel.hasted}, twice over{/if}

{/if} {#if duel.threats.length && !duel.timeStopped} @@ -111,7 +140,7 @@ {#if duel.hasted} -

You are hastened: a second pair of gestures follows the first, and both take effect together.

+

You are hastened, so a second pair of gestures follows the first, and both take effect together.

@@ -191,22 +220,35 @@ {#if duel.remote?.error}

{duel.remote.error}

{/if} - {#if duel.intentText && !duel.moved} -

{duel.intentText}

- {/if} - {#if duel.moved} -

Your move is in. Waiting for {duel.awaitingText}.

- {/if} - {#if duel.surrendering} -

Both palms at once is surrender. You will lose the duel.

- {/if} - {#if duel.conflict} -

Those two spells share a gesture. A gesture can finish only one spell; let one of them pass.

- {/if} - - +
+
+ {#each duel.activeSets as set (set)} + + left {duel.drawn(set, 'left')} + right {duel.drawn(set, 'right')} + + {/each} +
+
+ {#if duel.moved} +

Your hands are still. Waiting for {duel.awaitingText}.

+ {:else if duel.intentText} +

{duel.intentText}

+ {/if} + {#if duel.surrendering} +

Both palms at once is surrender. You will lose the duel.

+ {:else if duel.loneClap && !duel.moved} +

A clap needs both hands; one hand clapping does nothing. Clap with both, or rest the other hand.

+ {/if} + {#if duel.conflict} +

Those two spells share a gesture, and a gesture can finish only one spell. Let one of them pass.

+ {/if} + {#if !duel.moved} +

Or type: F P S W D C, K to stab, a dash to rest; Backspace undoes, Enter seals.

+ {/if} +
+ +
{/if} {/if} @@ -378,8 +420,66 @@ margin: 1rem 0 0.6rem; } - .move > .reveal { - margin-top: 0.8rem; + /* Your hands, framed apart from the ledger's columns above them. */ + .hands { + background: var(--slate); + border: 1px solid var(--rule-strong); + border-radius: 6px; + padding: 0.7rem 0.9rem 0.9rem; + } + + /* The turn about to be sealed, kept in view while the hands and orders scroll. */ + .commit { + position: sticky; + bottom: 0; + z-index: 4; + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 0.4rem 1rem; + margin-top: 0.9rem; + padding: 0.6rem 0.2rem; + background: var(--slate-deep); + border-top: 1px solid var(--rule-strong); + } + + .pair { + display: flex; + flex-direction: column; + gap: 0.1rem; + } + + .hand-pair { + display: flex; + align-items: baseline; + gap: 0.35rem; + } + + .which { + font-size: 0.75rem; + font-style: italic; + color: var(--bone-faint); + } + + .pair .letter { + font-size: 1.5rem; + min-width: 1ch; + } + + .pair .letter.empty { + color: var(--bone-faint); + } + + .notes p { + margin: 0; + } + + .notes p + p { + margin-top: 0.2rem; + } + + .keys { + font-size: 0.8rem; } .verdict { @@ -407,8 +507,16 @@ grid-template-columns: minmax(0, 1fr); } - /* The sticky bar carries the reveal button on phones. */ - .move > .reveal { + /* On phones the bar at the foot of the screen carries the pair and the button. */ + .commit { + position: static; + grid-template-columns: minmax(0, 1fr); + border-top: 0; + } + + .commit .pair, + .commit .reveal, + .keys { display: none; } @@ -425,7 +533,6 @@ .waiting { color: var(--frost); - margin-top: 0.8rem; } .reveal.again { diff --git a/src/lib/components/Chronicle.svelte b/src/lib/components/Chronicle.svelte index 30a2a89..7ded357 100644 --- a/src/lib/components/Chronicle.svelte +++ b/src/lib/components/Chronicle.svelte @@ -1,4 +1,5 @@ -
+

Chronicle

{#if turns.length === 0} -

Nothing has happened yet. Choose a gesture for each hand and reveal them.

+

Nothing has happened yet. Choose a gesture for each hand and seal the turn.

{/if} {#each turns as [turn, entries] (turn)}
diff --git a/src/lib/components/Hall.svelte b/src/lib/components/Hall.svelte index 2d5dd84..4722154 100644 --- a/src/lib/components/Hall.svelte +++ b/src/lib/components/Hall.svelte @@ -264,7 +264,7 @@ {/each} -

The ledger: every gesture both wizards have made, and the spells they finished.

+

A few turns of a duel. Each row is a turn, each letter one hand's gesture, and a spell is named under the gesture that finished it.

{#if seats.length || local} @@ -357,7 +357,7 @@

A rule read wrong, a bug, a duel you would like to tell of: write to eric@ericwagoner.com, @kestrelsnest.social on Bluesky, or @eric@toots.kestrelsnest.social on Mastodon. The keeper of this hall roosts at kestrelsnest.social.

{/snippet} - +