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:
Lucas WintherandClaude Fable 5.2 committed 2026-11-11 09:15:48 +01:00
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);
}