The automaton now plays number cards where they matter: attached to
value-scaled attacks (waterbolt, powerthrust) choosing the biggest
for damage, and played for movement when the goal is just out of
stride. It raises its monsters when no wizard is in spell range and
commands them every turn — creatures march at enemies by the same
hazard-shy BFS and maul whoever shares their square. Targets are
picked by lowest life.
And it has moods. The HUNTER plays the classic game: gold first,
violence when convenient. The BERSERKER hunts wizards over treasure
and finishes games by last-wizard-standing. The WORRIER paths around
enemies, counters at a lower threshold, shields against chaos, and
never brawls. Hosts pick the temperament when seating one (or the
workshop assigns at random); it persists with the seat, shows in the
roster, and the tournament harness proves every pairing finishes.
Across ten seeds of berserker versus hunter: six treasure wins to
four kills — the moods play genuinely different games.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 4 begins. The automaton is a pure function in the engine —
automatonCommand(view) — playing from its own redacted GameView, the
same information a human seat receives: hidden hands stay hidden from
the clockwork. It ranks simple damage spells, counters what hurts
(full shield at 3+, reflection at 4+, blunt at 2+), discards its
worst cards by a value order, BFS-pathfinds to enemy treasures and
home again, refuses to path through hazards, brawls when there is
nothing to steal, and always has a safe fallback; the server's drive
loop steps any bot-held seat through the same runCommand path as
humans, so bot commands log, persist, replay, and broadcast like
anyone's.
Hosts seat them pre-start with "⚙ seat an automaton" (Automaton,
Automaton II, ... V); bot seats persist as tokenless join lines and
restore on boot. Proven three ways: bot-vs-bot engine games conclude
across seeds in 8-14 rounds (~100 commands — human-scale), a full
four-automaton table finishes, and a live websocket game of human
vs. automaton ended with the clockwork carrying two treasures home
through a do-nothing opponent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live chat interleaved correctly, but a reload sank every past message
to the bottom: the rejoin sent the whole game history first and the
chat history after. Talk is now a tableTalk event in the room's event
stream at the moment it was said — the room file already preserved
the interleaving, since chat lines append between command lines — so
rejoining players read the banter between the battle lines where it
happened. The separate chatHistory message retires; unread counting
rides the event stream instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every player-facing D4 roll now lands in the chronicle as pips with
its purpose: "🎲 alice rolls a 3 — aiming at the unseen — only a 1
finds them." A shared rollD4 helper threads the generic dieRolled
event through blind staggers and swings, pit leaps, ooze and pit
struggles, and the invisible/shrink miss rolls — the table sees the
die land, exactly as it would in person. Rolls whose numbers already
showed (the troll's swing, the misdirection direction, the opening
roll-off) keep their lines.
And the physical die's other job — "any 50-50 call" — gets its
button: 🎲 beside the say-box in online games (server-rolled,
published through the chat ledger so it persists, replays, and counts
toward unread badges) and under the chronicle in hotseat, rolled on
the device. Chronicle only; no game state touched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rules rev 8 closes the last two fidelity threads. A SPEED bonus turn
burns a turn of duration spells on the hastened wizard — recipient-
counted, per the FAQ's most obscure ruling — with self-cast durations
already burning through the normal turn-start sweep (expiry extracted
into expireEffect so both paths share the cleanup). And nothing can
be created on a DIMENSIONAL WARP token, as the card face always said.
The general-topic FAQ sections (Combat, Line of Sight, Monsters,
Treasures, and five more) join the rules tab verbatim under a double-
ruled divider, completing the FAQ's absorption: card rulings on the
cards, general rulings in the rules.
The four promoted UX items: cell-target spells now dim ineligible
squares (creations mirror emptySquareTarget from the viewer's
knowledge, summons want clear sighted squares, teleport BFSes its
four spaces exactly as the engine does, stone-to-water lights only
stone, dispel lights only creations); hotseat games get the full
replay reel, each step seen from its actor's own seat; a "stop
announcing attacks on this device" link in the fanfare modal (undo
lives beside the notification toggle in the lobby); the corner
inspector now serves only the selected casting card, with every
examine-a-card peek unified on the centered treatment; and the dev
socket URL keys on vite's port instead of hijacking every localhost.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"When I cast Speed, it doesn't seem to do anything" — the engine had
refused correctly (actions end when you pick up an object) and said
so in a toast, but a toast fades in five seconds and leaves no trace.
Refusals now also land in the chronicle as "— reason —" lines, the
way hotseat games always recorded them, so the answer to "why did
nothing happen" survives long enough to be read.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A say-box under the chronicle sends table talk to the room; messages
land in the event log itself, styled as parchment asides — banter
between the battle lines, the way it happens at a real table. Talk is
seat-authenticated, stripped and capped at 300 chars, rate-limited by
the existing bucket, persisted as chat lines in the room file (the
game replay ignores them; the room keeps its last 500), and replayed
to anyone joining. Async games get unread badges in the lobby ledger
(💬3), cleared by watching the table, and catch-up replays speak the
lines at the step where they were said. Public to the whole room, no
whispers — and no chat in hotseat, where the table talks for itself.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
moves like a giant, and the boards are diagram-verified
TELEPORT as a counteraction (official FAQ: "the attack has no chance
of hitting you"): the defender names an escape square within four
spaces, the escape resolves before anything lands, and an ANTI-ANTI
pins their boots to the floor. Additive — no revision gate needed.
FILL SQUARE WITH SLIME holds spells now: an attack cast at the slime
sticks in the gel (leaving the discard pile), springs once at whoever
is inside or next enters, and counteractions against the trapped
blast cannot touch its caster — reflections vanish into the ooze. A
five-point waterbolt washes the slime and its cargo away, and the
waterwall waves clear slime from their path.
BIG MAN, under rules rev 5, finally moves like the card says: he
pushes players and monsters down the corridor ahead of him (stuck or
unpushable occupants block his advance), steps over a pit, tacks, or
killer ooze for two movement points without ever entering the square
(click two cells beyond the hazard), and monsters may not enter his
square. All gated so stored games replay under their own rules.
The boards were never wrong — the rulebook's Set-Up Diagram photo
confirms every pairing the code already had: 2p crossed, 3p stair
with the Aisle Warp arc, 4p/6p straight-across, 5p plus with all four
corner arcs. The stale TODOs are gone, replaced by tests pinning each
letter pair, and relocation's "only opposite board edges connect"
(which discards the aisle warp) is pinned too. Wall of Fire vs
Waterbolt turned out to be implemented and tested all along — its
TODO comment was the only thing wrong.
Also: hotseat saves now request durable storage (iOS evicts
unprotected origins under pressure).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The seed + command log has been the whole game all along; now it can
be watched. Finished games offer "⟲ Watch the whole game" beside the
face-up hands: the server replays every command from seq 0 through
the same redacted catch-up pipeline (the 200-step window remains a
live-game courtesy; full replays wait for the game to finish), and
the reel plays it titled "The whole tale, from the deal" with new
1×/2×/4× speed controls for the long tellings. Each viewer rewatches
from their own seat — hidden information stays hidden in the retelling
exactly as it was at the table.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"The creature-1 takes 2 damage" — the creature events carried only
internal ids, so the log printed them. creatureAttacked, Touched, and
Damaged now carry the creature's kind, and the lines read like a
table: "The Wraith strikes!", "The Troll swings (rolled 3)!", "The
Skeleton takes 2 damage."
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
At the table the discards sit face-up for anyone to leaf through. The
view now carries the pile (deck order stays secret), the "discards N"
count in the rail became a link, and it opens a booklet-styled reader:
every cast-away card newest first, tap any to enlarge. And the
chronicle stops mumbling "discards 1 card(s)" — it names names, so a
wraith's theft reads "bob discards Fireball," exactly what a table
would see as the card hits the pile.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Sticky Wand attaches its webs as a sustained effect with the
engine-internal id "sticky-web", which is not a printed card — and
cardDef throws on unknown ids. The scoresheet's new spell chips and
the chronicle's settles-over/wears-off lines both called it on
sustained ids, so the first webbing killed every render: mid-game the
actions bar died, and after a reload the whole table came up blank.
A spellName lookup now names synthetic effects properly ("Sticky
Wand webs") and falls back to the raw id rather than ever throwing,
and the chip peek only opens ids the card library can actually show.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WARD's text says "you may play at that time" — under rules revision 3
the choice is made by setting the trap: an armWard command toggles it
on your own turn (your secret; a private wardSet event and a rail
note), and the spring fires only while set, consuming the card and
the arming together. Async games keep their agency without an
interrupt window, exactly as the ambush system solved this before.
CHAOS gains its printed interactions: "FULL SHIELD removes a player
from participation" — a defender's shield sits them out instead of
stopping the spell, and after the counter chain every bystander gets
a shield window in seat order (chaosPending; queue head owes a
response in lobby summaries, the rail says whose hand hangs in the
balance) before the pile forms among the unshielded. "REFLECTIONS
have no effect" — both reflections are refused as counters. ABSORB
SPELL still eats the whole thing through the existing absorb path.
Both changes are frozen behind deckRev 3 so every stored game — and
the live one — replays byte-for-byte under its own rules. Five new
tests cover armed/unarmed wards, shielded defenders and bystanders,
the reflection refusal, and the legacy auto-ward.
Not deployed — a live game is in progress.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"It is possible, though time-consuming, to punch a wall down. A wall
takes 20 points of damage to destroy; a door takes 15. Any attack
against an inanimate object counts as your one attack for the turn."
Damage accumulates per edge in wallDamage (remapped through sector
rotations, public in the view), fed two ways: a punchWall command for
the bare-fisted (1 point, from a square touching the edge) and attack
spells cast at an edge target — LOS to the wall for L.O.S. cards,
touching it for same-square cards, amplify and power-attack honored,
wand charges spent, no counteractions since stonework plays none.
Thrown daggers and rocks clatter to the floor at the foot of the
wall. At the threshold the edge opens through the same override path
destroy-wall uses.
On the table: damaged walls wear spreading cracks, an attack card's
hint offers "or a wall line to batter it" with the edge layer live,
and a "Punch a wall…" stamp arms a click-the-wall mode. The chronicle
counts the blows: "alice batters the wall with bare fists — 3/20."
Not deployed — a live game is in progress.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Instead of re-scanning every room's log on demand, stats.json now
accumulates beside the room files: each room is remembered at its
highest counted stage (created → started → finished), so boots and
replays reconcile without double-counting, and the tally will survive
any future pruning of old rooms. Finished games contribute their
moves, table-time, and manner of victory exactly once, at the moment
of victory.
And with an accumulator to receive them, hotseat games finally count:
each local game mints an anonymous id, pings "started" with its
player count, tracks its own between-moves clock, and on the final
move reports counts only — commands, minutes, players, win reason.
No names, no moves leave the device. The server dedupes by id, clamps
everything to sane ranges, and the booklet's tally now shows how many
of the games were hotseat tables.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A new tab in the help booklet answers "how much is this getting
played?" from the ledgers themselves: games chronicled (begun and
fought to a finish), distinct wizards seated, every spell, step, and
punch recorded, time at the table estimated by the clock between
moves (gaps over ten minutes don't count — async games tell the
truth), victories split by treasure-theft versus last standing, the
longest game, the fullest table, and the date the first game was
dealt. Computed server-side over the in-memory rooms with a 60-second
cache, fetched over the socket when the tab opens.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
"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>
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>
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>
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>
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>
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>
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>
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>
The hard six: AROUND THE CORNER attaches to any LOS attack and bends
the sight line through one intermediate cell. BLIND victims lurch in
die-rolled directions (bumping a wall costs the movement point, per
the card) and their attacks fly wherever the die says — hitting
whoever stands that way, or dissipating. UGLY drives every opponent
in sight fleeing along shortest paths (breadth-first, die-broken ties)
until they cannot see the caster. ILLUSION WALL is per-player reality:
each opponent rolls 50/50 the first time it matters and the wall is
real for believers forever — believers' views render it as a wall,
the caster and those who saw through it get a ghostly dashed line,
and Dispel Creation banishes it. ROTATE SECTOR turns a sector 90
degrees with every wall, door, wizard, treasure, object, firewall and
alteration turning in place (the home star, being the exact center,
never moves); RELOCATE SECTOR slides a sector anywhere that keeps all
sectors adjacent, reassembling the map and recomputing wraparounds.
Client: modifier attachment (Amplify/Add/Extend/Around The Corner),
two-stage relocate, rotation direction toggle, dashed known-illusions.
Every card in the 6th edition basic deck is now implemented.
81 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>