From 647e8cb2bf6a0969ece874a58b8af37654055117 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Thu, 29 Oct 2026 10:37:40 -0400 Subject: [PATCH] =?UTF-8?q?The=20hall's=20game=20cards=20no=20longer=20lif?= =?UTF-8?q?t=20on=20hover=20or=20end=20in=20"Play=20X=20=E2=86=92"=20and?= =?UTF-8?q?=20a=20domain=20line,=20and=20the=20first=20game,=20the=20one?= =?UTF-8?q?=20newcomers=20are=20pointed=20at,=20takes=20the=20whole=20widt?= =?UTF-8?q?h=20with=20its=20art=20beside=20the=20words,=20so=20five=20game?= =?UTF-8?q?s=20fill=20the=20grid=20without=20an=20orphan;=20the=20template?= =?UTF-8?q?=20renders=20the=20hall,=20the=20rules=20and=20the=20guide=20at?= =?UTF-8?q?=20build=20time=20behind=20a=20fallback=20named=20shell.html,?= =?UTF-8?q?=20and=20its=20deploy=20renders=20the=20Caddy=20site=20file=20f?= =?UTF-8?q?rom=20the=20template=20each=20time;=20the=20family=20list=20car?= =?UTF-8?q?ries=20Wyrm=20and=20Dip's=20larger=20maps;=20the=20tally=20head?= =?UTF-8?q?ing=20and=20the=20hosting=20line=20are=20said=20plainly;=20and?= =?UTF-8?q?=20the=20writing=20checklist=20names=20one=20sentence=20shape?= =?UTF-8?q?=20for=20a=20whole=20list=20as=20a=20tell?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_0141G6xqLeNRYEtviLWSB5Up --- CONVENTIONS.md | 9 ++++ drift.sh | 1 + family.json | 2 +- gamehall/site/index.html | 60 ++++++++----------------- template/deploy/Caddyfile.tmpl | 2 +- template/deploy/deploy.sh | 8 +++- template/src/lib/components/Hall.svelte | 4 +- template/src/routes/+layout.ts | 5 ++- template/src/routes/+page.ts | 3 ++ template/src/routes/guide/+page.ts | 3 ++ template/src/routes/rules/+page.ts | 3 ++ template/vite.config.ts | 4 +- 12 files changed, 55 insertions(+), 49 deletions(-) create mode 100644 template/src/routes/+page.ts create mode 100644 template/src/routes/guide/+page.ts create mode 100644 template/src/routes/rules/+page.ts diff --git a/CONVENTIONS.md b/CONVENTIONS.md index 66d8559..1ea8e39 100644 --- a/CONVENTIONS.md +++ b/CONVENTIONS.md @@ -294,6 +294,10 @@ and read each new page against this list before it ships. deliberate triplet as its tagline when the energy is the point, as Wiz-War's "Steal treasure. Blast through walls. Outwit your friends." does. Once is a choice, and a pattern is a tell. +- **No one sentence shape for a whole list.** Thirty chronicle lines that + each run image, colon, consequence read as a template however good the + images are. Let the fact come first in some, in its own sentence in others, + and joined by "and" or "which" in the rest. - **No closing aphorisms.** A paragraph ends on its last fact, not on a line meant to sound wise ("This is the whole art", "Nothing is unlocked by it and nothing is withheld without it"). @@ -351,6 +355,11 @@ opened its mouth.") as long as the fact survives. - `npm install --omit=optional` on the droplet breaks Vite; the server carries its own small `package.json` and installs only that. - A dynamic `/join/[code]` route must set `prerender = false`. +- The hall, the rules and the guide set `ssr = true` in their own `+page.ts`, + so view-source, a crawler and a reader without scripts get the text; the + rest stays an empty shell. The adapter's fallback is `shell.html`, not + `index.html`, because a fallback named `index.html` overwrites the + prerendered hall, and Caddy's `try_files` ends in `/shell.html`. - Two tabs on one origin share a seat; test a second player from `127.0.0.1` against `localhost` (Vite needs `--host`). - Svelte proxies do not `structuredClone`: hand the engine `$state.snapshot`. diff --git a/drift.sh b/drift.sh index 9b15985..03ed601 100755 --- a/drift.sh +++ b/drift.sh @@ -11,6 +11,7 @@ SLUG="${2:-$(basename "$(cd "$GAME" && pwd)")}" KIT="$(cd "$(dirname "$0")" && pwd)" MAP="$KIT/drift-map/$SLUG.txt" SHARED="server/src/index.ts server/src/rooms.ts server/src/store.ts server/src/reports.ts server/src/ratelimit.ts server/src/tally.ts +src/routes/+layout.ts src/routes/+page.ts src/routes/rules/+page.ts src/routes/guide/+page.ts vite.config.ts src/lib/game/spec.ts src/lib/net/client.ts src/lib/net/talk.ts src/lib/net/view.ts src/lib/net/room.svelte.ts src/lib/components/TableTalk.svelte src/lib/components/ReportSlip.svelte src/lib/components/TabbedDialog.svelte src/lib/components/Tabs.svelte src/lib/components/Announcement.svelte deploy/deploy.sh deploy/setup-droplet.sh deploy/setup-server.sh deploy/Caddyfile.tmpl deploy/verify-ledgers.sh diff --git a/family.json b/family.json index 15fd16c..81980f2 100644 --- a/family.json +++ b/family.json @@ -31,7 +31,7 @@ "name": "Dip", "url": "https://dip.kestrelsnest.social", "pitch": "Seven powers share one map, and none of them has to keep its word.", - "players": "2 to 7", + "players": "2 to 10, depending on the map", "origin": "Allan B. Calhamer's Diplomacy, 1959; an unofficial rebuild" }, { diff --git a/gamehall/site/index.html b/gamehall/site/index.html index 9f9e9a8..0f2da6d 100644 --- a/gamehall/site/index.html +++ b/gamehall/site/index.html @@ -4,7 +4,7 @@ Kestrel's Hall - + @@ -58,18 +58,22 @@ header.mast p.newcomer { margin: 10px auto 0; max-width: 38em; font-size: 0.95rem; color: var(--bone-dim); } .games { display: grid; gap: 22px; grid-template-columns: repeat(2, 1fr); margin-top: 22px; } - @media (max-width: 860px) { .games { grid-template-columns: 1fr; max-width: 520px; margin-left: auto; margin-right: auto; } } .game { display: flex; flex-direction: column; background: var(--panel); border: 1px solid var(--line); - border-radius: 12px; + border-radius: 6px; overflow: hidden; color: inherit; text-decoration: none; - transition: transform .15s ease, border-color .15s ease; } - .game:hover { transform: translateY(-3px); border-color: var(--gold); text-decoration: none; } + .game:hover { border-color: var(--gold); text-decoration: none; } + .game:hover h2 { color: var(--gold); } + /* The first game is the one newcomers are pointed at, so it has the whole width: art beside the words. */ + .game.first { grid-column: 1 / -1; flex-direction: row; } + .game.first .art { flex: 0 0 55%; aspect-ratio: 2 / 1; } + .game.first .body { justify-content: center; padding: 22px 26px; } + .game.first h2 { font-size: 1.9rem; } .game .art { position: relative; aspect-ratio: 5 / 3; background: #222; overflow: hidden; } .game .art img.fill { width: 100%; height: 100%; object-fit: cover; display: block; } .game .art img.tafl { object-position: 50% 50%; } @@ -77,13 +81,15 @@ .game .body { padding: 16px 18px 18px; display: flex; flex-direction: column; gap: 6px; flex: 1; } .game h2 { margin: 0; font-size: 1.45rem; font-weight: 500; } .game .pitch { margin: 0; color: var(--bone); } - .game .players, .game .length { margin: 0; color: var(--bone-dim); font-size: 0.92rem; } + .game .players { margin: 0; color: var(--bone-dim); font-size: 0.92rem; } .game .origin { margin: 0; color: var(--bone-dim); font-size: 0.85rem; } - .game .length:empty { display: none; } - .game .go { margin-top: auto; padding-top: 10px; display: flex; flex-direction: column; gap: 2px; } - .game .play { color: var(--gold); font-size: 1rem; } - .game .site { color: var(--bone-dim); font-size: 0.85rem; } - .game:hover .play { text-decoration: underline; } + @media (max-width: 860px) { + .games { grid-template-columns: 1fr; max-width: 520px; margin-left: auto; margin-right: auto; } + .game.first { flex-direction: column; } + .game.first .art { flex: none; aspect-ratio: 5 / 3; } + .game.first .body { padding: 16px 18px 18px; } + .game.first h2 { font-size: 1.45rem; } + } .nowrap { white-space: nowrap; } /* The maze card: a stone floor in CSS, mortar-coloured walls, and the hall's own token drawings. */ @@ -128,14 +134,11 @@

