The great simplification: every vintage gate collapses, the count restarts at 1

All 50 rooms were retired (backed up on the droplet and in Spaces), so no
ledger needs an old branch to replay: forty revisions of deckRev gates
fold into one canonical ruleset — the newest behavior everywhere. The
ward's arming, the idiot's steering, the lazy illusion roll, the legacy
redirection swap, and the flat wave all leave with their gates; RULES_REV
restarts at 1 for whenever the rules fork again. Old-vintage test pins go
with them. The opening screen now quietly drops seats whose rooms the
server no longer knows, instead of listing them "unreachable" forever.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-21 12:45:35 -04:00
co-authored by Claude Fable 5
parent e8df6efcde
commit 8a50ff7f36
19 changed files with 383 additions and 685 deletions
+7 -12
View File
@@ -784,7 +784,7 @@ function respond(view: GameView, style: AutomatonStyle, tier: TierTraits): Comma
}
// Or vanish: INVISIBLE gives the attacker a 1-in-4 hit and lingers after.
const vanish = find("invisible");
if (vanish && (!stack.creatureId || view.deckRev >= 13) && incoming >= 3 - flinch) {
if (vanish && incoming >= 3 - flinch) {
const num = numbersInHand(view)[0];
return {
type: "counteract", instanceId: vanish.instanceId,
@@ -957,11 +957,6 @@ function selfCare(view: GameView, style: AutomatonStyle, tier: TierTraits): Comm
// Three or more wizards: LIFESAVER takes elimination off the table.
const lifesaver = inHand(view, "lifesaver");
if (lifesaver && view.players.length > 2) return { type: "cast", instanceId: lifesaver.instanceId };
// The ward guards the gold while its owner is away robbing yours
// (arming retired at rev 31: the window asks in the moment instead).
if (view.deckRev < 31 && inHand(view, "ward") && !view.yourWardArmed) {
return { type: "armWard", armed: true };
}
const enemyNear = livingEnemies(view).some(
(p) => Math.abs(p.position.x - self.position.x) + Math.abs(p.position.y - self.position.y) <= 3,
);
@@ -1323,12 +1318,12 @@ export function automatonCommand(
}
}
// IDIOT's curse: the maze no longer forces the feet (rev 37), so the
// IDIOT's curse: the maze no longer forces the feet, so the
// clockwork honors the march to its own gold itself — and when a thief
// carries that gold, DROP OBJECT (the rev-37 carve-out) shakes it onto
// the floor where it can finally be stood upon.
// carries that gold, DROP OBJECT (an IDIOT aid) shakes it onto the
// floor where it can finally be stood upon.
const cursedIdiot = view.sustained.some((e) => e.cardId === "idiot" && e.targetId === view.you);
if (cursedIdiot && view.deckRev >= 37 && view.turn.round > 1 && !view.turn.attackUsed) {
if (cursedIdiot && view.turn.round > 1 && !view.turn.attackUsed) {
const dropper = inHand(view, "drop-object");
if (dropper) {
const sighted = sightedCellsFor(view);
@@ -1365,8 +1360,8 @@ export function automatonCommand(
pathToward(view, self.position, objectives, { canUnlock }) ??
pathToward(view, self.position, enemyCells, { canUnlock });
// A shimmering illusion on the best crossing costs nothing to doubt:
// roll the free test before spending any card on that wall (rev 29).
if (view.deckRev >= 29) {
// roll the free test before spending any card on that wall.
{
const crossing = bestWallCrossing(view, self, objectives, canUnlock);
if (crossing && crossing.total < (path?.distance ?? Infinity)) {
const k = edgeKey(crossing.cell, crossing.side);