feat: robustness in Module 5 (nesting guard, Death Phase limit, well-formed Assemble, multi-summon order)

A trigger is never queued while that same (minion, trigger) is resolving;
more than DEATH_PHASE_LIMIT = 64 Death Phases is an error ('Death Phase
limit'); Assemble refuses and never offers ill-formed text (wellFormed /
well_formed); summon count > 1 places tokens in summon order. Modules 05-10,
both tracks. Four new Module 5 tests, a tools test that every shipped card
is well-formed, lessons 2, 3, 5, 13 and harness.md updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-11-11 11:07:54 +01:00
1 parent 372c99c306
commit 21fb40c6f5
81 files changed
+1880 -528

No files matched your search

@@ -143,6 +143,28 @@ export function compile(def: CardDef, inPool: boolean): Card {
export const hasTrigger = (card: Card, on: TriggerOn): boolean => card.triggers.some((t) => t.on === on);
/**
* Does `text` mean what it says? For a minion, every effect clause belongs to a
* trigger (none comes before the first trigger, or after an aura or keyword), and
* every trigger has at least one effect before the next trigger, aura, keyword or
* the end. The rule is for minions (Assemble makes only minions): a spell passes.
*/
export function wellFormed(text: readonly Clause[], type: CardDef["type"]): boolean {
if (type !== "minion") return true;
// The trigger whose effects are being listed: none, one with no effect yet, or one with some.
let open: "none" | "empty" | "filled" = "none";
for (const clause of text) {
if (clause.kind === "effect") {
if (open === "none") return false;
open = "filled";
} else {
if (open === "empty") return false;
open = clause.kind === "trigger" ? "empty" : "none";
}
}
return open !== "empty";
}
/** Every card the game can create, looked up by name. */
export class CardLibrary {
private readonly cards = new Map<string, Card>();