Kestrel's Hall

Free board games in the browser. Play alone against the house right away, or start a table and send friends the link. No accounts, no adverts, no timers, and nothing sold.

-

New here? Wiz-War is the friendliest place to start, and a game can be over in minutes. Dip is the long game.

- - - +
@@ -159,11 +162,6 @@

Steal treasure. Blast through walls. Outwit your friends.

2 to 6 players, or solo against the automaton

An unofficial rebuild of Tom Jolly's 1993 board game

-

-
- Play Wiz-War → - wizwar.kestrelsnest.social -
@@ -174,11 +172,6 @@

Wizards duel with their hands, and the right run of gestures is a spell.

2 to 12 players, or solo against a rival wizard

Richard Bartle's pencil-and-paper duel, 1977

-

-
- Play Waving Hands → - hands.kestrelsnest.social -
@@ -189,11 +182,6 @@

The Viking board game of a king breaking for safety and a ring of attackers closing in.

2 players, or solo against the housecarl

The Norse tafl game, by the Copenhagen rules or Linnaeus's 1732 Tablut

-

-
- Play Hnefatafl → - tafl.kestrelsnest.social -
@@ -204,11 +192,6 @@

Seven powers share one map, and none of them has to keep its word. Other maps seat five or ten.

2 to 10 players, depending on the map, or solo against the house's envoys

