The tester's second pass: the rule as written, advice that follows the board, the fold, the story on the replay page, and one click to begin

- Help said "walk, then act"; the rules let moves and cards come in any
  order, and now the help does too.
- The council's advice is derived from the live view while its panel is
  open, so a Number just played for movement is no longer recommended.
  It rests when the turn is not yours; "ask again" had nothing to add.
- The landing puts the name and the ways in before the clip, and beside
  it on a desktop: at 1280x720 the play button and Create/Join are above
  the fold.
- The replay page opens with the same deed the share card promises, and
  "play free" stands beside the reel instead of after the history.
- In hotseat the "pass the device" scrim stood above the roll-off, so the
  first click on "begin" landed on the handoff card. The handoff waits
  for the dice.

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-16 21:43:12 -04:00
co-authored by Claude Fable 5.1
parent 3d88f629b7
commit 4c2a253723
4 changed files with 90 additions and 43 deletions
+68 -39
View File
@@ -1388,12 +1388,17 @@
}
// The council of three: what each temperament would do from your seat.
let councilHints = $state<import("./hints").TableHint[] | null>(null);
// Its advice follows the board while the panel is open — a card just
// played is no longer recommended — and rests while it is not your turn.
let councilOpen = $state(false);
let hintsModule = $state<typeof import("./hints") | null>(null);
const councilHints = $derived(
councilOpen && isYourTurn && view && hintsModule ? hintsModule.tableHints(view) : null,
);
async function askCouncil() {
if (!view) return;
if (councilHints !== null) councilUnasked = false; // asked again: theirs to keep
const { tableHints } = await import("./hints");
councilHints = tableHints(view);
hintsModule ??= await import("./hints");
councilOpen = true;
}
/** "Play now": one tap from the landing to a game against a clockwork
@@ -1466,7 +1471,7 @@
/** The unasked advice is for that one turn: it leaves when the turn does. */
let councilUnasked = $state(false);
$effect(() => {
if (!view || !isYourTurn || prefs.coached || view.turn.round !== 1 || counciled || councilHints !== null) return;
if (!view || !isYourTurn || prefs.coached || view.turn.round !== 1 || counciled || councilOpen) return;
counciled = true;
councilUnasked = true;
void askCouncil();
@@ -1476,7 +1481,7 @@
// touches it would leave this effect with nothing to wake it.
const stillFirstTurn = isYourTurn && view?.turn.round === 1;
if (!councilUnasked || stillFirstTurn) return;
councilHints = null;
councilOpen = false;
councilUnasked = false;
});
@@ -2235,7 +2240,7 @@
<button class="hint-cancel setup-cancel" onclick={() => local.cancelSetup()}>never mind</button>
</div>
</div>
{:else if local.handoffTo}
{:else if local.handoffTo && !openingRolls}
<div class="scrim-handoff" role="button" tabindex="0"
onclick={() => local.takeSeat()} onkeydown={(e) => e.key === "Enter" && local.takeSeat()}>
<div class="handoff-card">
@@ -2248,39 +2253,45 @@
{#if !net.roomId && !local.active}
<section class="boxlid">
<div class="boxlid-inner">
<div class="boxlid-title">Wiz-War</div>
<div class="boxlid-pitch">Steal treasure. Blast through walls. Outwit your friends.</div>
<div class="boxlid-tag">A turn-based wizard board game for two to six, free in your browser, with
friends or clockwork opponents. Carry home two treasures that aren't yours, or be the last wizard standing.</div>
<button class="about-link" onclick={() => { helpTab = "about"; showHelp = true; }}>
about this game — a labor of love
</button>
<a class="hero" href="/clips" aria-label="watch clips from the labyrinth">
<img src="/hero.jpg" alt="Through a wizard's eyes: an ambush watches its corridor through a wormhole" />
</a>
<a class="about-link clips-link" href="/clips">watch clips from the labyrinth</a>
<label class="field">
<span>Your wizard's name</span>
<input bind:value={name} maxlength="20" placeholder="e.g. Mordecai" />
</label>
<button class="stamp primary play-now" disabled={!name.trim() || playNowPending !== null} onclick={playNow}>
{playNowPending ? "Flipping the boards…" : "Play now against a clockwork wizard"}
</button>
<div class="boxlid-actions">
<button class="stamp" disabled={!name.trim()} onclick={() => { setPref("wizardName", name.trim()); net.create(name); }}>
Create a game
</button>
<span class="or">or join one</span>
<input class="code" bind:value={joinCode} maxlength="4" placeholder="CODE" />
<button class="stamp" disabled={!name.trim() || !joinCode.trim()} onclick={() => { setPref("wizardName", name.trim()); net.join(joinCode, name); }}>
Join
<div class="boxlid-inner landing">
<div class="lid-head">
<div class="boxlid-title">Wiz-War</div>
<div class="boxlid-pitch">Steal treasure. Blast through walls. Outwit your friends.</div>
<div class="boxlid-tag">A turn-based wizard board game for two to six, free in your browser, with
friends or clockwork opponents. Carry home two treasures that aren't yours, or be the last wizard standing.</div>
<button class="about-link" onclick={() => { helpTab = "about"; showHelp = true; }}>
about this game — a labor of love
</button>
</div>
<div class="gallery-row">
<button class="hint-cancel" disabled={!joinCode.trim()} onclick={() => net.watch(joinCode)}>
👁 …or just watch that game from the Peanut Gallery
<div class="lid-play">
<label class="field">
<span>Your wizard's name</span>
<input bind:value={name} maxlength="20" placeholder="e.g. Mordecai" />
</label>
<button class="stamp primary play-now" disabled={!name.trim() || playNowPending !== null} onclick={playNow}>
{playNowPending ? "Flipping the boards…" : "Play now against a clockwork wizard"}
</button>
<div class="boxlid-actions">
<button class="stamp" disabled={!name.trim()} onclick={() => { setPref("wizardName", name.trim()); net.create(name); }}>
Create a game
</button>
<span class="or">or join one</span>
<input class="code" bind:value={joinCode} maxlength="4" placeholder="CODE" />
<button class="stamp" disabled={!name.trim() || !joinCode.trim()} onclick={() => { setPref("wizardName", name.trim()); net.join(joinCode, name); }}>
Join
</button>
</div>
<div class="gallery-row">
<button class="hint-cancel" disabled={!joinCode.trim()} onclick={() => net.watch(joinCode)}>
👁 …or just watch that game from the Peanut Gallery
</button>
</div>
</div>
<div class="lid-look">
<a class="hero" href="/clips" aria-label="watch clips from the labyrinth">
<img src="/hero.jpg" alt="Through a wizard's eyes: an ambush watches its corridor through a wormhole" />
</a>
<a class="about-link clips-link" href="/clips">watch clips from the labyrinth</a>
</div>
<details class="more-ways" open={local.hasSave()}>
@@ -2874,7 +2885,7 @@
{#if councilHints}
<div class="slip council">
<div class="council-head">The council considers your position…
<button class="hint-cancel" onclick={() => (councilHints = null)}>enough</button>
<button class="hint-cancel" onclick={() => { councilOpen = false; councilUnasked = false; }}>enough</button>
</div>
{#if councilHints.length > 1 && councilHints.every((h) => h.advice === councilHints![0]!.advice)}
<div class="council-row"><span class="council-name">The council agrees:</span> {councilHints[0]!.advice}</div>
@@ -2887,7 +2898,6 @@
</div>
{/each}
{/if}
<button class="stamp tiny" onclick={askCouncil}>ask again</button>
</div>
{/if}
{#if isYourTurn && view.players.some((p) => p.alive && p.id !== view.you &&
@@ -3538,6 +3548,25 @@
}
.hero { display: block; margin: 0.6rem auto 0; max-width: 24rem; border-radius: 6px; overflow: hidden;
border: 1.5px solid #43331f; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35); }
/* The landing: the name and the ways in come before the clip on a
* phone, and stand beside it on a desktop, so the play button is never
* below the fold. */
.lid-look { margin-top: 1.2rem; }
@media (min-width: 901px) {
.boxlid-inner.landing {
max-width: min(54rem, 100%);
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
grid-template-areas: "head head" "play look" "more more";
column-gap: 2.2rem;
padding-top: 1.8rem;
}
.lid-head { grid-area: head; }
.lid-play { grid-area: play; }
.lid-look { grid-area: look; margin-top: 0; }
.boxlid-inner.landing .more-ways { grid-area: more; }
.boxlid-inner.landing .hero { margin-top: 0; }
}
.hero img { display: block; width: 100%; }
.play-now { display: block; margin: 0.2rem auto 0.8rem; font-size: 0.95rem; padding: 0.6rem 1.2rem; }
.more-ways { margin-top: 1.2rem; }