feat: module 08 push chains
Movement planned in full, then committed: a Planner builds a plan (moving and destroyed ids) by running content's movement responses (OPEN/SHUT, PUSH, STOP) for every step, rolling back failed branches. Edge of level blocks; YOU objects move furthest-ahead first; rules are reparsed once after movement. Harness: move/move_dir(Dir), posOf, kindsAt. Expansion Drop: PULL as one word plus one content response. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d993457772
commit
bd80cefbef
64 files changed
+6933
No files matched your search
@@ -0,0 +1,139 @@
|
||||
// Module 8: Push chains. 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_08_chain_moves_together() {
|
||||
let mut b = new_baba(empty_level(6, 4));
|
||||
write(&mut b, &[("BABA", 0, 0), ("IS", 1, 0), ("YOU", 2, 0)]);
|
||||
write(&mut b, &[("ROCK", 0, 1), ("IS", 1, 1), ("PUSH", 2, 1)]);
|
||||
let baba = b.place("baba", 0, 3);
|
||||
let rocks = [
|
||||
b.place("rock", 1, 3),
|
||||
b.place("rock", 2, 3),
|
||||
b.place("rock", 3, 3),
|
||||
];
|
||||
|
||||
b.move_dir(Dir::Right);
|
||||
|
||||
assert_eq!(b.pos_of(baba), Some((1, 3)));
|
||||
assert_eq!(b.pos_of(rocks[0]), Some((2, 3)));
|
||||
assert_eq!(b.pos_of(rocks[1]), Some((3, 3)));
|
||||
assert_eq!(b.pos_of(rocks[2]), Some((4, 3)));
|
||||
assert_eq!(b.kinds_at(0, 3), Vec::<String>::new());
|
||||
assert_eq!(b.kinds_at(4, 3), vec!["rock"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_08_chain_into_stop_nothing_moves() {
|
||||
let mut b = new_baba(empty_level(6, 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, &[("WALL", 0, 2), ("IS", 1, 2), ("STOP", 2, 2)]);
|
||||
let baba = b.place("baba", 0, 4);
|
||||
let rocks = [
|
||||
b.place("rock", 1, 4),
|
||||
b.place("rock", 2, 4),
|
||||
b.place("rock", 3, 4),
|
||||
];
|
||||
let wall = b.place("wall", 4, 4);
|
||||
|
||||
b.move_dir(Dir::Right);
|
||||
|
||||
assert_eq!(b.pos_of(baba), Some((0, 4)));
|
||||
assert_eq!(b.pos_of(rocks[0]), Some((1, 4)));
|
||||
assert_eq!(b.pos_of(rocks[1]), Some((2, 4)));
|
||||
assert_eq!(b.pos_of(rocks[2]), Some((3, 4)));
|
||||
assert_eq!(b.pos_of(wall), Some((4, 4)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_08_open_shut_during_push() {
|
||||
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),
|
||||
("PUSH", 2, 1),
|
||||
("AND", 3, 1),
|
||||
("OPEN", 4, 1),
|
||||
],
|
||||
);
|
||||
write(
|
||||
&mut b,
|
||||
&[
|
||||
("DOOR", 0, 2),
|
||||
("IS", 1, 2),
|
||||
("STOP", 2, 2),
|
||||
("AND", 3, 2),
|
||||
("SHUT", 4, 2),
|
||||
],
|
||||
);
|
||||
let baba = b.place("baba", 0, 4);
|
||||
let key = b.place("key", 1, 4);
|
||||
let door = b.place("door", 2, 4);
|
||||
|
||||
b.move_dir(Dir::Right);
|
||||
|
||||
assert_eq!(b.pos_of(key), None);
|
||||
assert_eq!(b.pos_of(door), None);
|
||||
// Inferred, not stated by a source: Baba lands on the key's former tile.
|
||||
assert_eq!(b.pos_of(baba), Some((1, 4)));
|
||||
assert_eq!(b.kinds_at(2, 4), Vec::<String>::new());
|
||||
assert_eq!(b.kinds_at(1, 4), vec!["baba"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_08_text_pushed() {
|
||||
let mut b = new_baba(empty_level(4, 5));
|
||||
write(&mut b, &[("BABA", 0, 0), ("IS", 1, 0), ("YOU", 2, 0)]);
|
||||
let baba = b.place("baba", 0, 2);
|
||||
let rock_text = b.place("text:ROCK", 1, 2);
|
||||
write(&mut b, &[("IS", 2, 3), ("PUSH", 2, 4)]);
|
||||
|
||||
assert_eq!(b.rules(), vec!["BABA IS YOU", "TEXT IS PUSH"]);
|
||||
|
||||
b.move_dir(Dir::Right);
|
||||
|
||||
assert_eq!(b.pos_of(baba), Some((1, 2)));
|
||||
assert_eq!(b.pos_of(rock_text), Some((2, 2)));
|
||||
assert_eq!(
|
||||
b.rules(),
|
||||
vec!["BABA IS YOU", "ROCK IS PUSH", "TEXT IS PUSH"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_08_level_edge_is_stop() {
|
||||
let mut b = new_baba(empty_level(3, 3));
|
||||
write(&mut b, &[("BABA", 0, 0), ("IS", 1, 0), ("YOU", 2, 0)]);
|
||||
let baba = b.place("baba", 1, 2);
|
||||
let flag_text = b.place("text:FLAG", 2, 2);
|
||||
|
||||
b.move_dir(Dir::Right);
|
||||
|
||||
assert_eq!(b.pos_of(baba), Some((1, 2)));
|
||||
assert_eq!(b.pos_of(flag_text), Some((2, 2)));
|
||||
|
||||
b.move_dir(Dir::Down);
|
||||
|
||||
assert_eq!(b.pos_of(baba), Some((1, 2)));
|
||||
assert_eq!(b.pos_of(flag_text), Some((2, 2)));
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// Module 8: Push chains. Each test matches a heading in scenarios.md.
|
||||
import { expect, test } from "vitest";
|
||||
import { newBaba, type Baba } from "../src/harness";
|
||||
|
||||
/** An empty level `width` wide and `height` high. */
|
||||
const emptyLevel = (width: number, height: number) => ({ width, height, objects: [] });
|
||||
|
||||
/** Place one text object per word: `[word, x, y]`. */
|
||||
function write(b: Baba, words: [string, number, number][]): void {
|
||||
for (const [word, x, y] of words) b.place(`text:${word}`, x, y);
|
||||
}
|
||||
|
||||
test("test_08_chain_moves_together", () => {
|
||||
const b = newBaba(emptyLevel(6, 4));
|
||||
write(b, [
|
||||
["BABA", 0, 0],
|
||||
["IS", 1, 0],
|
||||
["YOU", 2, 0],
|
||||
]);
|
||||
write(b, [
|
||||
["ROCK", 0, 1],
|
||||
["IS", 1, 1],
|
||||
["PUSH", 2, 1],
|
||||
]);
|
||||
const baba = b.place("baba", 0, 3);
|
||||
const rocks = [b.place("rock", 1, 3), b.place("rock", 2, 3), b.place("rock", 3, 3)];
|
||||
|
||||
b.move("right");
|
||||
|
||||
expect(b.posOf(baba)).toEqual([1, 3]);
|
||||
expect(b.posOf(rocks[0])).toEqual([2, 3]);
|
||||
expect(b.posOf(rocks[1])).toEqual([3, 3]);
|
||||
expect(b.posOf(rocks[2])).toEqual([4, 3]);
|
||||
expect(b.kindsAt(0, 3)).toEqual([]);
|
||||
expect(b.kindsAt(4, 3)).toEqual(["rock"]);
|
||||
});
|
||||
|
||||
test("test_08_chain_into_stop_nothing_moves", () => {
|
||||
const b = newBaba(emptyLevel(6, 5));
|
||||
write(b, [
|
||||
["BABA", 0, 0],
|
||||
["IS", 1, 0],
|
||||
["YOU", 2, 0],
|
||||
]);
|
||||
write(b, [
|
||||
["ROCK", 0, 1],
|
||||
["IS", 1, 1],
|
||||
["PUSH", 2, 1],
|
||||
]);
|
||||
write(b, [
|
||||
["WALL", 0, 2],
|
||||
["IS", 1, 2],
|
||||
["STOP", 2, 2],
|
||||
]);
|
||||
const baba = b.place("baba", 0, 4);
|
||||
const rocks = [b.place("rock", 1, 4), b.place("rock", 2, 4), b.place("rock", 3, 4)];
|
||||
const wall = b.place("wall", 4, 4);
|
||||
|
||||
b.move("right");
|
||||
|
||||
expect(b.posOf(baba)).toEqual([0, 4]);
|
||||
expect(b.posOf(rocks[0])).toEqual([1, 4]);
|
||||
expect(b.posOf(rocks[1])).toEqual([2, 4]);
|
||||
expect(b.posOf(rocks[2])).toEqual([3, 4]);
|
||||
expect(b.posOf(wall)).toEqual([4, 4]);
|
||||
});
|
||||
|
||||
test("test_08_open_shut_during_push", () => {
|
||||
const b = newBaba(emptyLevel(5, 5));
|
||||
write(b, [
|
||||
["BABA", 0, 0],
|
||||
["IS", 1, 0],
|
||||
["YOU", 2, 0],
|
||||
]);
|
||||
write(b, [
|
||||
["KEY", 0, 1],
|
||||
["IS", 1, 1],
|
||||
["PUSH", 2, 1],
|
||||
["AND", 3, 1],
|
||||
["OPEN", 4, 1],
|
||||
]);
|
||||
write(b, [
|
||||
["DOOR", 0, 2],
|
||||
["IS", 1, 2],
|
||||
["STOP", 2, 2],
|
||||
["AND", 3, 2],
|
||||
["SHUT", 4, 2],
|
||||
]);
|
||||
const baba = b.place("baba", 0, 4);
|
||||
const key = b.place("key", 1, 4);
|
||||
const door = b.place("door", 2, 4);
|
||||
|
||||
b.move("right");
|
||||
|
||||
expect(b.posOf(key)).toBeNull();
|
||||
expect(b.posOf(door)).toBeNull();
|
||||
// Inferred, not stated by a source: Baba lands on the key's former tile.
|
||||
expect(b.posOf(baba)).toEqual([1, 4]);
|
||||
expect(b.kindsAt(2, 4)).toEqual([]);
|
||||
expect(b.kindsAt(1, 4)).toEqual(["baba"]);
|
||||
});
|
||||
|
||||
test("test_08_text_pushed", () => {
|
||||
const b = newBaba(emptyLevel(4, 5));
|
||||
write(b, [
|
||||
["BABA", 0, 0],
|
||||
["IS", 1, 0],
|
||||
["YOU", 2, 0],
|
||||
]);
|
||||
const baba = b.place("baba", 0, 2);
|
||||
const rockText = b.place("text:ROCK", 1, 2);
|
||||
write(b, [
|
||||
["IS", 2, 3],
|
||||
["PUSH", 2, 4],
|
||||
]);
|
||||
|
||||
expect(b.rules()).toEqual(["BABA IS YOU", "TEXT IS PUSH"]);
|
||||
|
||||
b.move("right");
|
||||
|
||||
expect(b.posOf(baba)).toEqual([1, 2]);
|
||||
expect(b.posOf(rockText)).toEqual([2, 2]);
|
||||
expect(b.rules()).toEqual(["BABA IS YOU", "ROCK IS PUSH", "TEXT IS PUSH"]);
|
||||
});
|
||||
|
||||
test("test_08_level_edge_is_stop", () => {
|
||||
const b = newBaba(emptyLevel(3, 3));
|
||||
write(b, [
|
||||
["BABA", 0, 0],
|
||||
["IS", 1, 0],
|
||||
["YOU", 2, 0],
|
||||
]);
|
||||
const baba = b.place("baba", 1, 2);
|
||||
const flagText = b.place("text:FLAG", 2, 2);
|
||||
|
||||
b.move("right");
|
||||
|
||||
expect(b.posOf(baba)).toEqual([1, 2]);
|
||||
expect(b.posOf(flagText)).toEqual([2, 2]);
|
||||
|
||||
b.move("down");
|
||||
|
||||
expect(b.posOf(baba)).toEqual([1, 2]);
|
||||
expect(b.posOf(flagText)).toEqual([2, 2]);
|
||||
});
|
||||
Reference in new issue
Block a user