Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc5f93c2fa | ||
|
|
cd9d14ec35 |
No files matched your search
@@ -158,6 +158,29 @@ emoji in the interface; the words do the work. Phone width first: a sticky
|
|||||||
bar for the move if the board runs long, lists that shorten, gutters of
|
bar for the move if the board runs long, lists that shorten, gutters of
|
||||||
16px, no horizontal scroll.
|
16px, no horizontal scroll.
|
||||||
|
|
||||||
|
## The voice
|
||||||
|
|
||||||
|
Every game speaks in its own world. Wherever the page tells a player what
|
||||||
|
happened, it gives an image as well as the fact: not "Corwin takes 5 from
|
||||||
|
a fireball" but "A fireball bursts over Corwin, for 5". The fact stays
|
||||||
|
exact and whole (the number, the duration, who is affected, the turn a
|
||||||
|
thing will happen), and the image carries it.
|
||||||
|
|
||||||
|
- **The chronicle** is where the voice matters most. Each move, blow,
|
||||||
|
enchantment, arrival and death gets its own picture. Something that
|
||||||
|
unfolds over turns (an illness, a siege, a fuse) is told a turn at a
|
||||||
|
time, names the turn it ends on when it begins, and warns on the last
|
||||||
|
turn anything can be done about it.
|
||||||
|
- **The rest of the surround** speaks the same way: the lobby, the guide,
|
||||||
|
the status lines, the verdict. Plain words for controls and buttons;
|
||||||
|
the images belong to what happens, not to what you press.
|
||||||
|
- **Our own words.** An older implementation's prose is a guide to the
|
||||||
|
tone, never a source to copy.
|
||||||
|
- **Changing text is not changing rules.** Chronicle wording is not part
|
||||||
|
of the ledger, so it ships without a rules revision; the deploy gate
|
||||||
|
compares outcomes, not sentences. A test that pins a phrase should pin
|
||||||
|
the fact inside it.
|
||||||
|
|
||||||
## Things learned the hard way
|
## Things learned the hard way
|
||||||
|
|
||||||
- Caddy's `try_files` rewrites `/api` before `reverse_proxy` unless the
|
- Caddy's `try_files` rewrites `/api` before `reverse_proxy` unless the
|
||||||
|
|||||||
+8
-6
@@ -1,9 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Stamp out a new game from the template.
|
# Stamp out a new game from the template.
|
||||||
# ./new-game.sh <slug> "<Name>" [port] [domain]
|
# ./new-game.sh <slug> "<Name>" [port] [domain]
|
||||||
# e.g. ./new-game.sh tumbleweed "Tumbleweed" 8789 tumbleweed.kestrelsnest.social
|
# e.g. ./new-game.sh tumbleweed "Tumbleweed" 8791 tumbleweed.kestrelsnest.social
|
||||||
# Ports so far: wizwar 8787, waving-hands 8788. Each game is its own droplet,
|
# Ports so far: wizwar 8787, waving-hands 8788, hnefatafl 8789, dip 8790. The
|
||||||
# so the port only has to be unique on your machine for running two locally.
|
# games share one host (deploy/README.md), so a new game takes the next free port.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
SLUG="${1:?usage: new-game.sh <slug> \"<Name>\" [port] [domain]}"
|
SLUG="${1:?usage: new-game.sh <slug> \"<Name>\" [port] [domain]}"
|
||||||
NAME="${2:?usage: new-game.sh <slug> \"<Name>\" [port] [domain]}"
|
NAME="${2:?usage: new-game.sh <slug> \"<Name>\" [port] [domain]}"
|
||||||
@@ -20,7 +20,7 @@ find "$DEST" -depth -name '*__SLUG__*' | while read -r f; do
|
|||||||
mv "$f" "$(dirname "$f")/$(basename "$f" | sed "s/__SLUG__/$SLUG/g")"
|
mv "$f" "$(dirname "$f")/$(basename "$f" | sed "s/__SLUG__/$SLUG/g")"
|
||||||
done
|
done
|
||||||
# Then the placeholders inside. __IP__ and the Sentry ids stay until they exist.
|
# Then the placeholders inside. __IP__ and the Sentry ids stay until they exist.
|
||||||
find "$DEST" -type f \( -name '*.ts' -o -name '*.svelte' -o -name '*.json' -o -name '*.md' -o -name '*.sh' -o -name '*.tmpl' -o -name '*.service' -o -name '*.cron' -o -name '*.html' -o -name '*.css' -o -name '.gitignore' \) -print0 \
|
find "$DEST" -type f \( -name '*.ts' -o -name '*.svelte' -o -name '*.json' -o -name '*.md' -o -name '*.sh' -o -name '*.py' -o -name '*.tmpl' -o -name '*.service' -o -name '*.cron' -o -name '*.html' -o -name '*.css' -o -name '.gitignore' \) -print0 \
|
||||||
| xargs -0 sed -i '' -e "s/__SLUG__/$SLUG/g" -e "s/__NAME__/$NAME/g" -e "s/__PORT__/$PORT/g" -e "s/__DOMAIN__/$DOMAIN/g"
|
| xargs -0 sed -i '' -e "s/__SLUG__/$SLUG/g" -e "s/__NAME__/$NAME/g" -e "s/__PORT__/$PORT/g" -e "s/__DOMAIN__/$DOMAIN/g"
|
||||||
cp "$KIT/CONVENTIONS.md" "$DEST/docs-conventions.md" 2>/dev/null || true
|
cp "$KIT/CONVENTIONS.md" "$DEST/docs-conventions.md" 2>/dev/null || true
|
||||||
mkdir -p "$DEST/docs" && mv "$DEST/docs-conventions.md" "$DEST/docs/conventions.md"
|
mkdir -p "$DEST/docs" && mv "$DEST/docs-conventions.md" "$DEST/docs/conventions.md"
|
||||||
@@ -37,11 +37,13 @@ Next:
|
|||||||
and src/lib/components/Board.svelte with its board. Keep the tests green.
|
and src/lib/components/Board.svelte with its board. Keep the tests green.
|
||||||
2. Fill in the hall's pitch, tag and about panel (src/lib/components/Hall.svelte),
|
2. Fill in the hall's pitch, tag and about panel (src/lib/components/Hall.svelte),
|
||||||
the rules page, the guide, src/app.html's description, and the palette in src/app.css.
|
the rules page, the guide, src/app.html's description, and the palette in src/app.css.
|
||||||
|
Write every line a player reads in the game's own voice: an image with each
|
||||||
|
fact (docs/conventions.md, "The voice").
|
||||||
Add static/og.png (1200 by 630) and static/apple-touch-icon.png (180 by 180):
|
Add static/og.png (1200 by 630) and static/apple-touch-icon.png (180 by 180):
|
||||||
app.html already points at them, and the static fallback serves the hall's
|
app.html already points at them, and the static fallback serves the hall's
|
||||||
HTML for a missing file, so a share card without the image breaks quietly.
|
HTML for a missing file, so a share card without the image breaks quietly.
|
||||||
3. Create the droplet (deploy/README.md), then replace __IP__ everywhere:
|
3. Add the game to the shared host (deploy/README.md), after replacing __IP__ with its address:
|
||||||
grep -rl __IP__ . --exclude-dir=node_modules | xargs sed -i '' 's/__IP__/<ip>/g'
|
grep -rl __IP__ . --exclude-dir=node_modules | xargs sed -i '' 's/__IP__/159.203.115.252/g'
|
||||||
4. Create a Sentry project (the Sentry MCP can) and put its DSN and project id
|
4. Create a Sentry project (the Sentry MCP can) and put its DSN and project id
|
||||||
in deploy/$SLUG.service and deploy/sentry-slack-alert.sh.
|
in deploy/$SLUG.service and deploy/sentry-slack-alert.sh.
|
||||||
5. deploy/deploy.sh <ip>
|
5. deploy/deploy.sh <ip>
|
||||||
|
|||||||
@@ -1,42 +1,42 @@
|
|||||||
__HOST__
|
__HOST__ {
|
||||||
|
root * /opt/__SLUG__/build
|
||||||
|
encode gzip zstd
|
||||||
|
|
||||||
root * /opt/__SLUG__/build
|
header {
|
||||||
encode gzip zstd
|
Strict-Transport-Security "max-age=31536000"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
X-Frame-Options "DENY"
|
||||||
|
Referrer-Policy "no-referrer"
|
||||||
|
}
|
||||||
|
|
||||||
header {
|
log {
|
||||||
Strict-Transport-Security "max-age=31536000"
|
output file /var/lib/caddy/__SLUG__-access.log {
|
||||||
X-Content-Type-Options "nosniff"
|
roll_size 10MiB
|
||||||
X-Frame-Options "DENY"
|
roll_keep 30
|
||||||
Referrer-Policy "no-referrer"
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log {
|
# The game server answers the API and the websocket; a deploy restarts it for
|
||||||
output file /var/lib/caddy/access.log {
|
# a few seconds, and the proxy holds requests that land in that gap.
|
||||||
roll_size 10MiB
|
@game path /api/* /ws
|
||||||
roll_keep 30
|
handle @game {
|
||||||
|
reverse_proxy localhost:__PORT__ {
|
||||||
|
lb_try_duration 30s
|
||||||
|
lb_try_interval 250ms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Everything else is the static site. SvelteKit writes each route as
|
||||||
|
# <route>.html, so /rules is tried as /rules.html before falling back to the
|
||||||
|
# app shell, which serves the rooms. Hashed assets under _app/immutable are
|
||||||
|
# cached for a year; every other response is revalidated so a deploy shows
|
||||||
|
# up on the next load.
|
||||||
|
handle {
|
||||||
|
@immutable path /_app/immutable/*
|
||||||
|
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
|
||||||
|
file_server
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# The game server answers the API and the websocket; a deploy restarts it for
|
|
||||||
# a few seconds, and the proxy holds requests that land in that gap.
|
|
||||||
@game path /api/* /ws
|
|
||||||
handle @game {
|
|
||||||
reverse_proxy localhost:__PORT__ {
|
|
||||||
lb_try_duration 30s
|
|
||||||
lb_try_interval 250ms
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Everything else is the static site. SvelteKit writes each route as
|
|
||||||
# <route>.html, so /rules is tried as /rules.html before falling back to the
|
|
||||||
# app shell, which serves the rooms. Hashed assets under _app/immutable are
|
|
||||||
# cached for a year; every other response is revalidated so a deploy shows
|
|
||||||
# up on the next load.
|
|
||||||
handle {
|
|
||||||
@immutable path /_app/immutable/*
|
|
||||||
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
|
|
||||||
file_server
|
|
||||||
}
|
|
||||||
+28
-24
@@ -1,22 +1,28 @@
|
|||||||
# Deploying __NAME__
|
# Deploying __NAME__
|
||||||
|
|
||||||
Every game, against a housecarl or a friend, is a room on a small Node
|
Every game is a room on a small Node process that keeps it as an
|
||||||
process that keeps it as an append-only ledger of moves in
|
append-only ledger of moves in /var/lib/__SLUG__/rooms. Production is the
|
||||||
/var/lib/__SLUG__/rooms. Production is one DigitalOcean droplet: Caddy
|
shared Kestrel's Hall host: Caddy terminates TLS with automatic
|
||||||
terminates TLS with automatic certificates, serves the static build from
|
certificates, serves the static build from /opt/__SLUG__/build, and proxies
|
||||||
/opt/__SLUG__/build, and proxies /api and /ws to the game server on port
|
/api and /ws to the game server on port __PORT__, which runs as the
|
||||||
__PORT__, which runs as the `__SLUG__` user under systemd from
|
`__SLUG__` user under systemd from /opt/__SLUG__/app.
|
||||||
/opt/__SLUG__/app.
|
|
||||||
|
|
||||||
## Sizing
|
## The shared host
|
||||||
|
|
||||||
The smallest droplet, `s-1vcpu-512mb-10gb` ($4 a month), carries both Caddy
|
The games of Kestrel's Hall share one droplet, `hall` (nyc3, s-1vcpu-1gb,
|
||||||
and the game server comfortably: the server is one small Node process capped
|
$6 a month, tag `kestrels-hall`, IP 159.203.115.252). Each game keeps its
|
||||||
at 300 MB by its unit file, and a room is a few kilobytes of ledger.
|
own Caddy site file in /etc/caddy/sites/<slug>.caddy, its own port,
|
||||||
|
service user, /opt/<slug> and /var/lib/<slug> trees, access log
|
||||||
|
(/var/lib/caddy/<slug>-access.log), cron file and ops scripts;
|
||||||
|
/etc/caddy/Caddyfile only says `import sites/*.caddy`. A game server is one
|
||||||
|
small Node process capped at 300 MB by its unit file, and a room is a few
|
||||||
|
kilobytes of ledger, so the host carries several. Each game needs a port
|
||||||
|
of its own on the host: wizwar 8787, waving-hands 8788, hnefatafl 8789,
|
||||||
|
dip 8790.
|
||||||
|
|
||||||
## Current production
|
## Current production
|
||||||
|
|
||||||
- Droplet: `__SLUG__` (nyc3, s-1vcpu-512mb-10gb, tag `__SLUG__`), IP __IP__
|
- Host: __IP__
|
||||||
- URLs: https://__DOMAIN__ (A record at Hover, where kestrelsnest.social's
|
- URLs: https://__DOMAIN__ (A record at Hover, where kestrelsnest.social's
|
||||||
DNS lives) and https://__SLUG__.__IP__.sslip.io (always works, zero DNS).
|
DNS lives) and https://__SLUG__.__IP__.sslip.io (always works, zero DNS).
|
||||||
- Everyday deploy: `deploy/deploy.sh __IP__`
|
- Everyday deploy: `deploy/deploy.sh __IP__`
|
||||||
@@ -24,21 +30,19 @@ at 300 MB by its unit file, and a room is a few kilobytes of ledger.
|
|||||||
and the screenshots to ~/Desktop/__SLUG__-reports with a digest;
|
and the screenshots to ~/Desktop/__SLUG__-reports with a digest;
|
||||||
`deploy/report-reply.sh __IP__ <id> <status> "text"` answers one.
|
`deploy/report-reply.sh __IP__ <id> <status> "text"` answers one.
|
||||||
|
|
||||||
## New droplet from scratch
|
## Adding this game to the host
|
||||||
|
|
||||||
1. `doctl compute droplet create __SLUG__ --region nyc3 \
|
1. `scp deploy/setup-droplet.sh deploy/Caddyfile.tmpl root@<ip>:/root/ && ssh root@<ip> \
|
||||||
--size s-1vcpu-512mb-10gb --image ubuntu-24-04-x64 \
|
|
||||||
--ssh-keys <your-key-ids> --tag-name __SLUG__ --wait`
|
|
||||||
2. `scp deploy/setup-droplet.sh deploy/Caddyfile.tmpl root@<ip>:/root/ && ssh root@<ip> \
|
|
||||||
"bash /root/setup-droplet.sh '__DOMAIN__, __SLUG__.<ip>.sslip.io'"`
|
"bash /root/setup-droplet.sh '__DOMAIN__, __SLUG__.<ip>.sslip.io'"`
|
||||||
(point the A record at the new IP first, or leave the real name out until it is).
|
It installs Caddy and Node if the host lacks them, writes this game's
|
||||||
3. `scp deploy/setup-server.sh deploy/Caddyfile.tmpl root@<ip>:/root/ && ssh root@<ip> "bash /root/setup-server.sh"`
|
site file, and touches nothing of another game's. Point the A record at
|
||||||
4. `deploy/deploy.sh <ip>`
|
the host first, or leave the real name out until it is; running it again
|
||||||
|
with the new line is how a name is added later.
|
||||||
|
2. `deploy/deploy.sh <ip>`
|
||||||
|
|
||||||
The sslip.io hostname works with no DNS at all. To add a real name, point an
|
A fresh host, when one is needed: `doctl compute droplet create hall --region nyc3 \
|
||||||
A record at the droplet and add the name to the first line of
|
--size s-1vcpu-1gb --image ubuntu-24-04-x64 --ssh-keys <your-key-ids> \
|
||||||
/etc/caddy/Caddyfile (space separated), then `systemctl reload caddy`; Caddy
|
--tag-name kestrels-hall --wait`, then step 1.
|
||||||
fetches the certificate on first request.
|
|
||||||
|
|
||||||
## Operations scripts on the droplet
|
## Operations scripts on the droplet
|
||||||
|
|
||||||
|
|||||||
@@ -33,4 +33,4 @@ else
|
|||||||
fi
|
fi
|
||||||
echo "--- box"
|
echo "--- box"
|
||||||
echo "disk: $(df -h / | awk 'NR==2{print $5" used of "$2}'); memory: $(free -m | awk 'NR==2{print $7" MB available of "$2}'); load: $(cut -d' ' -f1-3 /proc/loadavg)"
|
echo "disk: $(df -h / | awk 'NR==2{print $5" used of "$2}'); memory: $(free -m | awk 'NR==2{print $7" MB available of "$2}'); load: $(cut -d' ' -f1-3 /proc/loadavg)"
|
||||||
echo "caddy access log: $(du -sh /var/lib/caddy/access.log 2>/dev/null | cut -f1) live, $(ls /var/lib/caddy/access*.log* 2>/dev/null | wc -l) files"
|
echo "caddy access log: $(du -sh /var/lib/caddy/__SLUG__-access.log 2>/dev/null | cut -f1) live, $(ls /var/lib/caddy/__SLUG__-access*.log* 2>/dev/null | wc -l) files"
|
||||||
@@ -1,36 +1,69 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# One-time droplet setup. Copy this script and Caddyfile.tmpl to the droplet
|
# Prepare a host for __NAME__. The host may be shared with other games of
|
||||||
# and run ON the droplet as root:
|
# Kestrel's Hall: each keeps its own Caddy site file in /etc/caddy/sites, its
|
||||||
# bash setup-droplet.sh '__DOMAIN__, __SLUG__.<droplet-ip>.sslip.io'
|
# own port, service user, data directory and access log, and this script
|
||||||
# The argument is the Caddy site address line: one name, or several
|
# touches nothing that belongs to another game. Copy it and Caddyfile.tmpl
|
||||||
# separated by commas. Every name must already resolve to this droplet.
|
# to the host and run ON it as root; safe to run again.
|
||||||
# Caddy serves the built files and terminates TLS; setup-server.sh adds the
|
# bash setup-droplet.sh '__DOMAIN__, __SLUG__.<host-ip>.sslip.io'
|
||||||
# game server behind it.
|
# The argument is this game's Caddy site address line: one name, or several
|
||||||
|
# separated by commas. Every name must already resolve to this host.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
HOST="${1:?usage: setup-droplet.sh <hostname>}"
|
HOST="${1:?usage: setup-droplet.sh <site address line>}"
|
||||||
|
|
||||||
apt-get update -q
|
apt-get update -q
|
||||||
apt-get install -qy curl rsync
|
apt-get install -qy curl rsync
|
||||||
|
|
||||||
# Caddy (auto-HTTPS)
|
# Caddy (auto-HTTPS), once per host.
|
||||||
apt-get install -qy debian-keyring debian-archive-keyring apt-transport-https
|
if ! command -v caddy >/dev/null; then
|
||||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
|
apt-get install -qy debian-keyring debian-archive-keyring apt-transport-https
|
||||||
| gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
|
||||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
|
| gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||||||
| tee /etc/apt/sources.list.d/caddy-stable.list
|
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
|
||||||
apt-get update -q && apt-get install -qy caddy
|
| tee /etc/apt/sources.list.d/caddy-stable.list
|
||||||
|
apt-get update -q && apt-get install -qy caddy
|
||||||
|
fi
|
||||||
|
|
||||||
# Site directory, readable by Caddy.
|
# Node 22, once per host; every game runs its server on it.
|
||||||
mkdir -p /opt/__SLUG__/build
|
if ! command -v node >/dev/null || [[ "$(node -v)" != v22* ]]; then
|
||||||
chown -R root:caddy /opt/__SLUG__
|
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||||||
chmod -R g+rX /opt/__SLUG__
|
apt-get install -qy nodejs
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The host's Caddyfile only gathers the games' site files. A Caddyfile that
|
||||||
|
# is neither Caddy's stock one nor this is a single-game host from before the
|
||||||
|
# shared layout: stop rather than overwrite it.
|
||||||
|
ROOT_CADDY='# Kestrel'"'"'s Hall: every game on this host brings its own site file.
|
||||||
|
import sites/*.caddy'
|
||||||
|
mkdir -p /etc/caddy/sites
|
||||||
|
if ! grep -q '^import sites/\*.caddy' /etc/caddy/Caddyfile 2>/dev/null; then
|
||||||
|
if [ -s /etc/caddy/Caddyfile ] && ! grep -q 'root \* /usr/share/caddy' /etc/caddy/Caddyfile; then
|
||||||
|
echo "/etc/caddy/Caddyfile holds another site; move it into /etc/caddy/sites/ first" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf '%s\n' "$ROOT_CADDY" > /etc/caddy/Caddyfile
|
||||||
|
fi
|
||||||
|
|
||||||
|
# This game: its user, its directories, its site.
|
||||||
|
# The service user shares the slug. A slug that is already a system group
|
||||||
|
# (Ubuntu ships "dip", "games", "audio" and others) cannot also be a user:
|
||||||
|
# stop and say so, and choose another user name in this script and the unit.
|
||||||
|
if ! id -u __SLUG__ &>/dev/null; then
|
||||||
|
if getent group __SLUG__ >/dev/null; then
|
||||||
|
echo "a system group is already called __SLUG__; pick another service user name" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
useradd -r -m -d /opt/__SLUG__-home __SLUG__
|
||||||
|
fi
|
||||||
|
mkdir -p /opt/__SLUG__/build /opt/__SLUG__/app /var/lib/__SLUG__/rooms /var/log/__SLUG__
|
||||||
|
chown -R root:caddy /opt/__SLUG__/build
|
||||||
|
chmod -R g+rX /opt/__SLUG__/build
|
||||||
|
chown -R __SLUG__:__SLUG__ /var/lib/__SLUG__
|
||||||
|
sed "s|__HOST__|$HOST|" "$(dirname "$0")/Caddyfile.tmpl" > /etc/caddy/sites/__SLUG__.caddy
|
||||||
|
|
||||||
# Caddy: the site and the game server's paths, from the template beside this script.
|
|
||||||
sed "s|__HOST__|$HOST|" "$(dirname "$0")/Caddyfile.tmpl" > /etc/caddy/Caddyfile
|
|
||||||
caddy validate --config /etc/caddy/Caddyfile
|
caddy validate --config /etc/caddy/Caddyfile
|
||||||
# Validation as root touches the access log; Caddy runs as caddy and must own it.
|
# Validation as root touches the access logs; Caddy runs as caddy and must own them.
|
||||||
chown -R caddy:caddy /var/lib/caddy
|
chown -R caddy:caddy /var/lib/caddy
|
||||||
systemctl restart caddy
|
systemctl reload-or-restart caddy
|
||||||
|
|
||||||
# Firewall: ssh + web only.
|
# Firewall: ssh + web only.
|
||||||
ufw allow OpenSSH
|
ufw allow OpenSSH
|
||||||
@@ -38,4 +71,4 @@ ufw allow 80/tcp
|
|||||||
ufw allow 443/tcp
|
ufw allow 443/tcp
|
||||||
ufw --force enable
|
ufw --force enable
|
||||||
|
|
||||||
echo "droplet ready: now run deploy/deploy.sh <ip> from your machine"
|
echo "host ready for __SLUG__: now run deploy/deploy.sh <ip> from your machine"
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Adds the game server to a droplet that setup-droplet.sh already prepared.
|
|
||||||
# Copy this script and Caddyfile.tmpl to the droplet and run ON the droplet
|
|
||||||
# as root; safe to run again. Installs Node 22, creates the service user and
|
|
||||||
# data directory, and teaches Caddy to hand /api and /ws to the server while
|
|
||||||
# it keeps serving the static site itself.
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
if ! command -v node >/dev/null || [[ "$(node -v)" != v22* ]]; then
|
|
||||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
|
||||||
apt-get install -qy nodejs
|
|
||||||
fi
|
|
||||||
|
|
||||||
id -u __SLUG__ &>/dev/null || useradd -r -m -d /opt/__SLUG__-home __SLUG__
|
|
||||||
mkdir -p /opt/__SLUG__/app /var/lib/__SLUG__/rooms
|
|
||||||
chown -R __SLUG__:__SLUG__ /var/lib/__SLUG__
|
|
||||||
|
|
||||||
# Caddy: rewrite the site's configuration from the template, keeping its
|
|
||||||
# address line, so the game server's paths are routed before the files.
|
|
||||||
HOST_LINE=$(head -1 /etc/caddy/Caddyfile)
|
|
||||||
sed "s|__HOST__|$HOST_LINE|" "$(dirname "$0")/Caddyfile.tmpl" > /etc/caddy/Caddyfile
|
|
||||||
caddy validate --config /etc/caddy/Caddyfile
|
|
||||||
systemctl reload caddy
|
|
||||||
|
|
||||||
echo "server prerequisites ready: now run deploy/deploy.sh <ip> from your machine"
|
|
||||||
@@ -12,7 +12,8 @@ from collections import Counter
|
|||||||
BOT = re.compile(r"bot|crawl|spider|slurp|facebookexternalhit|slack|discord|twitter|preview|fetch|curl|python|go-http|wget|headless|scan|monitor|uptime", re.I)
|
BOT = re.compile(r"bot|crawl|spider|slurp|facebookexternalhit|slack|discord|twitter|preview|fetch|curl|python|go-http|wget|headless|scan|monitor|uptime", re.I)
|
||||||
PAGES = {"/": "hall", "/rules": "rules", "/guide": "guide"}
|
PAGES = {"/": "hall", "/rules": "rules", "/guide": "guide"}
|
||||||
SITE = "__DOMAIN__"
|
SITE = "__DOMAIN__"
|
||||||
LOGS = "/var/lib/caddy/access*.log*"
|
# Each game on a shared host keeps its own access log.
|
||||||
|
LOGS = "/var/lib/caddy/__SLUG__-access*.log*"
|
||||||
|
|
||||||
|
|
||||||
def scan(day, logs=LOGS):
|
def scan(day, logs=LOGS):
|
||||||
|
|||||||
@@ -56,12 +56,12 @@
|
|||||||
{:else if room.spectating}
|
{:else if room.spectating}
|
||||||
<section class="move">
|
<section class="move">
|
||||||
<h2>You watch from the Peanut Gallery</h2>
|
<h2>You watch from the Peanut Gallery</h2>
|
||||||
<p class="muted">{room.awaitingText ? `${room.awaitingText} ${room.awaiting.length === 1 ? 'is' : 'are'} still choosing.` : 'The round is being written.'}</p>
|
<p class="muted">{room.awaitingText ? `${room.awaitingText} ${room.awaiting.length === 1 ? 'turns a card over and over, still choosing.' : 'turn their cards over and over, still choosing.'}` : 'Cards are being chosen, face down.'}</p>
|
||||||
</section>
|
</section>
|
||||||
{:else}
|
{:else}
|
||||||
<section class="move">
|
<section class="move">
|
||||||
<h2>Round {g.rounds.length + 1}: name a number</h2>
|
<h2>Round {g.rounds.length + 1}: name a number</h2>
|
||||||
<p class="muted">The highest number named by exactly one player scores. First to {TARGET}.</p>
|
<p class="muted">Every player lays a card face down; the highest card nobody else laid takes the point. First to {TARGET}.</p>
|
||||||
<div class="picks">
|
<div class="picks">
|
||||||
{#each Array.from({ length: HIGHEST }, (_, i) => i + 1) as n (n)}
|
{#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>
|
<button type="button" class="pick" class:chosen={pick === n} disabled={room.moved} onclick={() => (pick = n)}>{n}</button>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export function resolveRound(previous: State, inputs: Record<SeatId, Input>): St
|
|||||||
if (scorer) state.players[scorer].points += 1;
|
if (scorer) state.players[scorer].points += 1;
|
||||||
state.rounds.push({ picks, scorer });
|
state.rounds.push({ picks, scorer });
|
||||||
if (scorer && state.players[scorer].points >= TARGET) {
|
if (scorer && state.players[scorer].points >= TARGET) {
|
||||||
state.over = { winner: scorer, reason: `${state.players[scorer].name} reached ${TARGET} points.` };
|
state.over = { winner: scorer, reason: `${state.players[scorer].name} lays down the last card and sweeps the table: ${TARGET} points, and the game.` };
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user