Compare commits

...
2 Commits
12 changed files with 58 additions and 49 deletions

No files matched your search

+9
View File
@@ -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`.
+1
View File
@@ -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
+1 -1
View File
@@ -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"
},
{
+19 -41
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Kestrel's Hall</title>
<meta name="description" content="Free board games in the browser: Wiz-War, Waving Hands, Hnefatafl, and Dip. Play with friends by link or alone against the house. No accounts, no adverts, nothing sold.">
<meta name="description" content="Free board games in the browser: Wiz-War, Waving Hands, Hnefatafl, Dip and Wyrm. Play with friends by link or alone against the house. No accounts, no adverts, nothing sold.">
<link rel="canonical" href="https://gamehall.kestrelsnest.social/">
<meta property="og:type" content="website">
<meta property="og:title" content="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 @@
<header class="mast">
<h1>Kestrel's Hall</h1>
<p class="lede"><b>Free board games in the browser.</b> 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.</p>
<!-- A word for newcomers, one line to tune or remove. -->
<p class="newcomer">New here? Wiz-War is the friendliest place to start, and a game can be over in minutes. Dip is the long game.</p>
</header>
<div class="games">
<!-- Each card's empty "length" line is for a typical game's length, once real play has shown it; it is hidden until filled. -->
<a class="game" href="https://wizwar.kestrelsnest.social">
<a class="game first" href="https://wizwar.kestrelsnest.social">
<div class="art">
<div class="maze">
<div class="w h" style="left:12.5%;top:19%;width:25%"></div>
@@ -159,11 +162,6 @@
<p class="pitch">Steal treasure. Blast through walls. Outwit your friends.</p>
<p class="players">2 to 6 players, or solo against the automaton</p>
<p class="origin">An unofficial rebuild of Tom Jolly's 1993 board game</p>
<p class="length"></p>
<div class="go">
<span class="play">Play Wiz-War →</span>
<span class="site">wizwar.kestrelsnest.social</span>
</div>
</div>
</a>
@@ -174,11 +172,6 @@
<p class="pitch">Wizards duel with their hands, and the right run of gestures is a spell.</p>
<p class="players">2 to 12 players, or solo against a rival wizard</p>
<p class="origin">Richard Bartle's pencil-and-paper duel, 1977</p>
<p class="length"></p>
<div class="go">
<span class="play">Play Waving Hands →</span>
<span class="site">hands.kestrelsnest.social</span>
</div>
</div>
</a>
@@ -189,11 +182,6 @@
<p class="pitch">The Viking board game of a king breaking for safety and a ring of attackers closing in.</p>
<p class="players">2 players, or solo against the housecarl</p>
<p class="origin">The Norse tafl game, by the Copenhagen rules or Linnaeus's 1732 Tablut</p>
<p class="length"></p>
<div class="go">
<span class="play">Play Hnefatafl →</span>
<span class="site">tafl.kestrelsnest.social</span>
</div>
</div>
</a>
@@ -204,11 +192,6 @@
<p class="pitch">Seven powers share one map, and none of them has to keep its word. Other maps seat five or ten.</p>
<p class="players">2 to 10 players, depending on the map, or solo against the house's envoys</p>
<p class="origin">An unofficial rebuild of Allan B. Calhamer's 1959 Diplomacy</p>
<p class="length"></p>
<div class="go">
<span class="play">Play Dip →</span>
<span class="site">dip.kestrelsnest.social</span>
</div>
</div>
</a>
@@ -219,11 +202,6 @@
<p class="pitch">Chess on three boards, with a Dragon in the air above your King and a Basilisk in the earth below.</p>
<p class="players">2 players, or solo against the drake</p>
<p class="origin">An unofficial rebuild of Gary Gygax's 1985 Dragonchess, from Dragon Magazine No. 100</p>
<p class="length"></p>
<div class="go">
<span class="play">Play Wyrm →</span>
<span class="site">wyrm.kestrelsnest.social</span>
</div>
</div>
</a>
@@ -250,7 +228,7 @@
</div>
</div>
<h2 style="margin-top:28px">The hosting</h2>
<p>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 <a class="nowrap" href="https://ko-fi.com/kestrelsnest">ko-fi.com/kestrelsnest</a>. Chipping in unlocks nothing, and nothing is held back without it.</p>
<p>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 <a class="nowrap" href="https://ko-fi.com/kestrelsnest">ko-fi.com/kestrelsnest</a>. Chipping in unlocks nothing, and the games are the same for everyone.</p>
</div>
</section>
+4 -1
View File
@@ -43,7 +43,10 @@ __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 passes the root over as a directory and would fall to the shell; the hall's
# own page is index.html.
rewrite / /index.html
try_files {path} {path}.html /shell.html
file_server
}
}
+7 -1
View File
@@ -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.
+2 -2
View File
@@ -324,7 +324,7 @@
{/snippet}
{#snippet aboutTally()}
{#if told}
<TallyBoard title="How much love the table is getting" headline={told.headline} details={told.details} note={told.note} />
<TallyBoard title="What the ledgers add up to" headline={told.headline} details={told.details} note={told.note} />
{:else}
<p class="muted">Counting…</p>
{/if}
@@ -340,7 +340,7 @@
{/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, 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>. Chipping in unlocks nothing, and nothing is held back without it.
<a href="https://ko-fi.com/kestrelsnest" target="_blank" rel="noreferrer">ko-fi.com/kestrelsnest</a>. Chipping in unlocks nothing, and the games are the same for everyone.
</p>
<p class="made">__NAME__ is its designer's.</p>
</div>
+3 -2
View File
@@ -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;
+3
View File
@@ -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;
+3
View File
@@ -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;
+3
View File
@@ -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;
+3 -1
View File
@@ -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: {