Production shape: one Node process serves the built client and the
websocket on a single port (SPA fallback, same-origin wss in the
client), with Caddy terminating auto-TLS in front. The droplet
(nyc3, $6/mo) runs it under systemd as an unprivileged user with room
files on the persistent disk at /var/lib/wizwar/rooms — deploys and
reboots cannot eat a game. deploy/ carries the one-time droplet setup
script, the systemd unit, the Caddyfile, an everyday deploy script
(build locally, rsync, install, restart), and a README. Live at
https://wizwar.104.236.96.198.sslip.io via sslip.io, so TLS needed no
DNS setup at all. Verified over the real internet: room created,
second player joined, expansion game started, hands dealt, room file
persisted. Fixed en route: rsync's unanchored "data" exclude was
stripping the engine's card database, and tsx must ship (it is the
runtime).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three player counts join the square and the column: 3 players get the
stair/L with the AUTO WARP joining the two openings that face the
concave notch (convex openings pair geometrically — flagged as
interpretation of the diagram's letters); 5 players get the plus/cross
with tip-to-tip wraps and aisle-warp corners; 6 players get the 3x2
rectangle with straight-across wraps. All six verified layouts are
drawn on, so no count repeats a sector. Rooms accept up to six seats;
the lobby copy follows. 122 tests passing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Raw seat tokens no longer touch disk or long-lived memory: rooms store
sha-256 hashes, joins compare timing-safely, sessions keep the raw
token they authenticated with only for minting transfer phrases, and
legacy plaintext room files still load (hashed on read). Verified:
rejoin and transfer both work, and the room file contains only
hostTokenHash — no raw token anywhere.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three words from 32 was ~15 bits — enumerable within a code's
lifetime against an unthrottled endpoint. Now: 64-word list, 4-word
phrases (~24 bits / 16.7M combinations), a per-connection cap of 5
failed claims, and a global backstop that voids all pending codes and
cools the endpoint after 30 failures in 10 minutes. Codes stay
speakable; a brute force now gets ~35 guesses against 16.7M before
the door closes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
While seated, "transfer seat" in the masthead mints a spoken-word
one-time phrase from the game's own vocabulary (ember-troll-dagger),
good for ten minutes. Typing it into the lobby's "claim a transferred
seat" box on any other device hands over the seat's token, adds the
game to that browser's ledger, and sits you straight down at the
table. Phrases are single-use, expire, are voided by a server restart
(seats never are), and both devices keep the seat afterward — the
phone on the couch and the desktop upstairs can play the same wizard.
Verified end to end: mint, claim, rejoin, and a second claim refused.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The lobby is now the front door to all your games. A "your games"
ledger lists every seat this browser holds — room code, whose turn it
is (including counteraction/discard/interrupt waits, which count as
your turn), the round, and how long since the last move — with
one-click resume, a forget control, and a green highlight when a game
waits on you. The server answers a token-validated myGames query with
per-seat summaries; the client polls every 45 seconds, so turns in
other rooms reach you wherever you are. Turn signals travel three
ways: the tab title flips to "● Your turn", the favicon grows a green
dot, and — opt-in via "notify me on my turn" — a browser notification
fires when a turn becomes yours anywhere. Landing in the app now
shows the ledger rather than teleporting into the last game;
mid-game socket drops still walk straight back to the table.
Verified live: two seeded games, ledger showing "gandalf's turn" and
YOUR TURN, one-click resume into the correct game with history.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every room is now an append-only JSONL file (data/rooms/CODE.jsonl):
a birth-certificate meta line, then every join, start, and command.
Because the engine is deterministic, seed + log IS the game — on boot
the server replays each file and reconstructs the exact state, hands,
deck order, and chronicle. Verified live: create, join, play, KILL
the server, restart ("restored 1 room(s) from disk"), rejoin with the
seat token — identical positions, deck count, and hand, with the
chronicle history redelivered redacted per player. The client
remembers its seat (name, room, token) in localStorage and walks back
to the table automatically on connect, clearing the memory if the
seat is stale; a quiet "leave table" control forgets it on purpose.
This is the foundation phase 2 (play-by-turn) sits on: games now wait
indefinitely for their players — and debugging restarts cost nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Creatures are first-class citizens: TROLL (D4 punches, 6 damage to
kill, regenerates at its creator's turn end), SKELETON (2-point
punches), WRAITH (walks through one wall a turn; its touch deals 2
and steals a random card), FIRE IMP (a stationary turret scorching
anyone in sight once per turn — including its creator — killed only
by Waterbolt or a Waterwall wave), DEMOCRATIC MONSTER (moved three
spaces by EVERY player on their turn, one claw per round), SHADOW (a
second body costing a life point per turn, destroyed by any damage),
and ALTER EGO (a stationary double). Monsters obey their creators,
move on the controller's turn, attack once per turn but never on
their creation turn (summoning IS your attack), refuse to strike
their creators, and vanish when their creator dies. Attacks can
target creatures directly (no counteraction window — monsters don't
counter); Dispel Creation un-creates them. Plus MEGA-MONSTER (double
a monster's toughness or speed), ADRENALINE (two attacks a turn),
MAD DASH, and LIFESAVER. Expansion Set #2 confirmed by Eric as a
5e-era product — marked historical-only in the data; the 6e game is
exactly base + Expansion #1 (200 cards, all verified). Lobby gains an
"include Expansion Set #1" toggle; the client renders creatures as
diamond tokens with select-move-attack interaction. 95 tests passing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Duration system: sustained effects expire at the start of the caster's
turns; SLOW (movement 1, no number cards, attack every other turn),
NO SPELL, MEDUSA (paralysis + damage immunity), INVISIBLE (1-in-4 hit
roll), SHRINK (50% miss, movement 2). Doors: PICK LOCK and MASTER KEY
(displayed, reusable) unlock adjacent doors until end of turn, REMOVE
LOCK is permanent, JAM LOCK seals a door for everyone. Movement:
TELEPORT (4 spaces through walls, ends movement), PASS THROUGH WALL
charges, POWER RUN (life for spaces), SWAP (consumes movement),
GO AWAY (knockback + lost turn), TELEPORT OPPONENT. Card warfare:
CARD ERASURE (named), THOUGHT-STEAL (2 random via seeded RNG),
TELEPATH (private hand reveal), POWER DRAIN (damage feeds the caster),
SUDDEN DEATH, STONE DEAD, WIZARDBLADE (same-square, number-powered,
stays displayed). Cast modifiers: AMPLIFY doubles power/duration
(stackable x2), ADD permits two number cards, EXTEND doubles duration;
REVERSE heals instead of harms but keeps secondary effects. Counters
now also halve durations (BLUNT) and split them (REFLECTION).
Security (from review findings): room codes and game seeds now come
from node:crypto, and every seat gets a secret token — reclaiming a
name in a room requires its token, closing the impersonation hole.
29 cards implemented; 51 tests passing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Server: room registry with 4-letter codes, host/join/start flow, the
authoritative command loop (seed + append-only command log per room —
the replay/async foundation), and per-player redacted views and events
broadcast after every change. Client: lobby, SVG board (floors, walls,
doors, homes, color-keyed treasures and wizard tokens matching the
physical set's six colors, warp arrows), click-to-move, click-to-punch,
card hand with tooltips from verified card text, cast flow with number
card attachment and waterbolt split, edge-click targeting for wall
spells, counteract-or-pass prompt, discard flow, end-turn draw
selector, and a humanized event log. Verified end-to-end over real
websockets with two clients: join, start, private deals, moves, turn
sync.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
npm workspaces with three TypeScript packages: @wizwar/engine (pure
game logic), @wizwar/server (Node websocket authority), @wizwar/web
(Svelte 5 + Vite client). Server handshake and client build verified.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>