Writing for players: the editing passes' lessons, and the template written by them

The conventions gain a list of the habits players read as generated text
(a semicolon in every paragraph, statement-colon-explanation, "The" plus
an abstract noun as a heading, clipped fragments in threes, closing
aphorisms, "X, not Y", the page talking about itself), and of the errors
that come from inherited text: hidden information where there is none,
points where there are none, "both" at a larger table, unchecked claims,
and ledger-stored words reworded. new-game.sh points at the list.

The template follows it: the about link loses 'a labor of love', the
tabs are Origins and Games so far, and the hall, lobby, room, report
slip, table talk, guide and demo board read as Waving Hands' and
Hnefatafl's now do. The landing page and family.json carry the two
games' new taglines and lose their semicolons and the tip jar's aphorism.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141G6xqLeNRYEtviLWSB5Up
This commit is contained in:
Eric Wagoner
2026-10-11 10:34:08 -04:00
co-authored by Claude Opus 5.5
parent f5b7416a9e
commit bbbe8ec312
13 changed files with 95 additions and 52 deletions
+3 -3
View File
@@ -7,21 +7,21 @@
<meta name="theme-color" content="#121a20" />
<title>__NAME__</title>
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta name="description" content="__NAME__: a game between friends, free in the browser." />
<meta name="description" content="__NAME__, a game between friends, free in the browser." />
<link rel="canonical" href="https://__DOMAIN__/" />
<link rel="icon" href="/favicon.ico" sizes="32x32" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="__NAME__" />
<meta property="og:title" content="__NAME__" />
<meta property="og:description" content="__NAME__: a game between friends, free in the browser." />
<meta property="og:description" content="__NAME__, a game between friends, free in the browser." />
<meta property="og:url" content="https://__DOMAIN__/" />
<meta property="og:image" content="https://__DOMAIN__/og.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="__NAME__" />
<meta name="twitter:description" content="__NAME__: a game between friends, free in the browser." />
<meta name="twitter:description" content="__NAME__, a game between friends, free in the browser." />
<meta name="twitter:image" content="https://__DOMAIN__/og.png" />
%sveltekit.head%
</head>
+2 -2
View File
@@ -67,8 +67,8 @@
</section>
{:else}
<section class="move">
<h2>Round {g.rounds.length + 1}: name a number</h2>
<p class="muted">Every player lays a card face down; the highest card nobody else laid takes the point. First to {TARGET}.</p>
<h2>Name a number for round {g.rounds.length + 1}</h2>
<p class="muted">Every player lays a card face down, and the highest card nobody else laid takes the point. The first to {TARGET} points wins.</p>
<div class="picks">
{#each Array.from({ length: HIGHEST }, (_, i) => i + 1) as n (n)}
<button type="button" class="pick" class:chosen={pick === n} disabled={room.moved} onclick={() => (pick = n)}>{n}</button>
+9 -9
View File
@@ -196,9 +196,9 @@
<section class="lid" class:returning={active.length > 0}>
<div class="lid-head">
<h1>__NAME__</h1>
<p class="pitch">One line that says what the game is.</p>
<p class="pitch">One sentence that says what the game is, in its own world.</p>
<p class="tag">Two or three sentences for someone who has never heard of it: what you do on a turn, what makes it fun, and that it is free to play here against the bot or with friends.</p>
<p class="links"><a href="/guide">how to play</a> · <a href="/rules">the rules</a> · <a class="about-link" href="#about">about this game — a labor of love</a> · <button type="button" class="link" onclick={() => (prefsOpen = true)}>preferences</button></p>
<p class="links"><a href="/guide">how to play</a> · <a href="/rules">the rules</a> · <a class="about-link" href="#about">about this game</a> · <button type="button" class="link" onclick={() => (prefsOpen = true)}>preferences</button></p>
<Preferences bind:open={prefsOpen} />
</div>
@@ -232,7 +232,7 @@
<button type="submit" class="quiet" disabled={!ready || code.trim().length < 4}>Join</button>
</form>
</div>
<p class="muted small">A code opens the door either way: take a seat if one is free, or watch from the Peanut Gallery.</p>
<p class="muted small">With a code you can take a seat if one is free, or watch from the Peanut Gallery.</p>
<form class="joinform" onsubmit={claim}>
<span class="or">or bring a seat from another device</span>
<input type="text" bind:value={phrase} placeholder="the four-word phrase" autocomplete="off" aria-label="transfer phrase" />
@@ -309,7 +309,7 @@
<div class="about-body">
<p>Where the game comes from, who made the original and when, and how the keeper of this page first met it.</p>
<p>What this version keeps and what it changes. Where the rules text it follows lives (the <a href="/rules">rules page</a>), and any borrowed art or text with its notice.</p>
<p>It is free and keeps no accounts. A game between people lives on a small server as an append-only ledger of moves, so it can be replayed from its first move, and each player is shown only what the rules let them see.</p>
<p>It is free and keeps no accounts. A game between people lives on a small server as an append-only ledger of moves, so it can be replayed from its first move. Each player is shown only what the rules let them see (drop this sentence if nothing in the game is hidden).</p>
</div>
{/snippet}
{#snippet aboutTally()}
@@ -329,15 +329,15 @@
</ul>
{/if}
<p class="hosting">
The games of Kestrel's Hall are free and will stay free. They run on small servers that cost a few dollars a month; anyone who wants to chip in toward the hosting can at
<a href="https://ko-fi.com/kestrelsnest" target="_blank" rel="noreferrer">ko-fi.com/kestrelsnest</a>. Nothing is unlocked by it and nothing is withheld without it.
The games of Kestrel's Hall are free and will stay free. They run on small servers that cost a few dollars a month, and anyone who wants to chip in toward the hosting can do so at
<a href="https://ko-fi.com/kestrelsnest" target="_blank" rel="noreferrer">ko-fi.com/kestrelsnest</a>. Every game stays whole either way.
</p>
<p class="made">__NAME__ is its designer's.</p>
</div>
{/snippet}
{#snippet aboutWord()}
<div class="about-body">
<p>A rule read wrong, a bug, a game you would like to tell of: write to <a href="mailto:eric@ericwagoner.com">eric@ericwagoner.com</a>, <a href="https://bsky.app/profile/kestrelsnest.social" target="_blank" rel="noreferrer">@kestrelsnest.social</a> on Bluesky, or <a href="https://toots.kestrelsnest.social/@eric" target="_blank" rel="noreferrer">@eric@toots.kestrelsnest.social</a> on Mastodon. The keeper of this hall roosts at <a href="https://kestrelsnest.social" target="_blank" rel="noreferrer">kestrelsnest.social</a>.</p>
<p>If a rule reads wrong, something breaks, or you have a game to tell of, write to <a href="mailto:eric@ericwagoner.com">eric@ericwagoner.com</a>, <a href="https://bsky.app/profile/kestrelsnest.social" target="_blank" rel="noreferrer">@kestrelsnest.social</a> on Bluesky, or <a href="https://toots.kestrelsnest.social/@eric" target="_blank" rel="noreferrer">@eric@toots.kestrelsnest.social</a> on Mastodon. The keeper of this hall roosts at <a href="https://kestrelsnest.social" target="_blank" rel="noreferrer">kestrelsnest.social</a>.</p>
</div>
{/snippet}
<TabbedDialog
@@ -346,8 +346,8 @@
title="About __NAME__"
label="About this game"
tabs={[
{ id: 'game', label: 'The game', body: aboutGame },
{ id: 'tally', label: 'The tally', body: aboutTally },
{ id: 'game', label: 'Origins', body: aboutGame },
{ id: 'tally', label: 'Games so far', body: aboutTally },
{ id: 'family', label: 'More games', body: aboutFamily },
{ id: 'word', label: 'Send word', body: aboutWord }
]}
+4 -4
View File
@@ -59,7 +59,7 @@
{#if room.seatEmpty}
<p>{hostName} is gathering players. Sit down above, or watch from the gallery until the game begins.</p>
{:else}
<p>The table is full. You watch from the Peanut Gallery; the game begins when {hostName} says.</p>
<p>The table is full, so you watch from the Peanut Gallery. The game begins when {hostName} says.</p>
{/if}
{:else}
<p class="eyebrow">room {v.roomId}</p>
@@ -84,13 +84,13 @@
<li class="empty muted">{v.size - v.seats.length - v.expected.length} of {v.size} seats empty{#if v.audience > 0}; {v.audience} in the gallery{v.galleryTalk ? ', allowed to talk' : ''}{/if}</li>
</ul>
{#if v.rematchOf}
<p class="muted">A rematch of table {v.rematchOf}: the same company, seats held for those not yet back.</p>
<p class="muted">This is a rematch of table {v.rematchOf}, with seats held for those not yet back.</p>
{/if}
{#if choices && !room.spectating}
<section class="ranking" aria-label="Your ranking">
<h3>Your ranking</h3>
<p class="muted small">
Best first. When the game begins, the deal reads everyone's ranking; nobody sees another player's.
Best first. When the game begins, the deal reads everyone's ranking, and nobody sees another player's.
{#if !v.yourRanking}Until you move one, you have no preference and take what is left.{:else}Your ranking is kept.{/if}
</p>
<ol>
@@ -132,7 +132,7 @@
<p class="muted small note">{v.keeper} keeps this site and answers every call, sometimes within the minute, sometimes after a night's sleep.{#if keeperHour}&nbsp;It is {keeperHour} where {v.keeper} lives.{/if} The seat is held either way.</p>
</div>
{:else if v.challenge}
<p class="muted small note">{v.keeper} has been called to the table by {room.nameOf(v.challenge.by)}.{#if keeperHour}&nbsp;It is {keeperHour} there.{/if} A seat is held: if they can come now they will take it; if they are asleep or away they will leave a word below when they can. Meanwhile the table is yours: seat a bot, invite a friend, or open another game in a new tab and play on. This table keeps its place in your hall until you come back.</p>
<p class="muted small note">{v.keeper} has been called to the table by {room.nameOf(v.challenge.by)}.{#if keeperHour}&nbsp;It is {keeperHour} there.{/if} A seat is held for them. If they can come now, they will take it. If they are asleep or away, they will leave a word below when they can. In the meantime you can seat a bot, invite a friend, or play another game in a new tab. This table keeps its place in your hall until you come back.</p>
{/if}
{/if}
{#if room.error}<p class="warning">{room.error}</p>{/if}
@@ -61,7 +61,7 @@
<form class="slip" aria-labelledby="report-heading" onsubmit={send}>
<h2 id="report-heading">Something amiss?</h2>
{#if sent}
<p class="thanks">Recorded beside the room's ledger, with the round. Thank you; the keeper will study the moment.{#if error} {error}{/if}</p>
<p class="thanks">Recorded beside the room's ledger, with the turn. Thank you. The keeper will look into it.{#if error} {error}{/if}</p>
{:else}
<label class="field">
<span>What happened?</span>
@@ -76,7 +76,7 @@
<input type="file" accept="image/png,image/jpeg,image/webp" onchange={pickImage} />
{#if preview}<img class="preview" src={preview} alt="The screenshot you chose" />{/if}
</label>
<p class="muted small note">The room code and the round ride along; the keeper can replay the game to this moment.</p>
<p class="muted small note">The room code and the turn are sent with it, so the keeper can replay the game to this moment.</p>
{#if error}<p class="warning">{error}</p>{/if}
<div class="actions">
<button type="submit" class="commit small" disabled={!happened.trim() || busy}>{busy ? 'Sending' : 'Send it'}</button>
+1 -1
View File
@@ -60,7 +60,7 @@
<button type="submit" class="quiet" disabled={!draft.trim() || !galleryName.trim() || sending}>Say</button>
</form>
{:else if room.spectating}
<p class="muted small note">The gallery listens; the host has not let it talk.</p>
<p class="muted small note">The host has not let the gallery talk at this table.</p>
{:else}
<form onsubmit={say}>
<input type="text" bind:value={draft} maxlength="300" placeholder="Say something to the table" aria-label="Say something to the table" autocomplete="off" />
+6 -6
View File
@@ -18,7 +18,7 @@
<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('/rules')}>The rules</a>
<h1>How to play</h1>
<p class="lede">Two sentences that set the scene and say what this page walks through.</p>
<p class="lede">Two sentences that set the scene: who the player is, and what they are trying to do.</p>
<nav aria-label="Sections">
<a href="#hall">The hall</a>
<a href="#table">The table</a>
@@ -30,12 +30,12 @@
<section id="hall">
<h2>The hall</h2>
<p>Write your name and choose your company. <strong>Play now against the bot</strong> seats you across from the house's construct. <strong>Open a table</strong> lays a table and hands you a link and a four-letter code to send. <strong>Join one</strong> takes a code someone sent you. Beneath, <em>your games</em> lists every table this browser holds a seat at, and whose move it is.</p>
<p>Write your name and choose your company. <strong>Play now against the bot</strong> seats you across from the bot and begins. <strong>Open a table</strong> lays a table and hands you a link and a four-letter code to send. <strong>Join one</strong> takes a code someone sent you. Beneath, <em>your games</em> lists every table this browser holds a seat at, and whose move it is.</p>
</section>
<section id="table">
<h2>The table</h2>
<p>A table fills as players open the link. Whoever laid it is the host: they may seat a bot in any empty chair or send one away, and they begin the game when the company suits them, however full the table. Once begun, the seats are closed.</p>
<p>A table fills as players open the link. Whoever laid it is the host. The host may seat a bot in any empty chair or send one away, and they begin the game when the company suits them, however full the table. Once begun, the seats are closed.</p>
</section>
<section id="board">
@@ -45,14 +45,14 @@
<section id="gallery">
<h2>The Peanut Gallery and table talk</h2>
<p>A room's link is an invitation to sit while the table is laid and to watch once the game has begun. Anyone who opens it without a seat takes a place in the Peanut Gallery, shown only what every player at the table could see. <em>Table talk</em> is for the players seated, unless the host lets the gallery talk; then a watcher signs a name and their lines are marked as the gallery's. A line goes to everyone at the table and everyone in the gallery, and is kept with the game.</p>
<p>A seat can follow you to another device: <em>Transfer seat</em> in the masthead gives four words, good for ten minutes, and the hall on the other device claims them. When a game ends, anyone at the table may call for a rematch: a new table with the same company, seats held until everyone is back. And while a table is laid, you may challenge the keeper of this site to a seat; the note beside the button says what hour it is where they live.</p>
<p>A room's link is an invitation to sit while the table is laid and to watch once the game has begun. Anyone who opens it without a seat takes a place in the Peanut Gallery, shown only what every player at the table could see. <em>Table talk</em> is for the players seated, unless the host lets the gallery talk. Then a watcher gives a name, and their lines are marked as the gallery's. A line goes to everyone at the table and everyone in the gallery, and is kept with the game.</p>
<p>A seat can follow you to another device. <em>Transfer seat</em> in the masthead gives four words, good for ten minutes, and the hall on the other device claims them. When a game ends, anyone at the table may call for a rematch, which opens a new table for the same company and holds their seats until they are back. While a table is laid, you may challenge the keeper of this site to a seat; the note beside the button gives the hour where they live.</p>
</section>
<section id="first">
<h2>A first game</h2>
<p>The advice you would give a friend across the table for their first few turns.</p>
<p>The full rules are on the <a href="/rules">rules page</a>. If a rule here reads wrong to you, or the page misbehaves, <a href="mailto:eric@ericwagoner.com">send word</a>; the hall's <em>about this game</em> has the other ways to reach the keeper.</p>
<p>The full rules are on the <a href="/rules">rules page</a>. If a rule here reads wrong to you, or something misbehaves, <a href="mailto:eric@ericwagoner.com">send word</a>. The hall's <em>about this game</em> lists the other ways to reach the keeper.</p>
</section>
</main>
+2 -2
View File
@@ -150,7 +150,7 @@
<a href={`/join/${room.view.rematch.to}`}>Take your seat</a>
{:else}
<button type="button" class="quiet" disabled={rematching} onclick={rematch}>Rematch with this table</button>
<span>the same company, at a new table; seats are held for everyone here.</span>
<span>The same company sits down at a new table, with a seat held for everyone here.</span>
{/if}
</p>
{/if}
@@ -159,7 +159,7 @@
<section class="seat">
<p class="eyebrow">room {roomId}</p>
<h2>Take a seat</h2>
<p>A player has opened this table and is gathering company. Choose a name and sit; the game begins when the host says.</p>
<p>A player has opened this table and is gathering company. Choose a name and sit down. The game begins when the host says.</p>
<form class="sit" onsubmit={takeSeat}>
<label class="field">
<span>Your name</span>
+1 -1
View File
@@ -35,7 +35,7 @@
<p>The full source text, in collapsible sections, so a rules question can be settled by the words the engine follows.</p>
</section>
<p class="word">If this page reads a rule differently from the original text, that is a bug: <a href="mailto:eric@ericwagoner.com">send word</a>.</p>
<p class="word">If this page reads a rule differently from the original text, that is a bug. Please <a href="mailto:eric@ericwagoner.com">send word</a>.</p>
</main>
<style>