[ { "name": "End of Phase: Aura Update before the Death Creation Step", "file": "src/engine/queue.ts", "find": "export const END_OF_PHASE: readonly EndOfPhaseStep[] = [\"deathCreation\", \"auraUpdate\"];", "replace": "export const END_OF_PHASE: readonly EndOfPhaseStep[] = [\"auraUpdate\", \"deathCreation\"];", "expect": [ "test_04_simultaneous_death_not_doubled", "test_04_aura_timing_order", "test_10_level_3_solution", "test_10_level_3_naive_would_differ" ] }, { "name": "Turn: no parse between movement and conversion", "file": "src/engine/puzzle/turn.ts", "find": "export const TURN: readonly Phase[] = [\"movement\", \"parse\", \"conversion\", \"parse\", \"hazards\", \"win\", \"parse\"];", "replace": "export const TURN: readonly Phase[] = [\"movement\", \"conversion\", \"parse\", \"hazards\", \"win\", \"parse\"];", "expect": [ "test_09_mid_turn_conversion", "test_10_level_5_solution" ] }, { "name": "Turn: win check before hazards", "file": "src/engine/puzzle/turn.ts", "find": "export const TURN: readonly Phase[] = [\"movement\", \"parse\", \"conversion\", \"parse\", \"hazards\", \"win\", \"parse\"];", "replace": "export const TURN: readonly Phase[] = [\"movement\", \"parse\", \"conversion\", \"parse\", \"win\", \"hazards\", \"parse\"];", "expect": [ "test_09_defeat_beats_win", "test_10_level_2_solution" ] }, { "name": "Turn: no parse between conversion and hazards", "file": "src/engine/puzzle/turn.ts", "find": "export const TURN: readonly Phase[] = [\"movement\", \"parse\", \"conversion\", \"parse\", \"hazards\", \"win\", \"parse\"];", "replace": "export const TURN: readonly Phase[] = [\"movement\", \"parse\", \"conversion\", \"hazards\", \"win\", \"parse\"];", "expect": [ "test_09_conversion_reparses_before_hazards" ] }, { "name": "Turn: hazards before conversion", "file": "src/engine/puzzle/turn.ts", "find": "export const TURN: readonly Phase[] = [\"movement\", \"parse\", \"conversion\", \"parse\", \"hazards\", \"win\", \"parse\"];", "replace": "export const TURN: readonly Phase[] = [\"movement\", \"parse\", \"hazards\", \"parse\", \"conversion\", \"win\", \"parse\"];", "expect": [ "test_09_conversion_before_hazards", "test_09_conversion_reparses_before_hazards", "test_09_undo_restores_everything" ] }, { "name": "Turn: no last parse", "file": "src/engine/puzzle/turn.ts", "find": "export const TURN: readonly Phase[] = [\"movement\", \"parse\", \"conversion\", \"parse\", \"hazards\", \"win\", \"parse\"];", "replace": "export const TURN: readonly Phase[] = [\"movement\", \"parse\", \"conversion\", \"parse\", \"hazards\", \"win\"];", "expect": [ "test_09_last_parse_matches_board" ] }, { "name": "Hazards in reverse order", "file": "src/content/hazards.ts", "find": "return [sink, hotMelt, defeat];", "replace": "return [defeat, hotMelt, sink];", "expect": [ "test_09_hazard_order_melt_before_defeat", "test_09_hazard_order_sink_before_melt", "test_10_level_6_solution", "test_11_condition_rechecked_between_hazards" ] }, { "name": "Auras read live from the board, no snapshot", "file": "src/engine/world.ts", "find": " activeAuras(): readonly ActiveAura[] {\n return this.auras;\n }", "replace": " activeAuras(): readonly ActiveAura[] {\n // MUTANT: every minion in play now, and the rule auras, read at each use.\n return [\n ...this.minionsInPlay().flatMap((id) => this.card(this.cardName(id)).auras.map((aura) => ({ owner: this.ownerOf(id), source: id, aura }))),\n ...this.rules.flatMap((aura) => ([0, 1] as const).map((owner) => ({ owner, aura }))),\n ];\n }", "expect": [ "test_05_own_aura_misses_own_battlecry", "test_10_level_3_naive_would_differ" ] }, { "name": "Hazards: DEFEAT before HOT/MELT", "file": "src/content/hazards.ts", "find": "return [sink, hotMelt, defeat];", "replace": "return [sink, defeat, hotMelt];", "expect": [ "test_09_hazard_order_melt_before_defeat", "test_10_level_6_solution" ] }, { "name": "Damage: Armor before redirect", "file": "src/engine/queue.ts", "find": "export const DAMAGE_STEPS: readonly DamageStep[] = [\"redirect\", \"armor\"];", "replace": "export const DAMAGE_STEPS: readonly DamageStep[] = [\"armor\", \"redirect\"];", "expect": [ "test_04_redirect_before_armor" ] }, { "name": "Conditions decided at parse time", "file": "src/engine/puzzle/level.ts", "find": " this.compiled = parse(this.board, this.lexicon);\n this.active = resolve(this.compiled);\n }", "replace": " this.compiled = parse(this.board, this.lexicon);\n this.active = resolve(this.compiled);\n // MUTANT: decide every condition now, for every object, and answer from that table until the next parse.\n const self = this as unknown as { live?: ConditionChecks; conditions: ConditionChecks };\n const live = (self.live ??= self.conditions);\n const decided = new Map();\n const key = (word: string, id: ObjectId, nouns: readonly string[]) => `${word} ${nouns.join(\" \")} ${id}`;\n for (const o of this.board.objects()) {\n for (const r of this.active) for (const c of r.conditions) decided.set(key(c.word, o.id, c.nouns), live[c.word](this, o.id, c.nouns));\n }\n self.conditions = Object.fromEntries(\n Object.keys(live).map((word) => [word, (_: Level, id: ObjectId, nouns: readonly string[]) => decided.get(key(word, id, nouns)) ?? false]),\n );\n }", "expect": [ "test_11_condition_rechecked_between_hazards" ] }, { "name": "Combat: no defender priority", "file": "src/engine/queue.ts", "find": " queue.sort((a, b) => b[4] - a[4]);\n", "replace": " // MUTANT: no sort; the Queue keeps the order it was filled in.\n", "expect": [ "test_12_defender_triggers_first" ] }, { "name": "SpellGrid: a summoned minion without a noun enters the grid", "file": "src/engine/spellgrid.ts", "find": " if (this.ids.has(event.id) || this.world.card(card).def.noun === undefined) continue;\n", "replace": " if (this.ids.has(event.id)) continue;\n", "expect": [ "test_10_summon_without_noun_stays_off_grid" ] } ]