A cosmetic effects layer on the board svg, fed by the same event
stream that writes the log. Fireballs streak and burst; waterbolts
arc and splash; lightning jags and flickers; teleports shimmer at
both ends; walls rise and fall in dust; stopped attacks flash a
golden shield; hits ring red; misses whiff; other attack spells
sparkle at the caster. Successive visuals from one command stagger by
a beat, everything self-expires, reduced-motion hides the layer
whole, and nothing here touches game state — the effects are mapped
from events in fx.ts and drawn in Board.svelte, online and hotseat
alike.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two ways out of the game, one cleanup. A wizard killed by damage took
their monsters, sustained spells, and armed ambushes with them; a
wizard eliminated by treasure loss left everything standing — a fire
imp scorching for a dead master, a Medusa still gazing at a corpse.
Both paths now share one sweep, gated at rev 14 so stored games keep
their orphans acting as recorded.
Also: Lifesaver lasts until used, carried as a near-billion-turn
sentinel — which the scoresheet printed. Durations that mean forever
no longer show a number.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First wizard was never random: createGame has always rolled a die
per player from the seed, re-rolling ties, and the gameStarted event
carried every pip — straight into a log line that said only "X goes
first." Now the table sees the ceremony: a dice modal as the boards
flip, one row per wizard, the trophy on the high roll, a note when
tied wizards rolled again — in online games and hotseat alike. The
chronicle line keeps the full roll-off too. Pure presentation; the
dice were always honest.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"The creature shares your square — there is nowhere to hide" appeared
on no card and in no rulebook. Neither did the convention beneath it:
the engine skipped INVISIBLE and SHRINK miss rolls for creature blows
on the theory that a monster in your square has nothing to aim. The
cards say otherwise — SHRINK reduces the chance to hit "in any
attack", and INVISIBLE's D4 carves out no exception for monsters.
From rev 13, creature blows roll the same dice as every other attack
(1 finds the unseen; 1-2 find the tiny), INVISIBLE may be counter-
cast against a creature's blow, and the automatons will do so. The
rolls consume RNG, so earlier games keep their certainty and replay
without them — the refusal survives only there, reworded to blame the
revision instead of inventing a rule.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
INVISIBLE's corner reads NEUTRAL/COUNTERACTION, and ANTI-ANTI's own
face names it an escape — yet the engine refused it at the door. Now
it counters: the vanishing goes up mid-stack (duration from a NUMBER
card riding the counteract command), the attacker's 1-in-4 hit roll
happens at resolution, the spell lingers after the exchange, and
ANTI-ANTI cannot pin it. WATERWALL — "Acts as counteraction to
FIREBALL" — now mirrors WALL OF FIRE's waterbolt-stopping role and
counts as a total stop. WALL OF FIRE itself joins the total-stop set
at rev 12 only: stored rev-10/11 games bounced after it and replay so.
While responding, tapping a number card holds it to ride the next
counter (unless a displayed Shieldstone makes the number itself the
counteraction, as before). The automatons learn the trick too: vanish
when the blow is heavy and the shields are spent, and never waste
ANTI-ANTI against an escape.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Relocate Sector could never land a sector west or north of the origin
— placements the physical game allows by simply sliding the whole map
across the table. The coordinate grid still cannot go negative, but
now it does not need to: a sector may land at negative coordinates
and the whole maze renormalizes, zero-anchoring on both axes (which
also pulls the maze snug when the origin corner is vacated, instead
of leaving a blank band). The client offers the new landings as ghost
slots on every side, verified slot-for-slot against engine truth.
The move also closes a real gap: remapState never carried creatures,
boobytrap tokens, glue, open safes, or dimensional warp tokens when a
sector relocated or rotated — they were left hovering at their old
coordinates. Sectors now carry everything standing on them. Both
changes ride rules rev 11; older ledgers replay their flaws intact,
as replay determinism demands.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Slow Death, countered with Force Field — and the battle modal popped
right back up as if nothing had happened, goading a second counter
out of a hand that had already won the exchange. The engine was
bouncing every attacker's pass back to the defender so counters could
stack; correct for partial stops (Blunt, Absorb, Reflection numbers),
senseless past a total one. Under rules rev 10, when a Full Shield,
Force Field, Teleport escape, Full Reflection, or Reverse stands
unnullified, the attacker's declined answer resolves the stack on the
spot. Older ledgers keep the bounce and replay unchanged.
And when the exchange legitimately returns — a partial counter
standing, or a shield broken by Anti-Anti — the modal now says so:
"Your Blunt stands — only what slips past it will land," or "Your
Full Shield was nullified — the attack comes on unchecked," instead
of replaying the fanfare of a fresh, unanswered attack.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Playtesting read the card right and the engine had it wrong. "Placing
the A tokens on one set of exits & the B tokens on the other" means
the two exits you mark BECOME a pair — and their former partners pair
with each other — not that the two exits trade destinations. Under
rev 9, click the exit and then the exit you want it to connect TO
(the hint now says so); already-connected exits are refused. Earlier
games keep the swap their logs were validated against. Test pins the
full four-way rewiring.
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>
The cards settle what the morning's feature got wrong. DIMENSIONAL
WARP: "There is no L.O.S. through the warp." The AUTO WARP: "for all
purposes, treat the connected board edges as though they are
adjacent" — an explicit grant, for edges that physically meet at a
corner. The lettered wraparounds get no grant at all: their rule is
about walking. So the pattern is that warps carry movement, not
sight, unless the text says otherwise — and only the aisle corners
say otherwise.
Warps now carry an aisle flag (the 3-player AUTO WARP and the
5-player corner arcs); hasWarpLineOfSight honors only those, with
the adjacency geometry — diagonals through the corner mouth —
unchanged from this morning where it rightfully applies. Games begun
before rev 7 keep the wider sight their logged commands were
validated against, via a legacy flag boardView stamps from the
game's rules revision. Tests pin all three states: lettered mouths
see nothing, aisle mouths see the corner fan, legacy games see
everything they used to.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"It's not letting me cast Absorb Spell on your Full Shield" — and
Jolly's FAQ says exactly why: "ABSORB SPELL will work on FULL
REFLECTION or REFLECTION, but not FULL SHIELD, as that is not used
against you, but cast upon the player using it." The refusal stands,
now with that reason in the error — and the neighboring case the
engine wrongly refused is fixed: an attacker may answer a reflection
with ABSORB SPELL, nullifying it and taking the reflection card into
their hand, with the original spell then landing unturned.
And the card face corrects my own hours-old work: ANTI-ANTI "does not
work against escape, such as SHRINK, TELEPORT, or INVISIBLE" — so
under rules rev 6 it can no longer pin a teleport escape. Earlier
games keep their stored chains.
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 WRAITH card assumes the window exists — "REFLECTIONs used on the
wraith's touch will damage the wraith" — but creature damage applied
instantly, so BLUNT had nothing to catch, as playtesting found. Under
revision 4 every creature blow against a wizard opens the same
counteraction stack a spell does: the wraith's entry touch, the
democratic monster's claw, and commanded troll/skeleton/shadow
attacks. The blow's damage rides the stack; BLUNT halves it (round
up); reflections work by name against creatures — half back for
REFLECTION, the whole blow for FULL REFLECTION — landing on the
creature, not its controller; FULL SHIELD correctly does nothing
("does not stop any physical attack"); the wraith's card theft is a
secondary effect that lands only if damage does. No aim-miss rolls
against invisible or shrunk defenders — the creature is already in
the square. The attack fanfare shows the creature's own card.
Earlier revisions keep the instant touch so every stored game — and
the live one that found this — replays unchanged.
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>
"Not applicable in a 2-player game." — the card face. New games deal
from deck revision 2, which removes it when exactly two wizards sit
down; three or more keep it. The revision travels in GameConfig and
the persisted start line, and stored games without one replay against
the original build — changing an existing game's deck composition
would scramble its deterministic replay. Test pins all three cases:
removed at two, present at three, present in legacy two-player games.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A three-reviewer sweep for tells of piecemeal machine generation,
every finding verified against the code before touching it. No
behavior changes; the full suite passes unchanged (plus two
strengthened pins).
Engine: removed four void-silenced fossils (a parseEdgeKey call
voided where it stood, stoneEffect's ignored cardId parameter, the
actualTarget remnant in doCast, a voided loop variable in shadow
upkeep); fixed the initialize-then-overwrite narration in
spawnCreature; replaced a filter(() => false) no-op; waterwall now
rides waveFromEdge instead of carrying its own verbatim copy (and the
single-caller washBack wrapper went with it); blind wall-bumps and
LOS blockers each collapsed to one implementation; the wand-id list
and the "permanent" duration sentinel became named constants; the
ambush number local no longer shadows the imported numberValue
function; assorted reviewer-aimed phrasings rewritten as the
constraints they guard.
Server/deploy: the protocol header now documents all eleven message
types; dropped an eslint pragma with no eslint, a test script with no
tests, and an rsync exclude anchored at a path that never existed
(the real data/ dir now excluded); the Caddy vhost has one source of
truth; stale "pending DNS" note removed — the record resolves.
Web: ~90 lines of CSS swallowed verbatim into a mobile media query
deduplicated; the reduced-motion guard on the board now actually
stops the marked-cell pulse; one shared color module replaces two
drifted palettes; an orphaned doc comment rejoined its function.
Tests: the ten-times-pasted helper block became test/helpers.ts;
wave-numbered files renamed for the behaviors they pin; deliberation
comments and void-ed corpses of unwritten assertions deleted; silent
seed-dependent early-returns now fail loudly; one assertion that
compared a value to itself now pins the home-translation it meant to;
the stored-log single-number command form gained the explicit
compatibility test it deserved.
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>
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>
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>
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>