An unofficial rebuild of Allan B. Calhamer's 1959 Diplomacy

-

-
- Play Dip → - dip.kestrelsnest.social -
@@ -219,11 +202,6 @@

Chess on three boards, with a Dragon in the air above your King and a Basilisk in the earth below.

2 players, or solo against the drake

An unofficial rebuild of Gary Gygax's 1985 Dragonchess, from Dragon Magazine No. 100

-

-
- Play Wyrm → - wyrm.kestrelsnest.social -
@@ -250,7 +228,7 @@

The hosting

-

The games 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 ko-fi.com/kestrelsnest. Chipping in unlocks nothing, and nothing is held back without it.

+

The games 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 ko-fi.com/kestrelsnest. Chipping in unlocks nothing, and the games are the same for everyone.

diff --git a/template/deploy/Caddyfile.tmpl b/template/deploy/Caddyfile.tmpl index 9b270fb..356ad54 100644 --- a/template/deploy/Caddyfile.tmpl +++ b/template/deploy/Caddyfile.tmpl @@ -43,7 +43,7 @@ __HOST__ { header @immutable Cache-Control "public, max-age=31536000, immutable" @mutable not path /_app/immutable/* header @mutable Cache-Control "no-cache" - try_files {path} {path}.html /index.html + try_files {path} {path}.html /shell.html file_server } } diff --git a/template/deploy/deploy.sh b/template/deploy/deploy.sh index b2c9bc7..594267a 100755 --- a/template/deploy/deploy.sh +++ b/template/deploy/deploy.sh @@ -25,7 +25,7 @@ rsync -az --delete --filter='P _app/immutable/*' \ # it needs only its code and the engine. Ledgers live outside this tree. rsync -az --delete --exclude='/server/node_modules' \ --include='/server/***' --include='/src/' --include='/src/lib/' --include='/src/lib/game/***' --include='/src/lib/net/' --include='/src/lib/net/view.ts' \ - --include='/deploy/' --include='/deploy/__SLUG__.service' --include='/deploy/__SLUG__.cron' --include='/deploy/*.sh' --include='/deploy/traffic.py' \ + --include='/deploy/' --include='/deploy/__SLUG__.service' --include='/deploy/__SLUG__.cron' --include='/deploy/*.sh' --include='/deploy/Caddyfile.tmpl' --include='/deploy/traffic.py' \ --exclude='*' \ ./ "root@$HOST:/opt/__SLUG__/app/" @@ -60,6 +60,12 @@ ssh "root@$HOST" ' systemctl daemon-reload systemctl enable --now __SLUG__ systemctl restart __SLUG__ + # The site file is rendered from the template on every deploy, with the address line the box was set up with. + ADDR=$(head -1 /etc/caddy/sites/__SLUG__.caddy | sed "s/ {\$//") + sed "s|__HOST__|$ADDR|" /opt/__SLUG__/app/deploy/Caddyfile.tmpl > /etc/caddy/sites/__SLUG__.caddy + caddy validate --config /etc/caddy/Caddyfile >/dev/null + # Validation as root touches the access logs; Caddy runs as caddy and must own them. + chown -R caddy:caddy /var/lib/caddy systemctl reload caddy # "active" means only that the process started: wait for it to answer HTTP, # so a server that dies loading its code fails the deploy instead of looping. diff --git a/template/src/lib/components/Hall.svelte b/template/src/lib/components/Hall.svelte index 8bd05c7..415ad5f 100644 --- a/template/src/lib/components/Hall.svelte +++ b/template/src/lib/components/Hall.svelte @@ -324,7 +324,7 @@ {/snippet} {#snippet aboutTally()} {#if told} - + {:else}

Counting…

{/if} @@ -340,7 +340,7 @@ {/if}

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 - ko-fi.com/kestrelsnest. Chipping in unlocks nothing, and nothing is held back without it. + ko-fi.com/kestrelsnest. Chipping in unlocks nothing, and the games are the same for everyone.

__NAME__ is its designer's.

diff --git a/template/src/routes/+layout.ts b/template/src/routes/+layout.ts index a3ded22..64db8f3 100644 --- a/template/src/routes/+layout.ts +++ b/template/src/routes/+layout.ts @@ -1,4 +1,5 @@ -// Everything happens in the browser against the game server, so nothing is rendered on the -// server. Each route is prerendered as an empty shell that the client fills in. +// Everything happens in the browser against the game server, so by default a route is +// prerendered as an empty shell that the client fills in. The hall, the rules and the guide +// turn rendering on in their own +page.ts, since their text is the same for everyone. export const ssr = false; export const prerender = true; diff --git a/template/src/routes/+page.ts b/template/src/routes/+page.ts new file mode 100644 index 0000000..3634166 --- /dev/null +++ b/template/src/routes/+page.ts @@ -0,0 +1,3 @@ +// The page is text that reads the same for everyone, so it is rendered at build time and served whole; +// the browser-only state it shows (held seats, the saved name) fills in once the script runs. +export const ssr = true; diff --git a/template/src/routes/guide/+page.ts b/template/src/routes/guide/+page.ts new file mode 100644 index 0000000..3634166 --- /dev/null +++ b/template/src/routes/guide/+page.ts @@ -0,0 +1,3 @@ +// The page is text that reads the same for everyone, so it is rendered at build time and served whole; +// the browser-only state it shows (held seats, the saved name) fills in once the script runs. +export const ssr = true; diff --git a/template/src/routes/rules/+page.ts b/template/src/routes/rules/+page.ts new file mode 100644 index 0000000..3634166 --- /dev/null +++ b/template/src/routes/rules/+page.ts @@ -0,0 +1,3 @@ +// The page is text that reads the same for everyone, so it is rendered at build time and served whole; +// the browser-only state it shows (held seats, the saved name) fills in once the script runs. +export const ssr = true; diff --git a/template/vite.config.ts b/template/vite.config.ts index 9b8c1e4..4a01a0b 100644 --- a/template/vite.config.ts +++ b/template/vite.config.ts @@ -10,7 +10,9 @@ export default defineConfig({ runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true) }, // A static build: Caddy serves files and proxies /api and /ws to the game server. - adapter: adapter({ fallback: 'index.html' }) + // The fallback is the empty shell Caddy serves for a room address; it is not index.html, so the + // hall's own prerendered page is not overwritten by it. + adapter: adapter({ fallback: 'shell.html' }) }) ], server: {