Baba.undo() restores the level before the last step or move (board snapshot: objects in age order with ids, the id counter, and won; rules recompiled). place clears the history. SpellGrid's undo throws/panics 'not supported'. stackAt / stack_at lists a tile's kinds oldest first, on Baba and SpellGrid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
414 lines
13 KiB
Rust
414 lines
13 KiB
Rust
// Module 9: The turn pipeline. Each test matches a heading in scenarios.md.
|
|
use engine::harness::*;
|
|
|
|
/// An empty level `width` wide and `height` high.
|
|
fn empty_level(width: i32, height: i32) -> LevelDef {
|
|
LevelDef {
|
|
width,
|
|
height,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Place one text object per word: `(word, x, y)`.
|
|
fn write(b: &mut Baba, words: &[(&str, i32, i32)]) {
|
|
for &(word, x, y) in words {
|
|
b.place(&format!("text:{word}"), x, y);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_09_lava_saved_by_reparse() {
|
|
let mut b = new_baba(empty_level(6, 5));
|
|
write(&mut b, &[("BABA", 0, 0), ("IS", 1, 0), ("YOU", 2, 0)]);
|
|
write(&mut b, &[("LAVA", 0, 1), ("IS", 1, 1), ("HOT", 2, 1)]);
|
|
write(&mut b, &[("BABA", 4, 2), ("IS", 4, 3)]);
|
|
let melt_text = b.place("text:MELT", 4, 4);
|
|
let lava = b.place("lava", 4, 4);
|
|
let baba = b.place("baba", 3, 4);
|
|
|
|
assert!(b.has_property(baba, "MELT"));
|
|
|
|
b.step(Dir::Right);
|
|
|
|
assert_eq!(b.pos_of(baba), Some((4, 4)));
|
|
assert_eq!(b.pos_of(melt_text), Some((5, 4)));
|
|
// Inferred from the order of operations, not stated by a source: Baba survives.
|
|
assert_eq!(b.pos_of(lava), Some((4, 4)));
|
|
assert_eq!(b.kinds_at(4, 4), vec!["baba", "lava"]);
|
|
assert!(!b.rules().contains(&"BABA IS MELT".to_string()));
|
|
assert!(!b.lost());
|
|
}
|
|
|
|
#[test]
|
|
fn test_09_mid_turn_conversion() {
|
|
let mut b = new_baba(empty_level(5, 5));
|
|
write(&mut b, &[("BABA", 0, 0), ("IS", 1, 0), ("YOU", 2, 0)]);
|
|
write(&mut b, &[("BABA", 0, 2), ("IS", 1, 2)]);
|
|
let rock_text = b.place("text:ROCK", 2, 3);
|
|
let pusher = b.place("baba", 2, 4);
|
|
let other = b.place("baba", 4, 4);
|
|
|
|
b.step(Dir::Up);
|
|
|
|
assert_eq!(b.pos_of(rock_text), Some((2, 2)));
|
|
assert!(b.rules().contains(&"BABA IS ROCK".to_string()));
|
|
assert_eq!(b.pos_of(pusher), Some((2, 3)));
|
|
assert_eq!(b.pos_of(other), Some((4, 3)));
|
|
assert_eq!(b.kinds_at(2, 3), vec!["rock"]);
|
|
assert_eq!(b.kinds_at(4, 3), vec!["rock"]);
|
|
assert_eq!(b.kinds_at(2, 4), Vec::<String>::new());
|
|
assert_eq!(b.kinds_at(4, 4), Vec::<String>::new());
|
|
}
|
|
|
|
#[test]
|
|
fn test_09_identity_lock_blocks_conversion() {
|
|
let mut b = new_baba(empty_level(5, 5));
|
|
write(&mut b, &[("BABA", 0, 0), ("IS", 1, 0), ("YOU", 2, 0)]);
|
|
write(&mut b, &[("BABA", 0, 1), ("IS", 1, 1), ("BABA", 2, 1)]);
|
|
write(&mut b, &[("BABA", 0, 2), ("IS", 1, 2), ("ROCK", 2, 2)]);
|
|
write(&mut b, &[("KEY", 0, 3), ("IS", 1, 3), ("ROCK", 2, 3)]);
|
|
let baba = b.place("baba", 0, 4);
|
|
let key = b.place("key", 3, 4);
|
|
|
|
b.step(Dir::Right);
|
|
|
|
assert_eq!(b.pos_of(baba), Some((1, 4)));
|
|
assert_eq!(b.kinds_at(1, 4), vec!["baba"]);
|
|
assert_eq!(b.kinds_at(3, 4), vec!["rock"]);
|
|
assert_eq!(b.pos_of(key), Some((3, 4)));
|
|
let active = b.active_rules();
|
|
assert!(!active.contains(&"BABA IS ROCK".to_string()));
|
|
assert!(active.contains(&"BABA IS BABA".to_string()));
|
|
assert!(active.contains(&"KEY IS ROCK".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_09_self_negation_deletes() {
|
|
let mut b = new_baba(empty_level(5, 5));
|
|
write(&mut b, &[("BABA", 0, 0), ("IS", 1, 0), ("YOU", 2, 0)]);
|
|
write(
|
|
&mut b,
|
|
&[("KEY", 0, 1), ("IS", 1, 1), ("NOT", 2, 1), ("KEY", 3, 1)],
|
|
);
|
|
write(&mut b, &[("KEY", 0, 2), ("IS", 1, 2), ("ROCK", 2, 2)]);
|
|
let baba = b.place("baba", 0, 4);
|
|
let key = b.place("key", 3, 4);
|
|
|
|
assert_eq!(b.pos_of(key), Some((3, 4)));
|
|
|
|
b.step(Dir::Right);
|
|
|
|
assert_eq!(b.pos_of(key), None);
|
|
assert_eq!(b.kinds_at(3, 4), vec!["rock"]);
|
|
assert_eq!(b.pos_of(baba), Some((1, 4)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_09_float_separates_hazards() {
|
|
let mut b = new_baba(empty_level(4, 6));
|
|
write(&mut b, &[("BABA", 0, 0), ("IS", 1, 0), ("YOU", 2, 0)]);
|
|
write(&mut b, &[("KEY", 0, 1), ("IS", 1, 1), ("YOU", 2, 1)]);
|
|
write(&mut b, &[("SKULL", 0, 2), ("IS", 1, 2), ("DEFEAT", 2, 2)]);
|
|
write(&mut b, &[("BABA", 0, 3), ("IS", 1, 3), ("FLOAT", 2, 3)]);
|
|
let baba = b.place("baba", 0, 4);
|
|
b.place("skull", 1, 4);
|
|
let key = b.place("key", 0, 5);
|
|
b.place("skull", 1, 5);
|
|
|
|
b.step(Dir::Right);
|
|
|
|
assert_eq!(b.pos_of(baba), Some((1, 4)));
|
|
assert_eq!(b.kinds_at(1, 4), vec!["baba", "skull"]);
|
|
assert_eq!(b.pos_of(key), None);
|
|
assert_eq!(b.kinds_at(1, 5), vec!["skull"]);
|
|
assert!(!b.lost());
|
|
}
|
|
|
|
#[test]
|
|
fn test_09_sink_destroys_both() {
|
|
let mut b = new_baba(empty_level(5, 5));
|
|
write(&mut b, &[("BABA", 0, 0), ("IS", 1, 0), ("YOU", 2, 0)]);
|
|
write(&mut b, &[("ROCK", 0, 1), ("IS", 1, 1), ("PUSH", 2, 1)]);
|
|
write(&mut b, &[("WATER", 0, 2), ("IS", 1, 2), ("SINK", 2, 2)]);
|
|
let baba = b.place("baba", 0, 4);
|
|
let rock = b.place("rock", 1, 4);
|
|
let water = b.place("water", 2, 4);
|
|
|
|
b.step(Dir::Right);
|
|
|
|
assert_eq!(b.pos_of(rock), None);
|
|
assert_eq!(b.pos_of(water), None);
|
|
assert_eq!(b.kinds_at(2, 4), Vec::<String>::new());
|
|
assert_eq!(b.pos_of(baba), Some((1, 4)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_09_you_and_win() {
|
|
let mut b = new_baba(empty_level(4, 4));
|
|
write(&mut b, &[("BABA", 0, 0), ("IS", 1, 0), ("YOU", 2, 0)]);
|
|
write(&mut b, &[("BABA", 0, 1), ("IS", 1, 1), ("WIN", 2, 1)]);
|
|
b.place("baba", 0, 3);
|
|
|
|
assert!(!b.won());
|
|
|
|
b.step(Dir::Right);
|
|
|
|
assert!(b.won());
|
|
assert!(!b.lost());
|
|
}
|
|
|
|
#[test]
|
|
fn test_09_defeat_beats_win() {
|
|
let mut b = new_baba(empty_level(5, 4));
|
|
write(&mut b, &[("BABA", 0, 0), ("IS", 1, 0), ("YOU", 2, 0)]);
|
|
write(
|
|
&mut b,
|
|
&[
|
|
("FLAG", 0, 1),
|
|
("IS", 1, 1),
|
|
("WIN", 2, 1),
|
|
("AND", 3, 1),
|
|
("DEFEAT", 4, 1),
|
|
],
|
|
);
|
|
let baba = b.place("baba", 0, 3);
|
|
let flag = b.place("flag", 1, 3);
|
|
|
|
b.step(Dir::Right);
|
|
|
|
assert_eq!(b.pos_of(baba), None);
|
|
assert!(!b.won());
|
|
assert!(b.lost());
|
|
assert_eq!(b.pos_of(flag), Some((1, 3)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_09_negated_conversion_beats_group() {
|
|
let mut b = new_baba(empty_level(4, 5));
|
|
write(&mut b, &[("BABA", 0, 0), ("IS", 1, 0), ("YOU", 2, 0)]);
|
|
write(&mut b, &[("ALL", 0, 1), ("IS", 1, 1), ("ROCK", 2, 1)]);
|
|
write(
|
|
&mut b,
|
|
&[("BABA", 0, 2), ("IS", 1, 2), ("NOT", 2, 2), ("ROCK", 3, 2)],
|
|
);
|
|
let baba = b.place("baba", 0, 4);
|
|
b.place("key", 3, 4);
|
|
|
|
b.step(Dir::Right);
|
|
|
|
assert_eq!(b.kinds_at(3, 4), vec!["rock"]);
|
|
assert_eq!(b.pos_of(baba), Some((1, 4)));
|
|
assert_eq!(b.kinds_at(1, 4), vec!["baba"]);
|
|
let active = b.active_rules();
|
|
assert!(active.contains(&"ALL IS ROCK".to_string()));
|
|
assert!(active.contains(&"BABA IS NOT ROCK".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_09_conversion_reparses_before_hazards() {
|
|
let mut b = new_baba(empty_level(5, 6));
|
|
write(&mut b, &[("BABA", 0, 0), ("IS", 1, 0), ("YOU", 2, 0)]);
|
|
write(&mut b, &[("LAVA", 0, 1), ("IS", 1, 1), ("HOT", 2, 1)]);
|
|
write(&mut b, &[("BABA", 0, 2), ("IS", 1, 2), ("MELT", 2, 2)]);
|
|
write(&mut b, &[("TEXT", 0, 3), ("IS", 1, 3), ("ROCK", 2, 3)]);
|
|
let baba = b.place("baba", 0, 5);
|
|
b.place("lava", 1, 5);
|
|
|
|
b.step(Dir::Right);
|
|
|
|
// Conversion turned every word into a rock, so no rule makes Baba MELT when the hazards run.
|
|
assert_eq!(b.kinds_at(0, 0), vec!["rock"]);
|
|
assert_eq!(b.rules(), vec!["TEXT IS PUSH"]);
|
|
assert_eq!(b.pos_of(baba), Some((1, 5)));
|
|
assert_eq!(b.kinds_at(1, 5), vec!["baba", "lava"]);
|
|
assert!(b.lost());
|
|
}
|
|
|
|
#[test]
|
|
fn test_09_conversion_before_hazards() {
|
|
let mut b = new_baba(empty_level(5, 6));
|
|
write(&mut b, &[("BABA", 0, 0), ("IS", 1, 0), ("YOU", 2, 0)]);
|
|
write(&mut b, &[("LAVA", 0, 1), ("IS", 1, 1), ("HOT", 2, 1)]);
|
|
write(&mut b, &[("KEY", 0, 2), ("IS", 1, 2), ("MELT", 2, 2)]);
|
|
write(&mut b, &[("KEY", 0, 3), ("IS", 1, 3), ("ROCK", 2, 3)]);
|
|
b.place("baba", 0, 5);
|
|
b.place("lava", 3, 5);
|
|
let key = b.place("key", 3, 5);
|
|
|
|
b.step(Dir::Right);
|
|
|
|
// The key became a rock before the hazards ran, and a rock is not MELT.
|
|
assert_eq!(b.pos_of(key), Some((3, 5)));
|
|
assert_eq!(b.kinds_at(3, 5), vec!["lava", "rock"]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_09_last_parse_matches_board() {
|
|
let mut b = new_baba(empty_level(6, 6));
|
|
write(&mut b, &[("BABA", 0, 0), ("IS", 1, 0), ("YOU", 2, 0)]);
|
|
write(&mut b, &[("FLAG", 0, 2), ("IS", 1, 2)]);
|
|
let win_text = b.place("text:WIN", 2, 2);
|
|
let water = b.place("water", 2, 2);
|
|
write(&mut b, &[("WATER", 3, 4), ("IS", 4, 4), ("SINK", 5, 3)]);
|
|
b.place("baba", 5, 2);
|
|
|
|
assert!(b.rules().contains(&"FLAG IS WIN".to_string()));
|
|
|
|
b.step(Dir::Down);
|
|
|
|
// The push completed WATER IS SINK; the water then sank the word WIN it lay under.
|
|
assert_eq!(b.pos_of(win_text), None);
|
|
assert_eq!(b.pos_of(water), None);
|
|
assert_eq!(
|
|
b.rules(),
|
|
vec!["BABA IS YOU", "TEXT IS PUSH", "WATER IS SINK"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_09_hazard_order_melt_before_defeat() {
|
|
let mut b = new_baba(empty_level(5, 5));
|
|
write(&mut b, &[("BABA", 0, 0), ("IS", 1, 0), ("YOU", 2, 0)]);
|
|
write(&mut b, &[("LAVA", 0, 1), ("IS", 1, 1), ("HOT", 2, 1)]);
|
|
write(
|
|
&mut b,
|
|
&[
|
|
("SKULL", 0, 2),
|
|
("IS", 1, 2),
|
|
("DEFEAT", 2, 2),
|
|
("AND", 3, 2),
|
|
("MELT", 4, 2),
|
|
],
|
|
);
|
|
let baba = b.place("baba", 0, 4);
|
|
b.place("lava", 1, 4);
|
|
let skull = b.place("skull", 1, 4);
|
|
|
|
b.step(Dir::Right);
|
|
|
|
// Inferred from the order of block(): HOT/MELT removes the skull before DEFEAT is checked.
|
|
assert_eq!(b.pos_of(skull), None);
|
|
assert_eq!(b.pos_of(baba), Some((1, 4)));
|
|
assert!(!b.lost());
|
|
}
|
|
|
|
#[test]
|
|
fn test_09_hazard_order_sink_before_melt() {
|
|
let mut b = new_baba(empty_level(5, 5));
|
|
write(&mut b, &[("BABA", 0, 0), ("IS", 1, 0), ("YOU", 2, 0)]);
|
|
write(&mut b, &[("WATER", 0, 1), ("IS", 1, 1), ("SINK", 2, 1)]);
|
|
write(
|
|
&mut b,
|
|
&[
|
|
("KEY", 0, 2),
|
|
("IS", 1, 2),
|
|
("HOT", 2, 2),
|
|
("AND", 3, 2),
|
|
("MELT", 4, 2),
|
|
],
|
|
);
|
|
b.place("baba", 0, 4);
|
|
let water = b.place("water", 4, 4);
|
|
let key = b.place("key", 4, 4);
|
|
|
|
b.step(Dir::Right);
|
|
|
|
// Inferred from the order of block(): SINK runs while the key is still there to sink with.
|
|
assert_eq!(b.pos_of(key), None);
|
|
assert_eq!(b.pos_of(water), None);
|
|
}
|
|
|
|
/// Everything the harness shows: every tile's kinds and stack, where each id up to
|
|
/// `last_id` is, the rules, the outcome.
|
|
#[derive(Debug, PartialEq)]
|
|
struct View {
|
|
tiles: Vec<(Vec<String>, Vec<String>)>,
|
|
positions: Vec<Option<(i32, i32)>>,
|
|
rules: Vec<String>,
|
|
active: Vec<String>,
|
|
won: bool,
|
|
lost: bool,
|
|
}
|
|
|
|
fn view(b: &Baba, width: i32, height: i32, last_id: Id) -> View {
|
|
let mut tiles = vec![];
|
|
for y in 0..height {
|
|
for x in 0..width {
|
|
tiles.push((b.kinds_at(x, y), b.stack_at(x, y)));
|
|
}
|
|
}
|
|
View {
|
|
tiles,
|
|
positions: (1..=last_id).map(|id| b.pos_of(id)).collect(),
|
|
rules: b.rules(),
|
|
active: b.active_rules(),
|
|
won: b.won(),
|
|
lost: b.lost(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_09_undo_restores_everything() {
|
|
let mut b = new_baba(empty_level(5, 5));
|
|
write(&mut b, &[("BABA", 0, 0), ("IS", 1, 0), ("YOU", 2, 0)]);
|
|
write(
|
|
&mut b,
|
|
&[
|
|
("KEY", 0, 1),
|
|
("IS", 1, 1),
|
|
("ROCK", 2, 1),
|
|
("AND", 3, 1),
|
|
("FLAG", 4, 1),
|
|
],
|
|
);
|
|
write(&mut b, &[("ROCK", 0, 2), ("IS", 1, 2), ("MELT", 2, 2)]);
|
|
write(&mut b, &[("LAVA", 0, 3), ("IS", 1, 3), ("HOT", 2, 3)]);
|
|
write(&mut b, &[("FLAG", 0, 4), ("IS", 1, 4)]);
|
|
let win_text = b.place("text:WIN", 3, 4);
|
|
let key = b.place("key", 3, 4);
|
|
b.place("lava", 3, 4);
|
|
let baba = b.place("baba", 4, 4);
|
|
assert_eq!(baba, 20);
|
|
let before = view(&b, 5, 5, 20);
|
|
|
|
b.step(Dir::Left);
|
|
|
|
// The turn moved Baba and the word WIN, converted the key, melted it and won.
|
|
assert_eq!(b.pos_of(baba), Some((3, 4)));
|
|
assert_eq!(b.pos_of(win_text), Some((2, 4)));
|
|
assert_eq!(b.pos_of(key), None);
|
|
assert_eq!(b.stack_at(3, 4), vec!["lava", "baba", "flag"]);
|
|
assert_eq!(b.pos_of(21), Some((3, 4)));
|
|
assert!(b.rules().contains(&"FLAG IS WIN".to_string()));
|
|
assert!(b.won());
|
|
let after = view(&b, 5, 5, 21);
|
|
|
|
assert!(b.undo());
|
|
|
|
assert_eq!(view(&b, 5, 5, 20), before);
|
|
|
|
b.step(Dir::Left);
|
|
|
|
// The same step gives the same result, ids included: the new flag is 21 again.
|
|
assert_eq!(view(&b, 5, 5, 21), after);
|
|
}
|
|
|
|
#[test]
|
|
fn test_09_undo_walks_back_and_stops() {
|
|
let mut b = new_baba(empty_level(4, 3));
|
|
write(&mut b, &[("BABA", 0, 0), ("IS", 1, 0), ("YOU", 2, 0)]);
|
|
let baba = b.place("baba", 0, 2);
|
|
|
|
b.step(Dir::Right);
|
|
b.step(Dir::Right);
|
|
|
|
assert_eq!(b.pos_of(baba), Some((2, 2)));
|
|
assert!(b.undo());
|
|
assert_eq!(b.pos_of(baba), Some((1, 2)));
|
|
assert!(b.undo());
|
|
assert_eq!(b.pos_of(baba), Some((0, 2)));
|
|
assert!(!b.undo());
|
|
assert_eq!(b.pos_of(baba), Some((0, 2)));
|
|
}
|