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
+4 -1
View File
@@ -325,7 +325,10 @@ const httpServer = createServer((req, res) => {
"cache-control": "public, max-age=60",
"access-control-allow-origin": "*",
});
res.end(JSON.stringify({ steps: data.steps, actor: data.actor, round: data.round, whole: data.whole === true }));
res.end(JSON.stringify({
steps: data.steps, actor: data.actor, round: data.round, whole: data.whole === true,
headline: data.whole ? null : headlineOf(data),
}));
return;
}
const watch = url.match(/^\/watch\/([a-z0-9]{4,20})(\/og\.png)?$/);
+43 -14
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,7 +2253,8 @@
{#if !net.roomId && !local.active}
<section class="boxlid">
<div class="boxlid-inner">
<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
@@ -2256,10 +2262,8 @@
<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>
</div>
<div class="lid-play">
<label class="field">
<span>Your wizard's name</span>
<input bind:value={name} maxlength="20" placeholder="e.g. Mordecai" />
@@ -2282,6 +2286,13 @@
👁 …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()}>
<summary>More ways to play — pass one device around, or claim a transferred seat</summary>
@@ -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; }
+17 -2
View File
@@ -15,6 +15,8 @@
actor: string;
round: number;
whole?: boolean;
/** The turn's deed in one line, as the share card says it. */
headline?: string | null;
}
let data = $state<ShareSteps | null>(null);
let failed = $state(false);
@@ -32,7 +34,14 @@
<a class="share-brand" href="/">WIZ-WAR</a>
{#if data}
<div class="share-title">
{#if data.whole}The whole tale{data.actor ? ` — ${data.actor} triumphant` : ""}{:else}Instant replay — {data.actor}'s turn{data.round ? ` (round ${data.round})` : ""}{/if}
{#if data.whole}
<span class="share-deed">The whole tale{data.actor ? ` — ${data.actor} triumphant` : ""}</span>
{:else if data.headline}
<span class="share-deed">{data.headline}</span>
<span class="share-sub">an instant replay of {data.actor}'s turn{data.round ? `, round ${data.round}` : ""}</span>
{:else}
<span class="share-deed">Instant replay — {data.actor}'s turn{data.round ? ` (round ${data.round})` : ""}</span>
{/if}
</div>
{/if}
</header>
@@ -51,6 +60,9 @@
onclose={() => (reelKey += 1)} />
{/key}
</div>
<div class="share-play">
<a class="share-cta" href="/">▶ play free — two to six wizards enter the maze</a>
</div>
{/if}
<footer class="share-foot">
@@ -61,7 +73,6 @@
transcribed, every wall verified, replays rebuilt move by move from
the game's own ledger.
</p>
<a class="share-cta" href="/">▶ play free — two to six wizards enter the maze</a>
</footer>
</div>
@@ -79,6 +90,7 @@
.share-head {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 1rem;
width: min(46rem, 100%);
margin-bottom: 0.6rem;
@@ -97,6 +109,9 @@
font-size: 0.85rem;
color: #e9e1cb;
}
.share-deed { display: block; }
.share-sub { display: block; text-transform: none; letter-spacing: 0.02em; color: #8d8672; font-size: 0.8rem; }
.share-play { width: min(46rem, 100%); margin-top: 0.8rem; text-align: center; }
/* The reel is a modal everywhere else; here it stands in the page. */
.share-stage { width: min(46rem, 100%); }
.share-stage :global(.replay-scrim) {
+1 -1
View File
@@ -95,7 +95,7 @@ export const HOW_TO_PLAY: RulesSection[] = [
title: "Winning",
body: [
"Carry two treasures that aren't yours home — one at a time, to your home square, and drop them there — or be the last wizard standing. Lose both of your own treasures to enemy homes and you're out.",
"A turn is: walk, then act. Step up to three squares, select a card and choose its target, and End turn. The banner above the board keeps count of your moves and your treasures home.",
"Move and play cards in any order, then end your turn. You have three steps and one attack a turn — a step, a spell, two more steps is a fine turn. The banner above the board keeps count of your moves and your treasures home.",
],
},
{