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:
co-authored by
Claude Fable 5.1
parent
3d88f629b7
commit
4c2a253723
@@ -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)?$/);
|
||||
|
||||
+68
-39
@@ -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; }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user