Engine events carry the nesting depth they were recorded at; the harness prints the log as an indented tree. New tests test_02_log_tree_shows_nesting and test_03_log_tree_death_phase; both SpellGrid CLIs print the tree of the last command's Sequences under a Log: line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
274 lines
7.7 KiB
Rust
274 lines
7.7 KiB
Rust
// Module 3: Deferred death. Each test matches a heading in scenarios.md.
|
|
use engine::harness::*;
|
|
|
|
fn fillers(n: usize) -> Vec<String> {
|
|
vec!["Test Filler".to_string(); n]
|
|
}
|
|
|
|
fn damaged(target: Id, amount: i32) -> GameEvent {
|
|
GameEvent::Damaged { target, amount }
|
|
}
|
|
|
|
fn triggered(source: Id, trigger: &str) -> GameEvent {
|
|
GameEvent::Triggered {
|
|
source,
|
|
trigger: trigger.to_string(),
|
|
}
|
|
}
|
|
|
|
fn whirlwind() -> GameEvent {
|
|
GameEvent::SpellCast {
|
|
card: "Whirlwind".to_string(),
|
|
}
|
|
}
|
|
|
|
/// 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 {
|
|
decks: Some([fillers(5), vec![]]),
|
|
});
|
|
let h = g.summon("Loot Hoarder", 0);
|
|
let s = g.summon("Armorsmith", 0);
|
|
g.clear_log();
|
|
|
|
g.cast("Whirlwind", 0, None);
|
|
|
|
let hand = g.hand(0);
|
|
assert_eq!(hand.len(), 1);
|
|
let c = hand[0];
|
|
assert_eq!(
|
|
g.log(),
|
|
vec![
|
|
whirlwind(),
|
|
damaged(h, 1),
|
|
damaged(s, 1),
|
|
triggered(s, "damaged"),
|
|
GameEvent::ArmorGained {
|
|
player: 0,
|
|
amount: 1
|
|
},
|
|
triggered(s, "damaged"),
|
|
GameEvent::ArmorGained {
|
|
player: 0,
|
|
amount: 1
|
|
},
|
|
GameEvent::Destroyed { id: h },
|
|
GameEvent::DeathPhase { round: 1 },
|
|
triggered(h, "deathrattle"),
|
|
GameEvent::Drew { player: 0, id: c },
|
|
]
|
|
);
|
|
assert_eq!(g.zone_of(h), Zone::Graveyard);
|
|
assert_eq!(g.board(0), vec![s]);
|
|
assert_eq!(g.armor(0), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_03_chained_death_phases() {
|
|
let mut g = new_game(GameOpts::default());
|
|
let golem = g.summon("Harvest Golem", 0);
|
|
let ghoul = g.summon("Unstable Ghoul", 0);
|
|
let r = g.summon("Test Reactor", 0);
|
|
g.deal_damage(golem, 2);
|
|
g.clear_log();
|
|
|
|
g.deal_damage(ghoul, 3);
|
|
|
|
let d = g.board(0)[0];
|
|
assert_eq!(
|
|
g.log(),
|
|
vec![
|
|
damaged(ghoul, 3),
|
|
GameEvent::Destroyed { id: ghoul },
|
|
GameEvent::DeathPhase { round: 1 },
|
|
triggered(ghoul, "deathrattle"),
|
|
damaged(golem, 1),
|
|
damaged(r, 1),
|
|
GameEvent::Destroyed { id: golem },
|
|
GameEvent::DeathPhase { round: 2 },
|
|
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(ghoul), Zone::Graveyard);
|
|
assert_eq!(g.zone_of(golem), Zone::Graveyard);
|
|
assert_eq!(g.health(r), 4);
|
|
}
|
|
|
|
#[test]
|
|
fn test_03_log_tree_death_phase() {
|
|
let mut g = new_game(GameOpts::default());
|
|
let golem = g.summon("Harvest Golem", 0);
|
|
let ghoul = g.summon("Unstable Ghoul", 0);
|
|
let r = g.summon("Test Reactor", 0);
|
|
g.deal_damage(golem, 2);
|
|
g.clear_log();
|
|
|
|
g.deal_damage(ghoul, 3);
|
|
|
|
let d = g.board(0)[0];
|
|
let expected = [
|
|
format!("Damaged{{target: {ghoul}, amount: 3}}"),
|
|
format!("Destroyed{{id: {ghoul}}}"),
|
|
"DeathPhase{round: 1}".to_string(),
|
|
format!("Triggered{{source: {ghoul}, trigger: \"deathrattle\"}}"),
|
|
format!(" Damaged{{target: {golem}, amount: 1}}"),
|
|
format!(" Damaged{{target: {r}, amount: 1}}"),
|
|
format!("Destroyed{{id: {golem}}}"),
|
|
"DeathPhase{round: 2}".to_string(),
|
|
format!("Triggered{{source: {golem}, trigger: \"deathrattle\"}}"),
|
|
format!(" Summoned{{id: {d}}}"),
|
|
];
|
|
assert_eq!(g.log_tree(), expected.join("\n") + "\n");
|
|
}
|
|
|
|
#[test]
|
|
fn test_03_simultaneous_removal() {
|
|
let mut g = new_game(GameOpts {
|
|
decks: Some([fillers(5), vec![]]),
|
|
});
|
|
let h1 = g.summon("Loot Hoarder", 0);
|
|
let h2 = g.summon("Loot Hoarder", 0);
|
|
g.clear_log();
|
|
|
|
g.cast("Whirlwind", 0, None);
|
|
|
|
let hand = g.hand(0);
|
|
assert_eq!(hand.len(), 2);
|
|
let (c1, c2) = (hand[0], hand[1]);
|
|
assert_eq!(
|
|
g.log(),
|
|
vec![
|
|
whirlwind(),
|
|
damaged(h1, 1),
|
|
damaged(h2, 1),
|
|
GameEvent::Destroyed { id: h1 },
|
|
GameEvent::Destroyed { id: h2 },
|
|
GameEvent::DeathPhase { round: 1 },
|
|
triggered(h1, "deathrattle"),
|
|
GameEvent::Drew { player: 0, id: c1 },
|
|
triggered(h2, "deathrattle"),
|
|
GameEvent::Drew { player: 0, id: c2 },
|
|
]
|
|
);
|
|
assert_eq!(g.zone_of(h1), Zone::Graveyard);
|
|
assert_eq!(g.zone_of(h2), Zone::Graveyard);
|
|
assert_eq!(g.board(0), Vec::<Id>::new());
|
|
}
|
|
|
|
#[test]
|
|
fn test_03_destroy_marks_pending() {
|
|
let mut g = new_game(GameOpts::default());
|
|
let golem = g.summon("Harvest Golem", 0);
|
|
g.clear_log();
|
|
|
|
g.destroy(golem);
|
|
|
|
let board = g.board(0);
|
|
assert_eq!(board.len(), 1);
|
|
let d = board[0];
|
|
assert_eq!(
|
|
g.log(),
|
|
vec![
|
|
GameEvent::Destroyed { id: golem },
|
|
GameEvent::DeathPhase { round: 1 },
|
|
triggered(golem, "deathrattle"),
|
|
GameEvent::Summoned { id: d },
|
|
]
|
|
);
|
|
assert_eq!(g.health(golem), 3);
|
|
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);
|
|
}
|