Files
systemic-game-design-course/course/09-turn-pipeline/tests/rust/test_09.rs
T
Lucas WintherandClaude Fable 5.2 8467c7929e fix(puzzle): an object steps once per movement phase; negated conversion is settled per object
Bug 1 (modules 08-10): a YOU object pulled by an earlier mover took a second step of its own. Level movement now keeps the ids that stepped in a committed plan this phase; such a YOU object starts no move, and Planner try_move answers no for it. Regression: test_08x_pulled_you_moves_once (Module 8 Expansion Drop).

Bug 2 (modules 09-10): convert honoured negation only per rule, so ALL IS ROCK beat BABA IS NOT ROCK. A target Y is now skipped for an object if any active negated rule IS NOT Y names it. Module 9 and 10 references (and their drop solutions) now carry ALL, the Module 7 Expansion Drop. Regression: test_09_negated_conversion_beats_group.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 09:18:00 +01:00

207 lines
6.4 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()));
}