'Destroys all magical stones an opponent is CARRYING' — the owner's
ruling: a stone hidden in the hand is a secret card, not a carried
stone. Only displayed stones burn; the old all-hand burning also leaked
hidden information by announcing cards nobody knew existed. Earlier
games replay their broader fire. All 42 production ledgers verified
before deploy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
Room QD3S: Kestrel cast Fear and a Democratic Monster warp-stepped from
eight squares away to ONE — moveCreature never knew the card existed.
'No player or monster will move to within 3 spaces of you' now guards
every willing move: walking wizards (as before), commanded monsters
(walks, wall-passes, and warp mouths alike), dimensional warp-steps,
and self-teleports. Forced movement — knockback, waves, Mental Force,
Teleport Opponent — is not willing, and still throws you anywhere.
The shared fearRepels helper replaces the inline wizard-only check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
'When a player picks up one of your treasures, you may play at that
time (out of turn) this card on him' — the owner read the card and is
right: no arming ahead. When a treasure whose owner holds WARD is
grabbed, the grab hangs (new wardPending phase, outranking all other
input) and the owner alone answers: spring it (3 damage, card spent)
or let them go (card kept, silence). Automatons always spring it on a
thief of their gold. The arming mechanic is refused at rev 31 and its
button hidden; rev 3-30 games keep their armed wards and rev 1-2 keep
the automatic spring, replaying unchanged. The window does reveal that
the owner holds SOMETHING - as it would at any real table when the
room turns to look at them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
A wizard eliminated by treasuresLost took any carried treasure into the
void with them — the killed path drops it, but checkVictory's branch
never did. At rev 30 the treasure lands where they stood, with the
usual treasureDropped event (onHomeOf included, so a capture completed
by the falling is honored in the same breath). Stored games replay the
vanishing unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
Per the owner's design — and truer to a real table, where the cast is
public: every illusion wall's location is open knowledge, rendered as a
shimmering wall to any player whose eyes have not yet ruled on it.
Testing is an explicit act: tap the shimmer (or bump into it) and a
modal explains the 50/50 and offers 'Roll to test your eyes' — the new
testIllusion command rolls once, then the wall either hardens or
dissolves for you alone. Nobody's verdict is moved by watching someone
else stroll through (the Q3WZ mystery: an automaton walking through
its own round-1 fake). Untested walls block movement and sight without
consuming RNG; the pre-29 lazy belief roll is preserved for stored
games. Automatons doubt free shimmers on their best crossing before
spending any card on that wall.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
FAQ: 'If this gets FULLY REFLECTED, you lose the rest of your turn and
cannot draw cards (in addition to damage).' When the returned bolt
stuns its own caster mid-turn, their remaining actions end at once and
the end-of-turn draw comes up empty (new turn.drawForbidden flag); the
normal stun still costs them the next turn. The affected player sees
'Your own bolt has left you reeling' in place of the usual spent-turn
slip. Older revisions played on and stored games replay so.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
'FULL REFLECTION lets the other player decide which objects, if any,
will be swapped' — the reflector's choice now rides their counteract
(params.cardId, reflector's item first), resolved with the roles
swapped when the reflection settles; 'none' or no choice trades
nothing. The client walks the reflector through the same give-and-take
picker, with a 'swap nothing' refusal.
Also from room XRT7: the trade picker now honors each room's own rules
revision (older rooms trade only object-typed cards), so it can no
longer offer a wizardblade a rev-25 engine will quietly refuse — and a
fizzled swap finally says so in the log instead of vanishing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
The pact fix: a clockwork no longer attacks a wizard it holds a BUDDY
pact on — Eric watched one sign the pact and punch him the next turn.
A pact is torn up for exactly one thing: a kill. And the post-attack
pact is signed only by a wizard who wants OUT of the fight (hauling
gold, bleeding, or with other enemies left) — a healthy duelist keeps
its options, and mutual-pact stalemates stay out of bot wars.
Swap Meet drops the 'name a card' prompt for a real trade picker:
click the other trader, choose one of your carried items, then claim
one of their displayed items. Rules rev 26 widens the engine's match
from object-typed cards to every movable object (daggers, rocks,
wands, stones); older games matched narrowly and replay so.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
A FULL REFLECTION's returned spell is a fresh attack on its own caster —
who, per the owner's ruling, gets a defender's counteraction window
against it (absorb the blast, shield it, or FULL REFLECTION again: the
ping-pong war ends when the cards run out). The reflection now settles
into a new stack with the roles swapped and the damage pre-priced
(numbers, amplifies, and surviving counters already applied); the blow
returns to its sender unerringly, with no fresh hit rolls. onResolved
effects run with the swapped roles, so a reflected IDIOT now afflicts
its own caster. Earlier revisions land the blow instantly and stored
games replay unchanged.
Automatons price the incoming reflected damage off the stack; the attack
modal announces 'Your own spell comes screaming back!' with the inbound
total.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
Rules rev 24: a waterwall wave that finds a victim dist cells from its
source has only range-dist spaces of push left — a range-2 wave throws its
adjacent victim two spaces but a victim at its far edge only one, and
spent force never converts into crush damage. Applies to WATERWALL and
both STONE TO WATER waves; older revisions keep the flat full-range wash
so stored games replay unchanged.
Automatons now use DESTROY WALL on the march: two BFS distance maps (from
the bot, from its objectives) price every visible wall by the shortcut its
removal opens; the bot blasts when it saves 4+ steps of walking — or when
no road exists at all — never standing beside the blast unless trapped
and healthy. The card also leaves the shed pile: discardValue 2 -> 6.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
Rules rev 23 — IDIOT, per the card and FAQ:
- No handling items: pick up / drop of treasures and objects refused
(dropping was the exploit: capturing a stolen treasure on your own home,
or dropping your own treasure underfoot for an instant cure)
- No punching, no thrown dagger / large rock (attacks on players)
- No effect on a victim already carrying one of their own treasures
Ungated (permissive): counteractions are now castable while idiotized (the
one thing the card expressly allows — the gate wrongly blocked them), and
goal-aiding spells (IDIOT_AIDS: destroy-wall, teleport, mad-dash, ...) per
the FAQ's 'you could, however, destroy a wall'.
Sight tracing: while an LOS attack sits on the stack, the board draws the
line it traveled — straight when direct, leg by leg through both warp
mouths (with pulsing rings) when the maze's wraparound carried it. Engine
traceSight/traceSightFor/stackSightTrace; overlay in Board.svelte; shown
live and in replays. Verified against H3PC's cross-board Idiot cast.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
A democratic monster touching its own creator opens a stack where one
player wears both hats, and doPass matched the attacker first: every
pass "bounced the exchange to the defender" — the same wizard —
forever. Room 928D livelocked there at seq 83; once the boot kick
revived its pump, the automaton politely passed 389 times. At rev 22
a self-stack pass RESOLVES (the claw lands, the game moves on);
older ledgers hold their recorded bounces and replay so. 928D itself
was repaired by truncating the no-op run and lifting it to rev 22,
verified clean and unstuck on a copy before installation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bot pump only ran when a command or join arrived, so a server
restart landing mid-bot-turn left the restored room waiting forever
for a human to poke it — room 928D sat exactly there, its automaton
holding a perfectly answerable stack. Boot now kicks the pump for
every restored still-running room, two seconds after restore.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DESTROY WALL's "anyone in either square next to the wall takes 4
points" swept only wizards; a troll leaned on the falling masonry
untouched. The expansion sheet is explicit — "if a card specifies
'Opponent' or 'Anyone' as a target, this includes Monsters" — so the
collapse now crushes creatures in both squares, at the rim breach's
far side too. Earlier ledgers keep their charmed monsters.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rulebook (Cards And Actions That Change The Map): a CREATE WALL
or DESTROY WALL alteration on a junction between sectors rolls a
50-50 when "some other moron" rotates one of them — 1-2 stays with
the standing sector, 3-4 travels with the moving one, per the book's
own low/high convention. Both rotation and relocation roll (the same
seam tears either way), demolished holes roll alongside conjured
walls, the pips land loud in the chronicle, and a traveling edge
rides anchored to its in-sector cell. Rev-gated: the roll consumes
dice that stored games never spent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Destroying a wall on the board's outer edge used to leave a one-sided
hole into nothing. The table's physics prevail: the outer rim wraps,
so the breach goes clean through — the opposite perimeter wall in the
same row or column crumbles too (collapse damage and all), and a new
warp pair opens between the two edges, shimmering at both mouths.
Rev-gated: stored games hold one-sided breaches and replay so. Sector
moves recompute the wraparounds and forget improvised openings, as
the maze's own reshaping always has.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Its one-attack-per-round refreshed inside beginTurnFor when the turn
index matched the roll-off winner's seat — and only the living begin
turns, so once that wizard fell the claw never refreshed again: one
attack, then a lifetime of harmless bumping. The refresh now lives at
the true round boundary in the turn-advance loop, which passes dead
seats without skipping the count. Earlier ledgers keep their gummed
monster and replay so.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stone to Water's wave (and every waterwall collapse) washed wizards
back and crushed them against the maze — while a skeleton in the
same water stood dry. The wave only knew creatures well enough to
drown fire imps. From rev 17 it carries them all: washed back along
the surge, one point of crush per space the maze refuses, fire imps
still destroyed outright. Earlier ledgers keep their dry monsters
and replay so.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A believed ILLUSIONARY ATTACK, the goat's ram, and the hurled
treasure all deal their damage inside the attack's own resolution
hook — past the pipeline where the empathy mirror lived, so a linked
caster walked away unbloodied. One resolvedBlow helper now carries
the mirror to all three: "any attack done in ANY form" means these
too. Rev-gated: proactive empathy predates this, so stored ledgers
may hold unmirrored blows and replay them as struck.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The table holds doors open to cast back through them at pursuers;
our held door opened for feet but not for eyes. From rev 15 a held
door is an open doorway to sight — casting at players, creatures,
and squares sees through it, the client's dimming aid and the
automatons alike (they share sightedCellsFor). REMOVE LOCK's "still
considered to block L.O.S." stands: that speaks of a CLOSED door,
and an unheld unlocked door still blocks. Rev-gated because rev-14
ledgers may already contain holds and must replay blind.
Pinned three ways: the holder blasts a pursuer through the held
doorway; an unheld unlocked door still blocks; rev 14 stays blind.
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>
"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>
Scoped to everything since the last pass (695307d). The residue of
fast iteration, removed: a reduced-motion media query that had
swallowed a full copy of the .faq-seal rules; doc comments orphaned
from their functions by inserted methods; the FAQ scrape's seams
(section headings run into ruling bodies under the wrong topics, a
next-page heading shipped as a ruling, an amputated "h", and rulings
filed under alphabetically-nearest strangers — Large Rock/Dagger now
lives on those cards; Book of Spells and Torquemada describe no card
in this set and are gone); the write-only aisle flag left behind by
the reverted rev 7; a linter-silenced dead destructure; a duplicated
median lookup; dead casts; and the fallback path that ignored the
apprentice's one-card draw.
Tests now typecheck (tsconfig includes test/), which surfaced the
missing type imports and a drifted creature literal hiding under an
as-cast. Also: a no-op self-assignment, mid-file imports hoisted, a
dynamic-import habit made static, a hedge comment replaced with a
loud setup failure, the fallback-retries-itself dead rung removed
from both bot drivers, and the twin peek scrims merged.
Deliberately kept: the TIERS lookup guard (ledger JSON is untrusted),
the wand-cost stanzas (they differ on the power-attack trade — a
rules question, not a dedup), and rollDie's coexistence with rollD4
(migrating changes visible logs; future work).
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>
Tiers degrade resources and repertoire, never judgment — the design
constraint was that no tier may ever look dumb. The APPRENTICE draws
one card a turn instead of two (a poorer wizard, not a worse one),
spends counters only on heavy hits (thrift, not blindness), and
carries a modest spellbook: damage, summons, stones, keys, and
treasure play, with no afflictions, amplifies, ambushes, guarding
tricks, or deja-vu. The ADEPT draws fully and knows everything except
ambushes and amplify. The ARCHMAGE is the full curriculum. Every card
any tier plays, it plays correctly.
The lobby workshop gains a tier picker beside the temperaments
(default adept); the tier persists with the seat, shows in roster and
scoresheet ("⚙ apprentice mystery"), and rides the drive loop.
Measured where it should matter: in berserker combat mirrors the
archmage beats the apprentice two to one, while pure treasure races
stay honest — the handicap lives in the card exchanges a human
actually feels, pinned deterministically in the tournament suite.
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>
"Random" read as random ACTIONS, and a revealed random pick is barely
a mystery anyway. The fourth workshop button is now "mystery": the
server rolls the temperament and keeps it — the join line records it
for replay, the drive loop plays it, but the room tells the table
only "mystery". Roster and scoresheet show ⚙ mystery; the machine's
behavior is the only tell.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
One command per beat — 1.5 seconds, the replay's 1x cadence — each
broadcast with its board state as it lands, starting a beat after
the human's own action settles. The pump is per-room, re-entrancy
guarded, and stops the moment the maze wants a human (or the game
ends). Watching an automaton stalk your treasure across the maze is
now a spectacle instead of a teleport.
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>
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>
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>
makeRoomCode checked only the in-memory map, which covers every live
and restored room — but a file that failed to restore stays on disk
with no map entry, and a new room taking its code would append a
second game into the orphaned file, corrupting both. The generator
now rerolls on disk presence too.
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 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>
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 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>
"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>
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>