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",
|
"cache-control": "public, max-age=60",
|
||||||
"access-control-allow-origin": "*",
|
"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;
|
return;
|
||||||
}
|
}
|
||||||
const watch = url.match(/^\/watch\/([a-z0-9]{4,20})(\/og\.png)?$/);
|
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.
|
// 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() {
|
async function askCouncil() {
|
||||||
if (!view) return;
|
if (!view) return;
|
||||||
if (councilHints !== null) councilUnasked = false; // asked again: theirs to keep
|
hintsModule ??= await import("./hints");
|
||||||
const { tableHints } = await import("./hints");
|
councilOpen = true;
|
||||||
councilHints = tableHints(view);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** "Play now": one tap from the landing to a game against a clockwork
|
/** "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. */
|
/** The unasked advice is for that one turn: it leaves when the turn does. */
|
||||||
let councilUnasked = $state(false);
|
let councilUnasked = $state(false);
|
||||||
$effect(() => {
|
$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;
|
counciled = true;
|
||||||
councilUnasked = true;
|
councilUnasked = true;
|
||||||
void askCouncil();
|
void askCouncil();
|
||||||
@@ -1476,7 +1481,7 @@
|
|||||||
// touches it would leave this effect with nothing to wake it.
|
// touches it would leave this effect with nothing to wake it.
|
||||||
const stillFirstTurn = isYourTurn && view?.turn.round === 1;
|
const stillFirstTurn = isYourTurn && view?.turn.round === 1;
|
||||||
if (!councilUnasked || stillFirstTurn) return;
|
if (!councilUnasked || stillFirstTurn) return;
|
||||||
councilHints = null;
|
councilOpen = false;
|
||||||
councilUnasked = false;
|
councilUnasked = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2235,7 +2240,7 @@
|
|||||||
<button class="hint-cancel setup-cancel" onclick={() => local.cancelSetup()}>never mind</button>
|
<button class="hint-cancel setup-cancel" onclick={() => local.cancelSetup()}>never mind</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if local.handoffTo}
|
{:else if local.handoffTo && !openingRolls}
|
||||||
<div class="scrim-handoff" role="button" tabindex="0"
|
<div class="scrim-handoff" role="button" tabindex="0"
|
||||||
onclick={() => local.takeSeat()} onkeydown={(e) => e.key === "Enter" && local.takeSeat()}>
|
onclick={() => local.takeSeat()} onkeydown={(e) => e.key === "Enter" && local.takeSeat()}>
|
||||||
<div class="handoff-card">
|
<div class="handoff-card">
|
||||||
@@ -2248,39 +2253,45 @@
|
|||||||
|
|
||||||
{#if !net.roomId && !local.active}
|
{#if !net.roomId && !local.active}
|
||||||
<section class="boxlid">
|
<section class="boxlid">
|
||||||
<div class="boxlid-inner">
|
<div class="boxlid-inner landing">
|
||||||
<div class="boxlid-title">Wiz-War</div>
|
<div class="lid-head">
|
||||||
<div class="boxlid-pitch">Steal treasure. Blast through walls. Outwit your friends.</div>
|
<div class="boxlid-title">Wiz-War</div>
|
||||||
<div class="boxlid-tag">A turn-based wizard board game for two to six, free in your browser, with
|
<div class="boxlid-pitch">Steal treasure. Blast through walls. Outwit your friends.</div>
|
||||||
friends or clockwork opponents. Carry home two treasures that aren't yours, or be the last wizard standing.</div>
|
<div class="boxlid-tag">A turn-based wizard board game for two to six, free in your browser, with
|
||||||
<button class="about-link" onclick={() => { helpTab = "about"; showHelp = true; }}>
|
friends or clockwork opponents. Carry home two treasures that aren't yours, or be the last wizard standing.</div>
|
||||||
about this game — a labor of love
|
<button class="about-link" onclick={() => { helpTab = "about"; showHelp = true; }}>
|
||||||
</button>
|
about this game — a labor of love
|
||||||
<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
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="gallery-row">
|
<div class="lid-play">
|
||||||
<button class="hint-cancel" disabled={!joinCode.trim()} onclick={() => net.watch(joinCode)}>
|
<label class="field">
|
||||||
👁 …or just watch that game from the Peanut Gallery
|
<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>
|
</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>
|
</div>
|
||||||
|
|
||||||
<details class="more-ways" open={local.hasSave()}>
|
<details class="more-ways" open={local.hasSave()}>
|
||||||
@@ -2874,7 +2885,7 @@
|
|||||||
{#if councilHints}
|
{#if councilHints}
|
||||||
<div class="slip council">
|
<div class="slip council">
|
||||||
<div class="council-head">The council considers your position…
|
<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>
|
</div>
|
||||||
{#if councilHints.length > 1 && councilHints.every((h) => h.advice === councilHints![0]!.advice)}
|
{#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>
|
<div class="council-row"><span class="council-name">The council agrees:</span> {councilHints[0]!.advice}</div>
|
||||||
@@ -2887,7 +2898,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
<button class="stamp tiny" onclick={askCouncil}>ask again</button>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if isYourTurn && view.players.some((p) => p.alive && p.id !== view.you &&
|
{#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;
|
.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); }
|
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%; }
|
.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; }
|
.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; }
|
.more-ways { margin-top: 1.2rem; }
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
actor: string;
|
actor: string;
|
||||||
round: number;
|
round: number;
|
||||||
whole?: boolean;
|
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 data = $state<ShareSteps | null>(null);
|
||||||
let failed = $state(false);
|
let failed = $state(false);
|
||||||
@@ -32,7 +34,14 @@
|
|||||||
<a class="share-brand" href="/">WIZ-WAR</a>
|
<a class="share-brand" href="/">WIZ-WAR</a>
|
||||||
{#if data}
|
{#if data}
|
||||||
<div class="share-title">
|
<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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</header>
|
</header>
|
||||||
@@ -51,6 +60,9 @@
|
|||||||
onclose={() => (reelKey += 1)} />
|
onclose={() => (reelKey += 1)} />
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="share-play">
|
||||||
|
<a class="share-cta" href="/">▶ play free — two to six wizards enter the maze</a>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<footer class="share-foot">
|
<footer class="share-foot">
|
||||||
@@ -61,7 +73,6 @@
|
|||||||
transcribed, every wall verified, replays rebuilt move by move from
|
transcribed, every wall verified, replays rebuilt move by move from
|
||||||
the game's own ledger.
|
the game's own ledger.
|
||||||
</p>
|
</p>
|
||||||
<a class="share-cta" href="/">▶ play free — two to six wizards enter the maze</a>
|
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -79,6 +90,7 @@
|
|||||||
.share-head {
|
.share-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
width: min(46rem, 100%);
|
width: min(46rem, 100%);
|
||||||
margin-bottom: 0.6rem;
|
margin-bottom: 0.6rem;
|
||||||
@@ -97,6 +109,9 @@
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: #e9e1cb;
|
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. */
|
/* The reel is a modal everywhere else; here it stands in the page. */
|
||||||
.share-stage { width: min(46rem, 100%); }
|
.share-stage { width: min(46rem, 100%); }
|
||||||
.share-stage :global(.replay-scrim) {
|
.share-stage :global(.replay-scrim) {
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export const HOW_TO_PLAY: RulesSection[] = [
|
|||||||
title: "Winning",
|
title: "Winning",
|
||||||
body: [
|
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.",
|
"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