Credibility pass: the seams get sanded

Scoped to everything since the last pass (3308850). Three blind
reviews, every finding verified before touching anything.

Confirmed and fixed: two identical comment-splitting insertions left
doc comments orphaned from their fields (net and local alike); a
51-line CSS fossil of the pre-extraction inline effects survived in
Board.svelte; the rev-13 miss-roll test asserted tautologies while
its comment claimed a check the code never made — it now proves the
die was consumed, and the skeleton is no longer returned as trollId;
the sprite registry's `as never` silently disabled the completeness
its annotation advertised (now a mapped type, one cast at the
dispatch seam); a dead ternary guarded a union that doesn't exist;
Bolt carried a duplicate .fork rule from a color iteration; fxTtl
contradicted three sprites' real animation lengths; the permanence
sentinel was reinvented as a magic 9000 (the engine now exports
isPermanentDuration); CELL was declared thrice (fx.ts now imports
it); App and Replay ran two divergent fx schedulers (one scheduleFx
now, cancellable — stale flourishes can no longer fire after leaving
a game); TokenArt retried missing files forever; the anti-anti
escape guards merge with the gate asymmetry explained; wall-of-fire's
rev-12 carve-out is marked; overLimit ignored a displayed BRAINSTONE
(bots over-discarded by two); botRemark's header mis-stated its own
branches; deliverGold fired on any drop, not a home-base delivery;
escape and win banter never fired from the steps that carry them.

Rejected: "as a human would" (house voice); FxGallery's dev-harness
framing (trimmed one plea, kept the facts).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-17 12:12:03 -04:00
co-authored by Claude Fable 5
parent 4528c66057
commit d2b40ec6f8
16 changed files with 116 additions and 130 deletions
+8 -4
View File
@@ -232,18 +232,22 @@ function thiefOfMine(view: GameView) {
return livingEnemies(view).find((p) => carriers.has(p.id)) ?? null;
}
function overLimit(view: GameView): number {
return Math.max(0, view.yourHand.length - 7);
/** The hand limit as this seat experiences it (a displayed BRAINSTONE
* raises it by two — mirrors the engine's handLimit). */
function handLimitOf(view: GameView): number {
return me(view).displayed.some((c) => c.cardId === "brainstone") ? 9 : 7;
}
function overLimit(view: GameView): number {
return Math.max(0, view.yourHand.length - handLimitOf(view));
}
/** End the turn drawing fully — shedding dead weight first if the hand is
* too full to take the draw. doEndTurn caps the draw by the room left, so a
* clogged hand never refreshes; only cards the brain has no play for are
* shed (good counters and attacks are hoarded, as a human would). */
function endTurnDrawing(view: GameView, tier: TierTraits): Command {
const limit = me(view).displayed.some((c) => c.cardId === "brainstone") ? 9 : 7;
const deficit = tier.draw - (limit - view.yourHand.length);
const deficit = tier.draw - (handLimitOf(view) - view.yourHand.length);
if (tier.sheds && deficit > 0) {
const shed = [...view.yourHand]
.filter((c) => discardValue(c) <= 3)
+11 -5
View File
@@ -138,6 +138,10 @@ const WAND_CARD_IDS = ["blaster-wand", "shift-wand", "sticky-wand", "warp-wand"]
/** Duration meaning "for the rest of the game" ("This card is permanent."). */
const PERMANENT_TURNS = 1_000_000_000;
/** Whether a sustained duration means "until used/removed", not a count. */
export function isPermanentDuration(turns: number): boolean {
return turns >= PERMANENT_TURNS / 2;
}
/** Which square contents block line of sight. */
export const LOS_BLOCKING_CONTENT: Record<SquareContent["kind"], boolean> = {
@@ -4760,11 +4764,11 @@ function doCounteract(
if (card.cardId !== "anti-anti") return err("only ANTI-ANTI or ABSORB SPELL can answer a counteraction");
if (!targetCounter) return err("no counteraction to nullify");
// "Does not work against escape, such as SHRINK, TELEPORT, or INVISIBLE"
// (the card face; rules rev 6 — earlier games replay their old chains).
if ((state.config.deckRev ?? 1) >= 6 && targetCounter.card.cardId === "teleport") {
return err("ANTI-ANTI does not work against escape");
}
if (targetCounter.card.cardId === "invisible") {
// (the card face). Teleport is rev-6-gated — earlier games replay their
// old chains; invisible needs no gate, since no earlier engine ever
// accepted it as a counteraction for a ledger to contain.
if (targetCounter.card.cardId === "invisible" ||
((state.config.deckRev ?? 1) >= 6 && targetCounter.card.cardId === "teleport")) {
return err("ANTI-ANTI does not work against escape");
}
takeFromHand(player, instanceId);
@@ -4801,6 +4805,8 @@ function doPass(prev: GameState, playerId: PlayerId): CommandResult {
stack.counters.some((c) =>
!c.nullified &&
(TOTAL_STOP_COUNTERS.has(c.card.cardId) ||
// Wall-of-fire counters pre-date the total-stop rule; their stored
// exchanges bounced, and replay so (rules rev 12).
(rev >= 12 && c.card.cardId === "wall-of-fire")) &&
(stack.kind === "spell" || c.card.cardId === "teleport"));
if (!totalStop) {