The solver's position key reads stackAt (each tile oldest first). New tool tools/verify-levels.mjs checks course/10-spellgrid/levels/proofs.json: each level's shortest solution, and that it exhausts without its idea (neverRule) or on the naive engine. Levels 4-10, one course idea each, with test_10_level_N_solution in both tracks, scenarios and Lesson 10's list; l1 no longer matches l10, and proofs.json is not listed as a level. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
688 lines
18 KiB
Rust
688 lines
18 KiB
Rust
// Module 10: SpellGrid. Each test matches a heading in scenarios.md.
|
|
use engine::harness::*;
|
|
|
|
/// A level from its JSON text (the course's level format).
|
|
fn level_json(json: &str) -> LevelDef {
|
|
serde_json::from_str(json).expect("a level in the course's level format")
|
|
}
|
|
|
|
/// A level `width` wide and `height` high: text written as `(word, x, y)`, other
|
|
/// objects as `(kind, x, y)`, minion cards as `(card, x, y)`, and a hand.
|
|
fn level(
|
|
width: i32,
|
|
height: i32,
|
|
words: &[(&str, i32, i32)],
|
|
objects: &[(&str, i32, i32)],
|
|
cards: &[(&str, i32, i32)],
|
|
hand: &[&str],
|
|
) -> LevelDef {
|
|
let words = words.iter().map(|&(w, x, y)| (format!("text:{w}"), x, y));
|
|
let objects = objects.iter().map(|&(k, x, y)| (k.to_string(), x, y));
|
|
LevelDef {
|
|
width,
|
|
height,
|
|
objects: words
|
|
.chain(objects)
|
|
.map(|(kind, x, y)| LevelObject { kind, x, y })
|
|
.collect(),
|
|
cards: cards
|
|
.iter()
|
|
.map(|&(card, x, y)| LevelCard {
|
|
card: card.to_string(),
|
|
x,
|
|
y,
|
|
})
|
|
.collect(),
|
|
hand: hand.iter().map(|c| c.to_string()).collect(),
|
|
}
|
|
}
|
|
|
|
/// The minion on tile (x, y); fails the test if there is none.
|
|
fn minion(g: &SpellGrid, x: i32, y: i32) -> Id {
|
|
g.minion_at(x, y)
|
|
.unwrap_or_else(|| panic!("no minion at ({x}, {y})"))
|
|
}
|
|
|
|
/// How many times `source`'s Deathrattle triggered in the log.
|
|
fn deathrattles(g: &SpellGrid, source: Id) -> usize {
|
|
g.log()
|
|
.iter()
|
|
.filter(|e| {
|
|
matches!(e, GameEvent::Triggered { source: s, trigger } if *s == source && trigger == "deathrattle")
|
|
})
|
|
.count()
|
|
}
|
|
|
|
fn has(rules: &[String], rule: &str) -> bool {
|
|
rules.iter().any(|r| r == rule)
|
|
}
|
|
|
|
/// Play one turn per direction, in order.
|
|
fn steps(g: &mut SpellGrid, dirs: &[Dir]) {
|
|
for &dir in dirs {
|
|
g.step(dir);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_10_spatial_rule_feeds_interceptor() {
|
|
let mut g = new_spell_grid(
|
|
level(
|
|
6,
|
|
3,
|
|
&[("DEATHRATTLE", 0, 0), ("IS", 1, 0)],
|
|
&[],
|
|
&[("Powder Keg", 0, 2), ("Bramble Wall", 2, 2)],
|
|
&["Spark"],
|
|
),
|
|
vec![],
|
|
);
|
|
let keg = minion(&g, 0, 2);
|
|
let bramble = minion(&g, 2, 2);
|
|
|
|
assert!(!has(&g.active_rules(), "DEATHRATTLE IS DOUBLE"));
|
|
|
|
g.place("text:DOUBLE", 2, 0);
|
|
|
|
assert!(has(&g.active_rules(), "DEATHRATTLE IS DOUBLE"));
|
|
|
|
g.clear_log();
|
|
g.cast("Spark", Some(keg));
|
|
|
|
assert_eq!(deathrattles(&g, keg), 2);
|
|
assert_eq!(g.health(bramble), 1);
|
|
assert_eq!(g.pos_of(keg), None);
|
|
assert_eq!(g.pos_of(bramble), Some((2, 2)));
|
|
assert_eq!(g.hand(), Vec::<String>::new());
|
|
}
|
|
|
|
#[test]
|
|
fn test_10_not_cancels_spatial_modifier() {
|
|
let mut g = new_spell_grid(
|
|
level(
|
|
6,
|
|
4,
|
|
&[
|
|
("DEATHRATTLE", 0, 0),
|
|
("IS", 1, 0),
|
|
("DOUBLE", 2, 0),
|
|
("DEATHRATTLE", 0, 1),
|
|
("IS", 1, 1),
|
|
("NOT", 2, 1),
|
|
("DOUBLE", 3, 1),
|
|
],
|
|
&[],
|
|
&[
|
|
("Grave Warden", 0, 3),
|
|
("Powder Keg", 1, 3),
|
|
("Bramble Wall", 2, 3),
|
|
],
|
|
&["Spark"],
|
|
),
|
|
vec![],
|
|
);
|
|
let warden = minion(&g, 0, 3);
|
|
let keg = minion(&g, 1, 3);
|
|
let bramble = minion(&g, 2, 3);
|
|
|
|
assert!(has(&g.rules(), "DEATHRATTLE IS DOUBLE"));
|
|
assert!(has(&g.rules(), "DEATHRATTLE IS NOT DOUBLE"));
|
|
assert!(has(&g.active_rules(), "DEATHRATTLE IS NOT DOUBLE"));
|
|
assert!(!has(&g.active_rules(), "DEATHRATTLE IS DOUBLE"));
|
|
|
|
g.clear_log();
|
|
g.cast("Spark", Some(keg));
|
|
|
|
assert_eq!(deathrattles(&g, keg), 1);
|
|
assert_eq!(g.health(bramble), 2);
|
|
assert_eq!(g.pos_of(warden), None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_10_grid_and_card_doublers_take_max() {
|
|
let mut g = new_spell_grid(
|
|
level(
|
|
6,
|
|
3,
|
|
&[("DEATHRATTLE", 0, 0), ("IS", 1, 0), ("DOUBLE", 2, 0)],
|
|
&[],
|
|
&[
|
|
("Grave Warden", 0, 2),
|
|
("Powder Keg", 1, 2),
|
|
("Bramble Wall", 2, 2),
|
|
],
|
|
&["Spark"],
|
|
),
|
|
vec![],
|
|
);
|
|
let keg = minion(&g, 1, 2);
|
|
let bramble = minion(&g, 2, 2);
|
|
|
|
g.clear_log();
|
|
g.cast("Spark", Some(keg));
|
|
|
|
assert_eq!(deathrattles(&g, keg), 2);
|
|
assert_eq!(g.health(bramble), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_10_hazard_feeds_death_step() {
|
|
let mut g = new_spell_grid(
|
|
level(
|
|
5,
|
|
4,
|
|
&[
|
|
("KEG", 0, 0),
|
|
("IS", 1, 0),
|
|
("YOU", 2, 0),
|
|
("AND", 3, 0),
|
|
("MELT", 4, 0),
|
|
("LAVA", 0, 1),
|
|
("IS", 1, 1),
|
|
("HOT", 2, 1),
|
|
],
|
|
&[("lava", 2, 3)],
|
|
&[("Powder Keg", 1, 3), ("Bramble Wall", 4, 3)],
|
|
&[],
|
|
),
|
|
vec![],
|
|
);
|
|
let keg = minion(&g, 1, 3);
|
|
let bramble = minion(&g, 4, 3);
|
|
|
|
g.clear_log();
|
|
g.step(Dir::Right);
|
|
|
|
assert_eq!(
|
|
g.log(),
|
|
vec![
|
|
GameEvent::Destroyed { id: keg },
|
|
GameEvent::DeathPhase { round: 1 },
|
|
GameEvent::Triggered {
|
|
source: keg,
|
|
trigger: "deathrattle".to_string()
|
|
},
|
|
GameEvent::Damaged {
|
|
target: bramble,
|
|
amount: 1
|
|
},
|
|
]
|
|
);
|
|
assert_eq!(g.pos_of(keg), None);
|
|
assert_eq!(g.kinds_at(2, 3), vec!["lava"]);
|
|
assert_eq!(g.health(bramble), 2);
|
|
assert!(g.lost());
|
|
}
|
|
|
|
#[test]
|
|
fn test_10_spawned_text_triggers_reparse() {
|
|
let mut g = new_spell_grid(
|
|
level(
|
|
5,
|
|
3,
|
|
&[
|
|
("BABA", 0, 0),
|
|
("IS", 1, 0),
|
|
("YOU", 2, 0),
|
|
("BABA", 0, 1),
|
|
("IS", 1, 1),
|
|
],
|
|
&[],
|
|
&[("Rune Scribe", 2, 1)],
|
|
&["Spark"],
|
|
),
|
|
vec![],
|
|
);
|
|
let scribe = minion(&g, 2, 1);
|
|
let baba = g.place("baba", 4, 2);
|
|
|
|
assert!(!has(&g.rules(), "BABA IS WIN"));
|
|
|
|
g.cast("Spark", Some(scribe));
|
|
|
|
assert_eq!(g.kinds_at(2, 1), vec!["text:WIN"]);
|
|
assert!(has(&g.rules(), "BABA IS WIN"));
|
|
assert!(g.has_property(baba, "WIN"));
|
|
assert!(!g.won());
|
|
|
|
g.step(Dir::Left);
|
|
|
|
assert!(g.won());
|
|
}
|
|
|
|
#[test]
|
|
fn test_10_targeted_spell_needs_target() {
|
|
let mut g = new_spell_grid(
|
|
level(3, 1, &[], &[], &[("Bramble Wall", 0, 0)], &["Spark"]),
|
|
vec![],
|
|
);
|
|
let bramble = minion(&g, 0, 0);
|
|
|
|
let cast = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| g.cast("Spark", None)));
|
|
assert!(cast.is_err());
|
|
|
|
assert_eq!(g.hand(), vec!["Spark"]);
|
|
assert_eq!(g.health(bramble), 3);
|
|
|
|
g.cast("Spark", Some(bramble));
|
|
|
|
assert_eq!(g.health(bramble), 2);
|
|
assert_eq!(g.hand(), Vec::<String>::new());
|
|
}
|
|
|
|
/// The events of a Spark that kills `minion`, whose Deathrattle summons `summoned`.
|
|
fn spark_kills_summoner(minion: Id, summoned: Id) -> Vec<GameEvent> {
|
|
vec![
|
|
GameEvent::SpellCast {
|
|
card: "Spark".to_string(),
|
|
},
|
|
GameEvent::Damaged {
|
|
target: minion,
|
|
amount: 1,
|
|
},
|
|
GameEvent::Destroyed { id: minion },
|
|
GameEvent::DeathPhase { round: 1 },
|
|
GameEvent::Triggered {
|
|
source: minion,
|
|
trigger: "deathrattle".to_string(),
|
|
},
|
|
GameEvent::Summoned { id: summoned },
|
|
]
|
|
}
|
|
|
|
#[test]
|
|
fn test_10_summoned_minion_enters_grid() {
|
|
let mut g = new_spell_grid(
|
|
level(
|
|
5,
|
|
3,
|
|
&[("SHARD", 0, 0), ("IS", 1, 0), ("PUSH", 2, 0)],
|
|
&[],
|
|
&[("Clay Golem", 2, 2)],
|
|
&["Spark", "Spark"],
|
|
),
|
|
vec![],
|
|
);
|
|
let golem = minion(&g, 2, 2);
|
|
g.cast("Spark", Some(golem));
|
|
|
|
g.clear_log();
|
|
g.cast("Spark", Some(golem));
|
|
|
|
let shard = minion(&g, 2, 2);
|
|
assert_eq!(g.log(), spark_kills_summoner(golem, shard));
|
|
assert_eq!(g.kinds_at(2, 2), vec!["shard"]);
|
|
assert_eq!(g.pos_of(golem), None);
|
|
assert_eq!(g.name_of(shard), "Clay Shard");
|
|
assert_eq!(g.health(shard), 1);
|
|
assert!(has(&g.active_rules(), "SHARD IS PUSH"));
|
|
assert!(g.has_property(shard, "PUSH"));
|
|
for older in [golem, g.hero(0), g.hero(1)] {
|
|
assert!(shard > older);
|
|
}
|
|
assert_eq!(g.place("rock", 4, 2), shard + 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_10_summon_without_noun_stays_off_grid() {
|
|
let mut g = new_spell_grid(
|
|
level(3, 1, &[], &[], &[], &["Graft", "Spark", "Spark"]),
|
|
vec![],
|
|
);
|
|
g.set_chooser(|kind, _| {
|
|
match kind {
|
|
"base" => "Clay Golem",
|
|
"donor" => "Harvest Golem",
|
|
_ => "bottom",
|
|
}
|
|
.to_string()
|
|
});
|
|
g.cast("Graft", None);
|
|
|
|
assert_eq!(g.hand(), vec!["Spark", "Spark", "Clay Golem (Assembled)"]);
|
|
|
|
let golem = g.play("Clay Golem (Assembled)", 1, 0);
|
|
g.cast("Spark", Some(golem));
|
|
g.clear_log();
|
|
g.cast("Spark", Some(golem));
|
|
|
|
let Some(&GameEvent::Summoned { id: token }) = g.log().last() else {
|
|
panic!("the last event is not a Summoned: {:?}", g.log());
|
|
};
|
|
assert_eq!(g.log(), spark_kills_summoner(golem, token));
|
|
assert_eq!(g.name_of(token), "Damaged Golem");
|
|
assert_eq!(g.health(token), 1);
|
|
assert_eq!(g.pos_of(token), None);
|
|
assert_eq!(g.kinds_at(1, 0), Vec::<String>::new());
|
|
assert_eq!(g.minion_at(1, 0), None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_10_level_1_solution() {
|
|
let mut g = new_spell_grid(
|
|
level_json(include_str!("../levels/l1-double-tap.json")),
|
|
vec![],
|
|
);
|
|
let keg = minion(&g, 0, 4);
|
|
let bramble = minion(&g, 5, 6);
|
|
|
|
g.step(Dir::Up);
|
|
g.step(Dir::Up);
|
|
|
|
assert!(has(&g.active_rules(), "DEATHRATTLE IS DOUBLE"));
|
|
|
|
g.cast("Whirlwind", None);
|
|
|
|
assert_eq!(deathrattles(&g, keg), 2);
|
|
assert_eq!(g.pos_of(bramble), None);
|
|
|
|
for dir in [
|
|
Dir::Down,
|
|
Dir::Down,
|
|
Dir::Left,
|
|
Dir::Down,
|
|
Dir::Down,
|
|
Dir::Right,
|
|
] {
|
|
g.step(dir);
|
|
}
|
|
|
|
assert!(!g.won());
|
|
assert_eq!(g.kinds_at(5, 6), vec!["baba"]);
|
|
|
|
g.step(Dir::Right);
|
|
|
|
assert!(g.won());
|
|
}
|
|
|
|
#[test]
|
|
fn test_10_level_2_solution() {
|
|
let mut g = new_spell_grid(
|
|
level_json(include_str!("../levels/l2-spliced-corpse.json")),
|
|
vec![],
|
|
);
|
|
g.set_chooser(|kind, _| {
|
|
match kind {
|
|
"base" => "Ember Moth",
|
|
"donor" => "Rune Scribe",
|
|
_ => "bottom",
|
|
}
|
|
.to_string()
|
|
});
|
|
|
|
g.cast("Graft", None);
|
|
|
|
assert_eq!(g.hand(), vec!["Ember Moth (Assembled)"]);
|
|
|
|
let moth = g.play("Ember Moth (Assembled)", 4, 3);
|
|
|
|
assert_eq!(g.kinds_at(4, 3), vec!["moth"]);
|
|
assert_eq!(g.name_of(moth), "Ember Moth (Assembled)");
|
|
|
|
g.step(Dir::Left);
|
|
g.step(Dir::Left);
|
|
|
|
assert_eq!(g.pos_of(moth), None);
|
|
assert_eq!(g.kinds_at(2, 3), vec!["lava", "text:WIN"]);
|
|
assert!(has(&g.rules(), "BABA IS WIN"));
|
|
assert!(g.won());
|
|
}
|
|
|
|
/// Level 3's first input, one Whirlwind, after clearing the log. Returns the
|
|
/// knight, keg, warden and hedge.
|
|
fn cast_level_3_whirlwind(g: &mut SpellGrid) -> (Id, Id, Id, Id) {
|
|
let ids = (
|
|
minion(g, 0, 4),
|
|
minion(g, 1, 5),
|
|
minion(g, 2, 5),
|
|
minion(g, 4, 4),
|
|
);
|
|
g.clear_log();
|
|
g.cast("Whirlwind", None);
|
|
ids
|
|
}
|
|
|
|
#[test]
|
|
fn test_10_level_3_solution() {
|
|
let mut g = new_spell_grid(
|
|
level_json(include_str!("../levels/l3-simultaneous-paradox.json")),
|
|
vec![],
|
|
);
|
|
let (knight, keg, warden, hedge) = cast_level_3_whirlwind(&mut g);
|
|
|
|
assert_eq!(deathrattles(&g, keg), 1);
|
|
assert_eq!(g.health(knight), 1);
|
|
assert_eq!(g.pos_of(hedge), None);
|
|
assert_eq!(g.pos_of(keg), None);
|
|
assert_eq!(g.pos_of(warden), None);
|
|
|
|
for _ in 0..5 {
|
|
g.step(Dir::Right);
|
|
}
|
|
|
|
assert!(g.won());
|
|
}
|
|
|
|
#[test]
|
|
fn test_10_level_3_naive_would_differ() {
|
|
let mut g = new_spell_grid(
|
|
level_json(include_str!("../levels/l3-simultaneous-paradox.json")),
|
|
vec![],
|
|
);
|
|
g.with_naive_aura_timing();
|
|
let (knight, keg, _, _) = cast_level_3_whirlwind(&mut g);
|
|
for _ in 0..5 {
|
|
g.step(Dir::Right);
|
|
}
|
|
|
|
assert_eq!(deathrattles(&g, keg), 2);
|
|
assert_eq!(g.pos_of(knight), None);
|
|
assert!(g.lost());
|
|
assert!(!g.won());
|
|
}
|
|
|
|
#[test]
|
|
fn test_10_level_4_solution() {
|
|
let mut g = new_spell_grid(
|
|
level_json(include_str!("../levels/l4-crossed-out.json")),
|
|
vec![],
|
|
);
|
|
let keg = minion(&g, 1, 6);
|
|
let hedge = minion(&g, 6, 4);
|
|
|
|
assert!(has(&g.rules(), "DEATHRATTLE IS DOUBLE"));
|
|
assert!(!has(&g.active_rules(), "DEATHRATTLE IS DOUBLE"));
|
|
|
|
steps(&mut g, &[Dir::Right, Dir::Down]);
|
|
|
|
assert!(!has(&g.rules(), "DEATHRATTLE IS NOT DOUBLE"));
|
|
assert!(has(&g.active_rules(), "DEATHRATTLE IS DOUBLE"));
|
|
|
|
steps(&mut g, &[Dir::Right, Dir::Right]);
|
|
g.cast("Spark", Some(keg));
|
|
|
|
assert_eq!(deathrattles(&g, keg), 2);
|
|
assert_eq!(g.pos_of(hedge), None);
|
|
|
|
steps(&mut g, &[Dir::Right, Dir::Up]);
|
|
|
|
assert!(g.won());
|
|
}
|
|
|
|
#[test]
|
|
fn test_10_level_5_solution() {
|
|
let mut g = new_spell_grid(
|
|
level_json(include_str!("../levels/l5-sealed-keg.json")),
|
|
vec![],
|
|
);
|
|
let keg = minion(&g, 1, 5);
|
|
|
|
steps(&mut g, &[Dir::Left, Dir::Left, Dir::Down, Dir::Left]);
|
|
|
|
assert_eq!(g.kinds_at(1, 5), vec!["keg"]);
|
|
|
|
g.step(Dir::Up);
|
|
|
|
assert!(has(&g.active_rules(), "KEG IS NOT KEG"));
|
|
assert!(!has(&g.active_rules(), "KEG IS KEG"));
|
|
assert_eq!(deathrattles(&g, keg), 1);
|
|
assert_eq!(g.pos_of(keg), None);
|
|
assert_eq!(g.kinds_at(1, 5), vec!["flag"]);
|
|
|
|
g.step(Dir::Left);
|
|
|
|
assert!(g.won());
|
|
}
|
|
|
|
#[test]
|
|
fn test_10_level_6_solution() {
|
|
let mut g = new_spell_grid(
|
|
level_json(include_str!("../levels/l6-melt-before-defeat.json")),
|
|
vec![],
|
|
);
|
|
let knight = minion(&g, 0, 6);
|
|
let keg = minion(&g, 3, 6);
|
|
|
|
assert_eq!(g.stack_at(3, 6), vec!["keg", "shard"]);
|
|
|
|
steps(&mut g, &[Dir::Right, Dir::Right, Dir::Up, Dir::Down]);
|
|
|
|
assert!(has(&g.active_rules(), "LAVA IS HOT"));
|
|
|
|
g.step(Dir::Right);
|
|
|
|
// The knight stepped onto the DEFEAT shard as the keg went into the lava. HOT/MELT runs
|
|
// first: the keg's Deathrattle kills the shard, and DEFEAT finds nothing on the knight's tile.
|
|
assert_eq!(deathrattles(&g, keg), 1);
|
|
assert_eq!(g.pos_of(keg), None);
|
|
assert_eq!(g.pos_of(knight), Some((3, 6)));
|
|
assert_eq!(g.kinds_at(3, 6), vec!["knight"]);
|
|
assert_eq!(g.health(knight), 2);
|
|
assert!(!g.lost());
|
|
|
|
steps(&mut g, &[Dir::Right, Dir::Right]);
|
|
|
|
assert!(g.won());
|
|
}
|
|
|
|
#[test]
|
|
fn test_10_level_7_solution() {
|
|
let mut g = new_spell_grid(
|
|
level_json(include_str!("../levels/l7-clay-key.json")),
|
|
vec![],
|
|
);
|
|
let golem = minion(&g, 4, 5);
|
|
|
|
steps(&mut g, &[Dir::Right, Dir::Up, Dir::Right]);
|
|
|
|
assert!(has(&g.active_rules(), "SHARD IS OPEN"));
|
|
|
|
g.cast("Spark", Some(golem));
|
|
g.cast("Spark", Some(golem));
|
|
|
|
assert_eq!(g.pos_of(golem), None);
|
|
assert_eq!(g.kinds_at(4, 5), vec!["shard"]);
|
|
assert_eq!(g.name_of(minion(&g, 4, 5)), "Clay Shard");
|
|
|
|
steps(&mut g, &[Dir::Right, Dir::Right]);
|
|
|
|
assert_eq!(g.kinds_at(6, 5), Vec::<String>::new());
|
|
|
|
steps(&mut g, &[Dir::Right, Dir::Up]);
|
|
|
|
assert!(g.won());
|
|
}
|
|
|
|
#[test]
|
|
fn test_10_level_8_solution() {
|
|
let mut g = new_spell_grid(
|
|
level_json(include_str!("../levels/l8-nothing-dies.json")),
|
|
vec![],
|
|
);
|
|
g.set_chooser(|kind, _| {
|
|
match kind {
|
|
"base" => "Clay Golem",
|
|
"donor" => "Novice Engineer",
|
|
_ => "top",
|
|
}
|
|
.to_string()
|
|
});
|
|
|
|
g.step(Dir::Left);
|
|
|
|
assert!(has(&g.active_rules(), "SHARD IS WIN"));
|
|
|
|
g.cast("Graft", None);
|
|
g.clear_log();
|
|
let golem = g.play("Clay Golem (Assembled)", 4, 3);
|
|
|
|
// Battlecry, not Deathrattle: the golem is alive and its shard already stands on its tile.
|
|
assert!(g.log().contains(&GameEvent::Triggered {
|
|
source: golem,
|
|
trigger: "battlecry".to_string(),
|
|
}));
|
|
assert_eq!(g.pos_of(golem), Some((4, 3)));
|
|
assert_eq!(g.stack_at(4, 3), vec!["golem", "shard"]);
|
|
|
|
g.step(Dir::Right);
|
|
|
|
assert!(g.won());
|
|
}
|
|
|
|
#[test]
|
|
fn test_10_level_9_solution() {
|
|
let mut g = new_spell_grid(
|
|
level_json(include_str!("../levels/l9-skull-river.json")),
|
|
vec![],
|
|
);
|
|
|
|
g.step(Dir::Right);
|
|
|
|
assert_eq!(g.kinds_at(4, 5), vec!["rock", "skull"]);
|
|
assert_eq!(g.kinds_at(3, 5), vec!["rock"]);
|
|
|
|
steps(&mut g, &[Dir::Down, Dir::Left]);
|
|
|
|
// SKULL IS SINK: at the end of the turn the skull under the rock takes the rock with it.
|
|
assert!(has(&g.active_rules(), "SKULL IS SINK"));
|
|
assert_eq!(g.kinds_at(4, 5), Vec::<String>::new());
|
|
|
|
steps(&mut g, &[Dir::Up, Dir::Right, Dir::Right, Dir::Right]);
|
|
|
|
assert_eq!(g.kinds_at(5, 5), Vec::<String>::new());
|
|
|
|
steps(&mut g, &[Dir::Right, Dir::Right]);
|
|
|
|
assert!(g.won());
|
|
}
|
|
|
|
#[test]
|
|
fn test_10_level_10_solution() {
|
|
let mut g = new_spell_grid(
|
|
level_json(include_str!("../levels/l10-friendly-fire.json")),
|
|
vec![],
|
|
);
|
|
let shard = minion(&g, 1, 6);
|
|
let knight = minion(&g, 5, 5);
|
|
let hedge = minion(&g, 6, 4);
|
|
|
|
steps(&mut g, &[Dir::Left, Dir::Up]);
|
|
|
|
assert!(has(&g.active_rules(), "KNIGHT IS YOU"));
|
|
assert!(!has(&g.active_rules(), "SHARD IS YOU"));
|
|
|
|
g.step(Dir::Up);
|
|
g.cast("Whirlwind", None);
|
|
|
|
// Whirlwind and the keg's Deathrattle: 2 damage to everything. The shard you were dies;
|
|
// the knight you are now is left on 1.
|
|
assert_eq!(g.pos_of(shard), None);
|
|
assert_eq!(g.pos_of(hedge), None);
|
|
assert_eq!(g.health(knight), 1);
|
|
assert!(!g.lost());
|
|
|
|
steps(&mut g, &[Dir::Right, Dir::Up]);
|
|
|
|
assert!(g.won());
|
|
}
|