Learn the pieces: fifteen scenes and a checkmate, each a small position with a move to make and a word on what came of it

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CCLa2PaEeCj8njQZSWGqHx
This commit is contained in:
Eric WagonerandClaude Fable 5.1 committed 2026-10-26 18:30:55 -04:00
1 parent 808c27e5d3
commit c8443c6f50
7 files changed
+595 -2

No files matched your search

+1
View File
@@ -65,6 +65,7 @@
<section id="first">
<h2>A first game</h2>
<p>Before that, <a href="/learn">learn the pieces</a>: fifteen short scenes, one a piece, each a small position with a move for you to make and a word on what came of it.</p>
<p>Play a drake first. It plays a fair game and never minds how long you take. Begin as you would at chess, with Warriors stepping forward and the knights of the land, the Unicorns and Heroes, coming out behind them. Gygax's own advice is to hold the Mage and the Paladin back until the lesser pieces are developed, and to keep your Dragon home. It starts directly above your King for a reason. A Dragon that flies off to raid leaves the King open to the other Dragon, which strikes the land from the air without ever coming down.</p>
<p>Watch the Basilisks. Anything you leave on the land directly above an enemy Basilisk is frozen on the spot, and a frozen piece may as well be off the board. The Cleric is stronger than it looks, since it is the only piece that plays the same on all three boards, and the Dwarves, if you do not hurry them forward, make good guards late in the game.</p>
<p>The full rules, piece by piece, are on the <a href={withRoom('/rules')}>rules page</a>, with Gygax's article beneath them. If a rule here reads wrong to you, or something misbehaves, <a href="mailto:eric@ericwagoner.com">send word</a>, or use <em>Report</em> in the masthead at the table, which pins what you saw to the move it happened on. The hall's <em>about this game</em> lists the other ways to reach the keeper.</p>
+202
View File
@@ -0,0 +1,202 @@
<script lang="ts">
// Learn the pieces: one scene a piece. The boards the scene needs, what to
// look at, the move made by the reader, and what came of it.
import { page } from '$app/state';
import LessonBoard from '$lib/components/LessonBoard.svelte';
import PiecePanel from '$lib/components/PiecePanel.svelte';
import { SCENES, sceneCells } from '$lib/game/lessons';
import { tell } from '$lib/game/chronicle';
import { createGame, playOn, square, squareName, type Cell, type Move } from '$lib/game';
import { kindOf, inCheck, hasLegalMove, other } from '$lib/game/board';
const room = $derived((page.url.searchParams.get('room') ?? '').toUpperCase());
const withRoom = (path: string) => (room ? `${path}?room=${room}` : path);
let n = $state(0);
const scene = $derived(SCENES[n]);
/** The scene's position, moved on by the reader or set again. */
let cells = $state<Cell[]>(sceneCells(SCENES[0]));
/** What the reader's move came to: the move told, and the scene's word on it. */
let outcome = $state<{ told: string; text: string; right: boolean } | null>(null);
let inspected = $state<number | null>(null);
function setScene(i: number) {
n = Math.max(0, Math.min(SCENES.length - 1, i));
cells = sceneCells(SCENES[n]);
outcome = null;
inspected = null;
}
function moved(from: number, to: number) {
const right = scene.moves.some(([a, b]) => square(a) === from && square(b) === to);
const played = playOn(cells, from, to);
const move: Move = { by: scene.side, from, to, kind: kindOf(cells[from]), captured: played.captured, afar: played.afar, promoted: played.promoted, check: inCheck(played.cells, other(scene.side)) };
const name = (s: typeof scene.side) => (s === scene.side ? 'You' : 'Scarlet');
let told = tell(cells, move, name).replace(/You’s/g, 'Your');
const mated = move.check && !hasLegalMove(played.cells, other(scene.side), 'printed');
if (mated) told += ' Checkmate.';
outcome = { told, text: right ? scene.after : (scene.aside ?? scene.after), right };
cells = played.cells;
}
/** The panel reads a game; this one is the scene's. */
const g = $derived.by(() => {
const s = createGame({ A: 'You', B: 'Scarlet' });
s.cells = cells;
s.toMove = scene.side;
return s;
});
</script>
<svelte:head>
<title>Wyrm: learn the pieces</title>
</svelte:head>
<main class="lesson">
<header>
{#if room}<a class="back" href={`/join/${room}`}>Back to your game</a>{' · '}{/if}<a class="back" href="/">Back to the hall</a> · <a class="back" href={withRoom('/guide')}>How to play</a> · <a class="back" href={withRoom('/rules')}>The rules</a>
<h1>Learn the pieces</h1>
<p class="lede">Fifteen pieces, one scene each, and a checkmate to finish. Every scene is a small position with something to see and one move to make. Tap any piece to read about it, as at the table.</p>
</header>
<nav class="scenes" aria-label="Scenes">
{#each SCENES as s, i (s.id)}
<button type="button" class="scene-tab" class:current={i === n} onclick={() => setScene(i)}>{s.title}</button>
{/each}
</nav>
<section class="stage">
<div class="boards">
<LessonBoard {cells} side={scene.side} levels={scene.levels} locked={outcome !== null} onMove={moved} onInspect={(at) => (inspected = at)} />
</div>
<div class="words">
<p class="eyebrow">Scene {n + 1} of {SCENES.length}</p>
<h2>{scene.title}</h2>
<p>{scene.before}</p>
{#if outcome}
<p class="told" class:right={outcome.right}>{outcome.told}</p>
<p>{outcome.text}</p>
<div class="ways">
<button type="button" class="quiet" onclick={() => setScene(n)}>Set the scene again</button>
{#if n < SCENES.length - 1}<button type="button" class="commit small" onclick={() => setScene(n + 1)}>Next: {SCENES[n + 1].title}</button>{:else}<a class="commit small" href="/">Back to the hall</a>{/if}
</div>
{:else}
<p class="task">{scene.task}</p>
<div class="ways">
{#if n > 0}<button type="button" class="quiet" onclick={() => setScene(n - 1)}>Back</button>{/if}
{#if n < SCENES.length - 1}<button type="button" class="quiet" onclick={() => setScene(n + 1)}>Skip</button>{/if}
</div>
{/if}
{#if inspected !== null && cells[inspected] !== ''}
<div class="panel">
<PiecePanel {g} at={inspected} mySide={scene.side} close={() => (inspected = null)} />
</div>
{/if}
</div>
</section>
</main>
<style>
.lesson {
max-width: 1280px;
margin: 0 auto;
padding: 1.5rem var(--gutter) 4rem;
}
.lesson a.back {
color: var(--frost);
font-size: 0.95rem;
}
h1 {
font-size: 2.4rem;
font-weight: 300;
margin: 0.5rem 0 0.3rem;
}
.lede {
color: var(--bone-dim);
max-width: 44em;
}
.scenes {
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
margin: 1rem 0 1.2rem;
}
.scene-tab {
background: none;
border: 1px solid var(--rule);
border-radius: 999px;
padding: 0.2rem 0.7rem;
font-size: 0.85rem;
color: var(--bone-dim);
}
.scene-tab.current {
border-color: var(--ember);
color: var(--ember);
}
.stage {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(300px, 420px);
gap: 1.5rem clamp(1.5rem, 4vw, 3.5rem);
align-items: start;
}
@media (max-width: 900px) {
.stage {
grid-template-columns: minmax(0, 1fr);
}
/* On a phone the words come first, so the reader knows what to look for before the boards. */
.words {
order: -1;
}
}
.words h2 {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.words p + p {
margin-top: 0.7rem;
}
.task {
color: var(--bone);
border-left: 2px solid var(--ember);
padding-left: 0.6rem;
font-weight: 500;
}
.told {
color: var(--bone);
border-left: 2px solid var(--frost);
padding-left: 0.6rem;
}
.told.right {
border-color: var(--ember);
}
.ways {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 0.8rem;
margin-top: 0.9rem;
align-items: center;
}
.ways .commit {
text-decoration: none;
}
.panel {
margin-top: 1rem;
}
</style>