Commit Graph
168 Commits
Author SHA1 Message Date
Eric WagonerandClaude Fable 5 da4ab03135 The reveal remembers the hand a wizard fell holding
Elimination destroys the evidence: a killed wizard's cards pass to
the killer, a treasure-lost wizard's go to the discard — so the
post-game reveal showed "empty-handed" for exactly the player whose
hand everyone wants to see. Both elimination paths now snapshot the
hand at the moment of death (finalHand), the finished view serves it,
and the gallery marks those rows "as they fell" in script. Because
rooms replay from the command log, already-finished games show their
fallen hands too. Test kills a wizard holding a Fireball and a 6 and
finds them in the reveal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 11:20:10 -04:00
Eric WagonerandClaude Fable 5 e7b17f7643 The finished table lays every hand face-up
Once the game is won, the view stops keeping secrets: revealedHands
carries every player's cards, and the table replaces your hand row
with a face-up gallery — each wizard's name (trophy on the winner)
beside their final hand, tap any card to enlarge, "empty-handed"
written in script for those who died with nothing. In hotseat this is
the thing the winner passes around after dismissing the fanfare; in
async games it greets each player when they open the finished game.
Redaction still guards every phase before "finished."

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 11:17:07 -04:00
Eric WagonerandClaude Fable 5 13aeeea0ef Victory gets its moment: a gold-ringed proclamation
Winning showed only as a small slip in the rail — anticlimactic, as
playtesting put it. The engine now records HOW the game was won
(winReason: treasures carried home, or last wizard standing) alongside
the winner, the view carries it, and the finish opens a gold-ringed
modal: trophy, the winner's name in tall caps, and the manner of
victory in a handwritten line. "Behold the final board" dismisses it
to the finished table. In hotseat the one screen proclaims to the
whole table at once; in async games each player is greeted by it when
they next open the game.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 11:11:24 -04:00
Eric WagonerandClaude Fable 5 8f7beb5ef3 The card-naming field suggests as you type
Illusionary Attack, Deja-Vu, Card Erasure, and the other name-a-card
spells asked for exact spell names from memory. The field is now a
native datalist: type a few letters and matching card names appear,
tuned per card — Illusionary Attack offers only attack spells (it can
only pretend to be one), Drop Object offers only the movable objects
plus Treasure, and the rest offer the whole playable pool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 11:04:14 -04:00
Eric WagonerandClaude Fable 5 33b36feb8b Illusion Wall is castable: it was missing from the edge-target list
The engine has always accepted it — edge target, line of sight to the
edge, open corridor only — but the client's EDGE_CARDS list never
included illusion-wall, so selecting it offered no clickable wall
lines and the card was simply uncastable. The LOS dimming that
appears alongside is correct and stays: placement requires sight of
the wall line, so the lit squares show where the shimmering can go.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 10:58:32 -04:00
Eric WagonerandClaude Fable 5 db1fbf7099 Duration spells show themselves: chips on the sheet, size on the board
view.sustained was never rendered — every duration spell in play was
invisible, Big Man included, as playtesting found. Now each player's
scoresheet row lists the spells on them as violet chips (name and
turns remaining, tap to read the card), beside their displayed cards.
And on the board itself, Big Man makes the wizard token tower half
again as large, Shrink dwindles it — the exact at-a-glance "I have
become big" the table owes you.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 10:55:45 -04:00
Eric WagonerandClaude Fable 5 7ad3e7acc8 Selecting a creature opens its card, with living stats
Tapping a creature you command now does both things at once: selects
it for orders AND opens its card in the inspector corner, with a
stats line pinned beneath — hits left, moves left, attack ready or
spent (or barred on the turn it appears; the shadow reports itself
unharmed by ordinary damage). The same stats ride along on any
hold-to-peek of a creature, and they are derived live, so marching
the monster updates the numbers as you spend its moves. While
commanding, the hint strip carries the same line beside the hand,
with a "done" to dismiss. Deselecting closes the card.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 10:51:46 -04:00
Eric WagonerandClaude Fable 5 d34173df9f Hold-to-read works on occupied squares — creatures and wizards included
The press-and-hold card peek lived only on the floor rects, and any
creature or wizard token sitting there swallowed the pointer events —
so the democratic monster, whose plain tap selects it for commands,
had no path to its card at all. The hold gesture now lives on the
tokens too (a fired hold swallows the tap so nothing double-fires),
and the board suppresses the mobile long-press callout so the gesture
stays clean on phones.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 10:49:59 -04:00
Eric WagonerandClaude Fable 5 6e6691c6c0 Creatures step through board-edge warps by click, like wizards
The engine always warped a creature marched off a lettered opening —
verified headless — but the table offered no way to ask for it: cell
clicks for a selected creature matched only geometric neighbors, and
the warp arrows moved only your wizard. Now a selected creature
standing on an opening lights the arrow (and, per the existing cue,
its destination), the arrow marches the creature through, and
clicking the far mouth directly does the same.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 10:46:35 -04:00
Eric WagonerandClaude Fable 5 c4dafe1df7 Creatures with orders left announce themselves in the rail
Summoned monsters act during their controller's turn, but nothing
reminded you — a democratic monster in play was invisibly waiting on
every single player. Now each creature you may command this turn gets
a moss-green slip in the paper rail: name, moves remaining, whether
its attack is ready (respecting no-attack-on-creation and the
democratic monster's one-attack-per-round), and the tap-to-command
hint. The democratic monster reminds every player on their turn,
since all players get to move it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 10:43:13 -04:00
Eric WagonerandClaude Fable 5 97a3305d07 Every token on the board can be read as its card
"What does a rosebush do again?" now has an in-game answer: press and
hold any square (450ms) to open the card behind whatever occupies it —
creature, terrain, dropped object, warp token, or boobytrap marker —
in the view-only inspector corner. A plain click that claims no game
action peeks too: out of turn, every tap reads; on your turn, taps
beyond your reach read instead of doing nothing, and clicking an
opponent's creature with nothing selected reads its card. Priority
when a square holds several stories: creature, then terrain, then the
topmost dropped object.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 10:39:56 -04:00
Eric WagonerandClaude Fable 5 a9d40d73ac Face-up cards show under each name on the scoresheet
At the table, a displayed card sits in front of its wizard for all to
see — Master Key, Wizardblade, the stones, wands. The view always
carried every player's displayed cards, but only your own hand marked
them. Now each scoresheet row lists that player's face-up cards as
parchment chips, wands with their remaining charges as pips, and
tapping a chip enlarges the card in the inspector corner, view-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 10:34:28 -04:00
Eric WagonerandClaude Fable 5 4f3083c490 Targeting dims out-of-reach squares; Speedstone pays out immediately
Selecting an L.O.S. card now drops every square you cannot see into
shadow, leaving eligible targets lit — computed by the engine's own
hasLineOfSight over the player's VIEW, so illusion walls you still
believe in block your aim, stone and bushes and the Big Man block
everyone's, and Around The Corner widens the light one bend when
attached. ADJACENT cards light just your square and its four
neighbors. The shade paints over tokens and wizards alike, and
switches off while arming an ambush, since ambushes aim at the
future. A new sightedCellsFor lives in the engine view module so the
client can never disagree with the rules.

Speedstone, meanwhile, only took effect at next turn's allowance
recompute — displaying it mid-turn granted nothing, as playtesting
found. It now bumps the current allowance by one on display (a delta,
so number-card movement already played survives), unless SLOW or a
zeroed allowance says otherwise. Test covers the immediate grant and
the persistence into later turns.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 10:33:02 -04:00
Eric WagonerandClaude Fable 5 a0d862ca10 Attacks announce themselves; the response moves beside the hand
Being attacked was announced only by a slip in the paper rail, easy
to miss mid-turn — the playtest complaint was real. Now an attack on
you (or a counter to your spell) opens a modal: the attacker's name
in red Oswald caps, the actual card face enlarged, the number backing
it if any, a bare 👊 for a punch, and a single "To arms!" stamp.
Dismissing it leaves the actionable prompt in the hint strip beside
your hand — where the counteraction cards you would tap actually
live — with the "let it resolve" escape. The rail keeps a one-line
reminder. Each new decision point in a counter-chain re-announces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 10:26:58 -04:00
Eric WagonerandClaude Fable 5 f9eff3341b Dimensional warps get the edge-warp treatment
Placing the two tokens worked, but using them asked too much: the
only affordance was a "Step through the warp" button in the actions
row, easy to miss and far from the board. Now standing on a warp
token pulses its twin with the same green dashed ring the lettered
edge openings use, and clicking the twin steps you through — one
gesture, learned once, working for both kinds of warp. The button
stays for those who found it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 10:21:27 -04:00
Eric WagonerandClaude Fable 5 57bd036417 The first square of a two-square spell now shows on the board
Dimensional Warp, Trader, Redirection, and Relocate Sector all take
two clicks, but only the hint bar acknowledged the first — the board
itself sat unchanged until the second click landed. The first chosen
square now wears a pulsing amber dashed ring (same rhythm as the warp
destination ring, still under prefers-reduced-motion) until the spell
completes or is cancelled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 10:17:57 -04:00
Eric WagonerandClaude Fable 5 f0a264147f Security hardening for public playtesting
The server code learns to distrust strangers: a 64KB WebSocket payload
cap (the ws default is 100MB — an easy OOM on a 1GB droplet), a
per-connection token-bucket rate limit, caps on concurrent sockets,
total rooms, rooms per connection, pending transfer codes, and seats
per myGames query. Player names are stripped of control characters
and bounded at 24 chars, room codes at 8, serialized commands at
16KB before they touch the append-only log. Catch-up replays — a
full game rebuild per request — get a 3-second cooldown. Unexpected
exceptions now log server-side and send strangers a bare "internal
error" instead of the exception text.

One real bug found by the sweep: myGames compared the client's raw
seat token against the stored hash, so the lobby ledger silently
matched nothing since tokens were hashed at rest — and the comparison
wasn't timing-safe either. It now goes through the same timingSafeEqual
path as every other seat check, via a new exported seatTokenValid.

The droplet tightens too: the game server binds loopback (HOST env)
so port 8787 no longer answers the internet — it was reachable
directly, plaintext, bypassing Caddy — and ufw now allows only ssh,
80, and 443. The systemd unit gains a sandbox (ProtectSystem=strict,
ProtectHome, NoNewPrivileges, PrivateTmp, MemoryMax=700M so a runaway
process is killed and restarted before it takes the box down) and
execs tsx directly instead of through npx. Caddy adds HSTS, nosniff,
frame-denial, and no-referrer headers; setup-droplet.sh records all
of it for future rebuilds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 10:08:23 -04:00
Eric WagonerandClaude Fable 5 789d927122 Daggers, rocks, blades, and wands can be set down too
The rulebook's movable-object list is wider than the stones: "magic
stones, treasure chests, the dagger, the large rock, and the
wizardblade" — and the expansion expects wands to change hands, since
"its remaining charges go with it." A new isMovableObject helper in
the engine names that full set, doDropObject honors it, and the hint
bar's "Drop it here" button follows suit. Charges are keyed by card
instance, so a dropped wand carries them to whoever picks it up —
covered by new tests, along with the refusal to drop spell cards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 09:59:09 -04:00
Eric WagonerandClaude Fable 5 c04ed1e0ee Objects can be set down and picked back up
The engine always knew how — dropObject and pickUpObject were in the
command set from the start — but the table never offered a hand for
it. Now selecting a magic stone (or any object card) on your turn
shows a "Drop it here" button in the hint bar, and standing on a
square with objects lying on it offers a "Pick up <name>" stamp per
object, disabled once your actions have ended (picking anything up
ends them, per the rulebook). The chronicle narrates both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 09:55:04 -04:00
Eric WagonerandClaude Fable 5 7a231cbe1b About: the story of the game, told on the box lid
A handwritten "about this game — a labor of love" link on the box lid
opens a new About tab in the booklet: what Wiz-War is, that Tom Jolly
created it in 1983, that this is a faithful reproduction of the
out-of-print Chessex sixth edition (with its one expansion) that Eric
and his friends played for countless hours in the early nineties —
rebuilt from that very copy, card by photographed card, wall by
verified wall, with the original hand-inked token art. It names Steve
Jackson Games as the current rightsholder, states plainly that this
is an unofficial, non-commercial fan work made with love and
admiration, and closes by pointing anyone the game delights toward
the edition currently in print and a real table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 09:43:09 -04:00
Eric WagonerandClaude Fable 5 45666bca16 Ambushes: the async-native form of Interrupt and Opportunity Fire
Eric's diagnosis: "in the moment" interruption cards are worthless in
correspondence play — there is no moment. The answer was already in
the game: WARD is a contingency card, and ambushes generalize it. On
your turn, playing Interrupt or Opportunity Fire now arms an ambush:
commit it with an attack from your hand (plus an optional number
card) and a trigger — an opponent entering your line of sight, coming
beside you, or grabbing a treasure — and it springs automatically
when the condition occurs, whether you are watching or asleep. The
sprung attack opens the normal counteraction stack, so the victim
gets their defense (asynchronously, like any attack). Committed cards
leave your hand until the trap springs or you disarm it; ambushes are
invisible to everyone but their owner (view-level redaction), die
with their owner, stay armed if the shot is momentarily illegal, and
honor the no-combat first round. Live play in the moment still works
too — and the client now actually offers it (the old UI never let
you click those cards out of turn). The rail shows your armed traps
with a disarm control; the chronicle announces AMBUSH! when one
springs. 124 tests passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 01:36:53 -04:00
Eric WagonerandClaude Fable 5 295ad2ad55 Catch-up replays and named attention for async games
"While you were away": returning to a game with unseen moves shows a
banner — "You missed N moves. Watch what happened" — that opens a
replay reel. The server rebuilds the game and captures a redacted
per-move view for the viewer (own hand only, capped at the last 200
moves); the client plays the reel on a full board with the actor and
humanized events captioned per step, auto-advancing with pause,
step-back/forward, arrow-key control, and skip-to-now. Seen progress
is tracked per room in the browser (every state broadcast now carries
the log sequence), so the banner only appears when there is genuinely
something to watch.

Attention between turns is now named, not just signaled: game
summaries carry WHY a game waits on you — your turn, counteract
(you're being attacked mid-someone-else's-turn), forced discard, or a
pending interruption — the ledger prints it ("UNDER ATTACK —
respond!"), and browser notifications say "you are under attack in
GNSK!" rather than a generic your-turn.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 01:26:27 -04:00
Eric WagonerandClaude Fable 5 4cbf5a013e Lobby standee picker for online rooms
Waiting rooms now show the six wizard standees: tap to claim yours,
claimed ones gray out with the claimant named for screen readers, the
roster shows each player's chosen standee (or "choosing..."), and
conflicts are refused first-come. Choices sync live to everyone in
the room, resolve to first-free defaults for the undecided when the
host flips the boards, persist in the start line of the room log (so
restored games keep their colors), and flow through the same engine
config field the hotseat picker uses. Also fixed: the static-serving
realpath guard crashed the dev server when no client build exists —
it now 404s static requests instead. Verified end to end: claim,
conflict refusal, second pick, start, and in-game colorIndex 5/2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 01:19:42 -04:00
Eric WagonerandClaude Fable 5 77ae65b263 Pick your standee: hotseat wizards choose their color while naming
The naming round now shows the six real wizard standees; each player
taps theirs before typing their name, claimed standees gray out, and
the next player defaults to the first free one. The choice travels as
an optional colors array in the engine config, the view exposes each
player's colorIndex, and the board, score sheet, treasure chests, and
wizard art all follow the chosen color instead of seat order —
persisting through hotseat saves. Verified: Alice claimed the yellow
standee and marched onto the board as the yellow wizard with yellow
treasure chests. (Online rooms still assign by seat order; a lobby
color picker can reuse the same config field later.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 01:15:07 -04:00
Eric WagonerandClaude Fable 5 98d7dee5c7 Hotseat setup round: pick a count, then each wizard names themselves
The comma-separated name field is gone. Hotseat now starts with a
2-6 count picker and "Gather N wizards", which opens a naming round
in the pass-the-device style: "Wizard 1 of 3 — what is your name?",
type it, pass the device on; the last wizard's button reads "Flip the
boards" and starts the game. Duplicate and empty names are refused
in place, the roster-so-far shows beneath, and "never mind" backs
out. Verified end to end: three names entered, game started, opening
handoff went to the die-roll winner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 01:12:13 -04:00
Eric WagonerandClaude Fable 5 ac82c28750 Fix lobby overflow on phones: global box-sizing and wrapping rows
The box lid combined width:100% with 2.6rem side padding and no
box-sizing reset, so the card ran off the right edge of real phones
(Join, Play Here, and Claim all clipped). A global border-box reset
fixes the class of bug; the hotseat and claim rows now wrap, their
inputs go full-width on narrow screens, and the lid tightens its
padding and title below 900px.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 01:07:32 -04:00
Eric WagonerandClaude Fable 5 30aa1e778d Tap-to-inspect: selecting a card floats an enlarged, readable copy
Selecting any card in hand now also shows a 1.55x (1.3x on phones)
enlarged rendering floating at the corner — full card text readable
without pinch-zooming — with a dismiss button, auto-reset when the
selection changes, and the board left clear for choosing a target.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 01:06:18 -04:00
Eric WagonerandClaude Fable 5 0927836e7e Mobile pass: hand above the paperwork, dvh sizing, tight masthead
At phone width the layout now flows board -> actions -> your hand ->
turn slip -> score sheet -> chronicle, instead of burying the cards
beneath the paperwork below the fold. The board caps at 56dvh (dvh
throughout, for iOS's shifting address bar), the chronicle stops
hogging height, and the masthead holds one line. The hand is a
thumb-swipeable card row; pinch-zoom stays enabled for reading fine
card text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 01:00:23 -04:00
Eric WagonerandClaude Fable 5 6ac50c4d58 Harden the static file server against path traversal (security review)
The internet-facing static handler now decodes the URL (rejecting bad
encodings and null bytes), anchors the containment check with a
trailing separator so sibling-prefix directories cannot slip past,
and realpath-resolves the final file to defeat symlink escapes —
serving only what provably lives inside the built client directory.
Deployed and probed live: normal requests 200, literal and
percent-encoded traversal attempts both 403.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 00:56:16 -04:00
Eric WagonerandClaude Fable 5 23bdd009a0 Deploy to a DigitalOcean droplet: wizwar is on the internet
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>
2026-08-16 00:54:15 -04:00
Eric WagonerandClaude Fable 5 002dbd2547 Crowded squares: every token type gets its own zone, multiples fan out
A square can legally hold terrain, a wizard or three, two treasures
(the win condition itself), floor objects, creatures, a warp token,
and a boobytrap disc all at once. The cell is now zoned: terrain
underlays the whole square, wizards fan across the top, treasures fan
across the bottom, floor objects stack bottom-left, creatures fan in
from the right, the dimensional warp shrinks into the top-left
corner, and boobytrap discs sit top-right. Two treasures on a home
base — the winning tableau — now reads as two treasures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 00:27:05 -04:00
Eric WagonerandClaude Fable 5 bd8ca99dc0 The real 1993 token art takes the board
All 45 tokens cropped from Eric's photos of the physical counter
sheets by connected-component segmentation (with cable/reflection
filtering, merged-blob rescue, and one hand-tuned crop for the red
treasure the charging cable was hugging). Terrain squares, monsters,
wizards, treasures, floor objects, and dimensional warps now render
the genuine hand-inked art: solid stone, thornbush, rosebush, killer
ooze, dustcloud, slime, tacks, pit, safe, skeleton, troll, wraith,
fire imp, democratic monster, shadow, alter ego, dagger, rock,
master key, magic stones and wands, the six wizard standees, and the
six color-keyed treasure chests. Wizards and creatures wear a thin
controller-color ring; art tokens cast a soft cardboard shadow;
terrain art passes clicks through to the floor while wizards and
creatures stay clickable. Every vector rendering remains in place as
automatic fallback (USE_TOKEN_ART flag plus per-kind mapping), per
Eric's request. Sprites live in packages/web/public/tokens at 144px.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 00:26:06 -04:00
Eric WagonerandClaude Fable 5 9323399b60 Phase 3: hotseat play — the whole game in one browser
Hotseat runs the engine entirely client-side; no server is involved.
The lobby takes comma-separated names (2-6 wizards), and the game
plays through the same table UI, with one addition: a full-screen
hand-off card between actors — "pass the device to Morgana, tap when
only they can see the screen" — shown whenever the needed input moves
to another wizard (turns, counteractions, forced discards,
interrupts). Each player sees only their own hand while seated. The
game saves itself to localStorage after every command (config +
command log, replayed on resume — the server's own determinism
trick), so "set the game aside" keeps it and "abandon game" forgets
it; a Resume Hotseat button appears whenever a save exists. Fixed en
route: the engine's structuredClone cannot digest Svelte's reactive
proxies, so hotseat snapshots state before every engine call.
Verified live: 3-player game started, reloaded, resumed, turn ended,
device handed to the next wizard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 00:13:59 -04:00
Eric WagonerandClaude Fable 5 85b40be113 Support 2-6 players: the L, the cross, and the 3x2 from the rulebook
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>
2026-08-16 00:08:15 -04:00
Eric WagonerandClaude Fable 5 8d10dfee67 In-game help: how-to-play guide, the 6e rules, and a card library
"Help & rules" in the masthead (lobby and table alike) opens a paper
booklet with three tabs: How To Play (the client's own controls —
moving, casting, attaching numbers and modifiers, fighting, turns,
transfers), The Rules (the 6th edition rules condensed faithfully
from the verified rulebook transcription, exact numbers throughout,
with a Jolly Games colophon), and a Card Library — all 135 playable
cards rendered as the real card faces, searchable by name or text,
each labeled with its quantity and set. Escape closes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 00:06:51 -04:00
Eric WagonerandClaude Fable 5 0221e91ba3 Hash seat tokens at rest (security review finding)
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>
2026-08-16 00:03:36 -04:00
Eric WagonerandClaude Fable 5 ce8b6a315b Harden seat transfer phrases (security review finding)
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>
2026-08-16 00:00:13 -04:00
Eric WagonerandClaude Fable 5 2184721ab4 Seat transfer phrases: carry your games to another device
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>
2026-08-15 23:58:52 -04:00
Eric WagonerandClaude Fable 5 bc8013863b Phase 2: async play-by-turn — the games ledger and turn signals
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>
2026-08-15 23:53:13 -04:00
Eric WagonerandClaude Fable 5 7cfffb8ab5 Persistent games: rooms survive server restarts by log replay
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>
2026-08-15 23:46:23 -04:00
Eric WagonerandClaude Fable 5 89961bbb59 ADD joins two number cards on movement, as printed
"You may add two NUMBER cards together for any single action" —
movement included. The engine accepts a second movement number when an
ADD accompanies it (once per turn); the client spends your Add
automatically and says so on the button. 120 tests passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 23:41:37 -04:00
Eric WagonerandClaude Fable 5 42ea9b8f2c Voluntary discards, and no card ever casts on the first click
The rulebook allows discarding cards purely to churn for new ones, but
the client had no way to start it: a "Discard cards..." action now
switches the hand into marking mode (with a "never mind" exit), and
confirming sends the discard. Alongside it, the hair-trigger casts are
gone: selecting a card never plays it anymore — untargeted spells and
stone displays get a Cast button in the hint bar, and a bare number
card gets an explicit "Play for +N movement" button. Every play is
now a two-step, which is the correct cost in a game with no undo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 23:35:02 -04:00
Eric WagonerandClaude Fable 5 d7afe3a5ca Letter the warp openings and show where they lead
Paired openings now share a printed letter (A, B, C ...), exactly as
the physical boards mark them — making the 2-player crossed pairings
legible at a glance. While standing on an opening, its destination
square pulses with a dashed green ring, so "leave at B, re-enter at B"
stops being a leap of faith.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 23:31:42 -04:00
Eric WagonerandClaude Fable 5 87a5067c77 Make wraparound warps usable: click the arrow to step through
The engine's warps worked all along (verified in all six directions),
but the client offered no way to trigger one short of clicking the
destination cell across the map. The warp arrows are now buttons: when
you stand on an opening its arrow lights green — click it to step
through. Clicking a far arrow behaves as a click on its cell.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 23:28:51 -04:00
Eric WagonerandClaude Fable 5 3e1fe059fa Disable treasure buttons when they cannot apply
Pick Up Treasure is disabled unless an uncarried treasure sits on your
square (and your actions haven't ended); Drop Treasure unless you are
actually carrying one. The warp-step button reuses the same derived
state. Buttons stay visible for stable layout — they just stop
inviting impossible clicks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 23:26:20 -04:00
Eric WagonerandClaude Fable 5 8c85c13e15 Widen the room-code input so CODE fits with its letter-spacing
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 23:25:06 -04:00
Eric WagonerandClaude Fable 5 9501a85be4 Design pass: the client becomes Eric's table
The design system is the physical set itself, taken from the scans:
card-stock cream, warm ink, and the six wizard marker colors, laid on
a dark leather tabletop. Cards in your hand are now the actual cards —
vertical corner rails (ATTACK / L.O.S. / ADJACENT), condensed gothic
titles, brown body text on cream, number cards as big bordered
numerals with per-value border styles; selecting lifts a card from
the fan. The board matches the physical tiles: ink-outlined white
brick walls on cracked flagstone, plank doors, eight-point home stars
in each wizard's color, swirled treasure discs, warp arrows. The
sidebar is paper: a turn slip, the rulebook's own hand-scrawled
life-points score sheet (Caveat over ruled lines), and a typewritten
chronicle that scrolls itself. The lobby is the box lid. Type:
Oswald condensed display / Archivo Narrow body / Courier Prime log.
Buttons are ink stamps. Board height-caps so board, actions, and the
full hand share one screen; responsive stack below 900px; reduced
motion respected. Verified with live screenshots at 1440x900.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 23:21:03 -04:00
Eric WagonerandClaude Fable 5 c9524a37db Thumb Of God lands: the 6th edition is 100% implemented
Eric chose the "divine meteor" redesign for the one card that cannot
be digitized faithfully (the physical version has you flick the die at
the board from six inches). Digital form: aim at a square in sight;
the die drifts 0-2 squares in a random direction, then every token in
and around the landing square — ground objects, treasures, creatures,
and wizards alike — is flung to a random nearby square. Walls mean
nothing to falling cardboard; tokens knocked off the board settle at
the nearest edge, per the original card; there is no counteraction.

With this, all 128 unique cards of the 6th edition game (69 basic +
59 Expansion Set #1) are implemented, tested, and playable online.
The "unimplemented card" guard test now points at an Expansion #2
shelf card, which is the only kind left. 119 tests passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 23:05:47 -04:00
Eric WagonerandClaude Fable 5 21dcd532a3 Add Swap Home Bases — the audit's one escapee
Neutral, LOS to the other player, legal only when both home bases hold
an equal number of treasures. With this, every card in the 6th edition
game is implemented except Thumb Of God, which awaits its digital
redesign. 118 tests passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 23:02:43 -04:00
Eric WagonerandClaude Fable 5 06eea72ded Expansion wave 4: 27 combat, curse, and utility cards
Fortune: GIFT FROM ABOVE (+3, no ceiling), GIFT FROM BELOW (a trap in
the deck — 3 damage on the draw, harmless in the opening deal).
Modifiers: POWER ATTACK burns life into any damage spell. Curses:
WEAKNESS (drop your treasure, take double, carry nothing), STRENGTH
(double physical dealt; the two cancel), WALKING DEAD (half a point
per space walked, forever), DISEASE (the victim becomes a carrier who
infects everyone in squares they enter), IDIOT (the victim shambles
toward their nearest own treasure, able only to counteract, until
they stand on it and ask "What am I doing here?"). Defense: EMPATHY
(attacks bite their caster too), FORCE FIELD (spell-stopping
counteraction). Mischief: MENTAL SWAP (trade whole hands), MENTAL
FORCE (march someone three spaces), BUTT-HEAD (become a goat, ram for
the distance charged), HEAVE-HO (throw your carried treasure as a
weapon), THIEF and SWAP-MEET (item larceny), CHAOS (all hands in a
pile, shuffled, redealt), ILLUSIONARY ATTACK (a fake spell that hurts
if believed), WARD (a trapped treasure bites its thief), REMOVE CURSE
(strip any duration spell, rolling to hit the shrunken or invisible),
SWARTHMORE'S ENCHANTMENT (+1 on an enchanted object). Space-time:
DIMENSIONAL WARP (paired step-through tokens), REDIRECTION (swap two
outer exits' wraparounds), BIG MAN (fills the corridor: no entry, no
punches, no casting past him), FEAR (nobody approaches within 3),
and the out-of-turn pair — INTERRUPT and OPPORTUNITY FIRE — which
open a one-action window in another player's turn. Only THUMB OF GOD
remains, awaiting its digital redesign. 117 tests passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 23:01:42 -04:00