fix(reference): Death Events remember a right-hand neighbour, not a board index
A Deathrattle summon used the dying minion's board index from before the Death Creation Step removed anything. That index is stale when a minion to its left dies in the same step, so the token landed one place too far right. Each Death Event now records `before`: the nearest minion to the right that is not dying in the same step. The position handed to the effect context is resolved when the trigger resolves: the current index of `before`, or the right end. Applied to the Rust and TS references of Modules 3-10. Module 3 gains two tests, documented in scenarios.md: test_03_summon_position_after_simultaneous_deaths (fails before the fix) and test_03_simultaneous_summons_keep_board_order. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
1 parent
638549161f
commit
429137a6ef
79 files changed
+808
-236
No files matched your search
@@ -22,6 +22,16 @@ fn whirlwind() -> GameEvent {
|
||||
}
|
||||
}
|
||||
|
||||
/// The ids of the `Summoned` events in `log`, in order.
|
||||
fn summoned_ids(log: &[GameEvent]) -> Vec<Id> {
|
||||
log.iter()
|
||||
.filter_map(|e| match e {
|
||||
GameEvent::Summoned { id } => Some(*id),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_03_removed_only_after_sequence() {
|
||||
let mut g = new_game(GameOpts {
|
||||
@@ -155,3 +165,82 @@ fn test_03_destroy_marks_pending() {
|
||||
assert_eq!(g.zone_of(golem), Zone::Graveyard);
|
||||
assert_eq!(g.name_of(d), "Damaged Golem");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_03_summon_position_after_simultaneous_deaths() {
|
||||
let mut g = new_game(GameOpts {
|
||||
decks: Some([fillers(5), vec![]]),
|
||||
});
|
||||
let h = g.summon("Loot Hoarder", 0);
|
||||
let golem = g.summon("Harvest Golem", 0);
|
||||
let r = g.summon("Test Reactor", 0);
|
||||
g.deal_damage(golem, 2);
|
||||
g.clear_log();
|
||||
|
||||
g.cast("Whirlwind", 0, None);
|
||||
|
||||
let hand = g.hand(0);
|
||||
assert_eq!(hand.len(), 1);
|
||||
let c = hand[0];
|
||||
let summoned = summoned_ids(&g.log());
|
||||
assert_eq!(summoned.len(), 1);
|
||||
let d = summoned[0];
|
||||
assert_eq!(
|
||||
g.log(),
|
||||
vec![
|
||||
whirlwind(),
|
||||
damaged(h, 1),
|
||||
damaged(golem, 1),
|
||||
damaged(r, 1),
|
||||
GameEvent::Destroyed { id: h },
|
||||
GameEvent::Destroyed { id: golem },
|
||||
GameEvent::DeathPhase { round: 1 },
|
||||
triggered(h, "deathrattle"),
|
||||
GameEvent::Drew { player: 0, id: c },
|
||||
triggered(golem, "deathrattle"),
|
||||
GameEvent::Summoned { id: d },
|
||||
]
|
||||
);
|
||||
assert_eq!(g.name_of(d), "Damaged Golem");
|
||||
assert_eq!(g.board(0), vec![d, r]);
|
||||
assert_eq!(g.zone_of(h), Zone::Graveyard);
|
||||
assert_eq!(g.zone_of(golem), Zone::Graveyard);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_03_simultaneous_summons_keep_board_order() {
|
||||
let mut g = new_game(GameOpts::default());
|
||||
let a = g.summon("Harvest Golem", 0);
|
||||
let b = g.summon("Harvest Golem", 0);
|
||||
let r = g.summon("Test Reactor", 0);
|
||||
g.deal_damage(a, 2);
|
||||
g.deal_damage(b, 2);
|
||||
g.clear_log();
|
||||
|
||||
g.cast("Whirlwind", 0, None);
|
||||
|
||||
let summoned = summoned_ids(&g.log());
|
||||
assert_eq!(summoned.len(), 2);
|
||||
let (da, db) = (summoned[0], summoned[1]);
|
||||
assert_eq!(
|
||||
g.log(),
|
||||
vec![
|
||||
whirlwind(),
|
||||
damaged(a, 1),
|
||||
damaged(b, 1),
|
||||
damaged(r, 1),
|
||||
GameEvent::Destroyed { id: a },
|
||||
GameEvent::Destroyed { id: b },
|
||||
GameEvent::DeathPhase { round: 1 },
|
||||
triggered(a, "deathrattle"),
|
||||
GameEvent::Summoned { id: da },
|
||||
triggered(b, "deathrattle"),
|
||||
GameEvent::Summoned { id: db },
|
||||
]
|
||||
);
|
||||
assert_eq!(g.name_of(da), "Damaged Golem");
|
||||
assert_eq!(g.name_of(db), "Damaged Golem");
|
||||
assert_eq!(g.board(0), vec![da, db, r]);
|
||||
assert_eq!(g.zone_of(a), Zone::Graveyard);
|
||||
assert_eq!(g.zone_of(b), Zone::Graveyard);
|
||||
}
|
||||
@@ -111,3 +111,72 @@ test("test_03_destroy_marks_pending", () => {
|
||||
expect(g.zoneOf(golem)).toBe("graveyard");
|
||||
expect(g.nameOf(d)).toBe("Damaged Golem");
|
||||
});
|
||||
|
||||
test("test_03_summon_position_after_simultaneous_deaths", () => {
|
||||
const g = newGame({ decks: [fillers(5), []] });
|
||||
const h = g.summon("Loot Hoarder", 0);
|
||||
const golem = g.summon("Harvest Golem", 0);
|
||||
const r = g.summon("Test Reactor", 0);
|
||||
g.dealDamage(golem, 2);
|
||||
g.clearLog();
|
||||
|
||||
g.cast("Whirlwind", 0);
|
||||
|
||||
const hand = g.hand(0);
|
||||
expect(hand).toHaveLength(1);
|
||||
const c = hand[0];
|
||||
const summoned = g.log().flatMap((e) => (e.type === "Summoned" ? [e.id] : []));
|
||||
expect(summoned).toHaveLength(1);
|
||||
const d = summoned[0];
|
||||
expect(g.log()).toEqual([
|
||||
{ type: "SpellCast", card: "Whirlwind" },
|
||||
{ type: "Damaged", target: h, amount: 1 },
|
||||
{ type: "Damaged", target: golem, amount: 1 },
|
||||
{ type: "Damaged", target: r, amount: 1 },
|
||||
{ type: "Destroyed", id: h },
|
||||
{ type: "Destroyed", id: golem },
|
||||
{ type: "DeathPhase", round: 1 },
|
||||
{ type: "Triggered", source: h, trigger: "deathrattle" },
|
||||
{ type: "Drew", player: 0, id: c },
|
||||
{ type: "Triggered", source: golem, trigger: "deathrattle" },
|
||||
{ type: "Summoned", id: d },
|
||||
]);
|
||||
expect(g.nameOf(d)).toBe("Damaged Golem");
|
||||
expect(g.board(0)).toEqual([d, r]);
|
||||
expect(g.zoneOf(h)).toBe("graveyard");
|
||||
expect(g.zoneOf(golem)).toBe("graveyard");
|
||||
});
|
||||
|
||||
test("test_03_simultaneous_summons_keep_board_order", () => {
|
||||
const g = newGame();
|
||||
const a = g.summon("Harvest Golem", 0);
|
||||
const b = g.summon("Harvest Golem", 0);
|
||||
const r = g.summon("Test Reactor", 0);
|
||||
g.dealDamage(a, 2);
|
||||
g.dealDamage(b, 2);
|
||||
g.clearLog();
|
||||
|
||||
g.cast("Whirlwind", 0);
|
||||
|
||||
const summoned = g.log().flatMap((e) => (e.type === "Summoned" ? [e.id] : []));
|
||||
expect(summoned).toHaveLength(2);
|
||||
const [da, db] = summoned;
|
||||
expect(g.log()).toEqual([
|
||||
{ type: "SpellCast", card: "Whirlwind" },
|
||||
{ type: "Damaged", target: a, amount: 1 },
|
||||
{ type: "Damaged", target: b, amount: 1 },
|
||||
{ type: "Damaged", target: r, amount: 1 },
|
||||
{ type: "Destroyed", id: a },
|
||||
{ type: "Destroyed", id: b },
|
||||
{ type: "DeathPhase", round: 1 },
|
||||
{ type: "Triggered", source: a, trigger: "deathrattle" },
|
||||
{ type: "Summoned", id: da },
|
||||
{ type: "Triggered", source: b, trigger: "deathrattle" },
|
||||
{ type: "Summoned", id: db },
|
||||
]);
|
||||
expect(g.nameOf(da)).toBe("Damaged Golem");
|
||||
expect(g.nameOf(db)).toBe("Damaged Golem");
|
||||
expect(g.board(0)).toEqual([da, db, r]);
|
||||
expect(g.zoneOf(a)).toBe("graveyard");
|
||||
expect(g.zoneOf(b)).toBe("graveyard");
|
||||
});
|
||||
Reference in new issue
Block a user