feat: module 06 parser

First Part II module. The cumulative reference adds a second engine
(src/engine/puzzle: grid, lexer, parser) beside Part I's; nouns and
properties are data in src/content/words.json, and the engine names only
IS/AND/NOT/TEXT and the base rule TEXT IS PUSH. Rules are recompiled from
the board on every change; rules() is a sorted set. Adds the M6 scenarios
and tests (rows, columns, shared IS, AND distribution, stacked text, base
rule), the HOT word as a data-only Expansion Drop, the lesson with the
Part II bridge, and the Rust LevelDef shape in harness.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-29 03:33:53 +01:00
1 parent e04dc1a11f
commit 68fe9834c3
55 files changed
+5679 -1

No files matched your search

+118
View File
@@ -0,0 +1,118 @@
// Module 6: Grid, lexer and parser. 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()
}
}
fn object(kind: &str, x: i32, y: i32) -> LevelObject {
LevelObject {
kind: kind.to_string(),
x,
y,
}
}
/// 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_06_horizontal_rule() {
let b = new_baba(LevelDef {
width: 5,
height: 3,
objects: vec![
object("text:BABA", 0, 0),
object("text:IS", 1, 0),
object("text:YOU", 2, 0),
object("baba", 3, 2),
object("text:YOU", 0, 2),
object("text:IS", 1, 2),
object("text:FLAG", 2, 2),
],
..Default::default()
});
assert_eq!(b.rules(), vec!["BABA IS YOU", "TEXT IS PUSH"]);
}
#[test]
fn test_06_crossword_shared_is() {
let mut b = new_baba(empty_level(3, 3));
write(
&mut b,
&[("BABA", 0, 1), ("IS", 1, 1), ("YOU", 2, 1), ("FLAG", 1, 0)],
);
assert_eq!(b.rules(), vec!["BABA IS YOU", "TEXT IS PUSH"]);
b.place("text:WIN", 1, 2);
assert_eq!(
b.rules(),
vec!["BABA IS YOU", "FLAG IS WIN", "TEXT IS PUSH"]
);
}
#[test]
fn test_06_and_distributes() {
let mut b = new_baba(empty_level(7, 1));
write(
&mut b,
&[
("BABA", 0, 0),
("AND", 1, 0),
("ROCK", 2, 0),
("IS", 3, 0),
("PUSH", 4, 0),
("AND", 5, 0),
("STOP", 6, 0),
],
);
assert_eq!(
b.rules(),
vec![
"BABA IS PUSH",
"BABA IS STOP",
"ROCK IS PUSH",
"ROCK IS STOP",
"TEXT IS PUSH"
]
);
}
#[test]
fn test_06_stacked_text_all_combinations() {
let mut b = new_baba(empty_level(3, 1));
write(
&mut b,
&[("BABA", 0, 0), ("ROCK", 0, 0), ("IS", 1, 0), ("YOU", 2, 0)],
);
assert_eq!(
b.rules(),
vec!["BABA IS YOU", "ROCK IS YOU", "TEXT IS PUSH"]
);
}
#[test]
fn test_06_default_text_is_push() {
let b = new_baba(LevelDef {
width: 3,
height: 3,
objects: vec![object("baba", 1, 1)],
..Default::default()
});
assert_eq!(b.rules(), vec!["TEXT IS PUSH"]);
}
+78
View File
@@ -0,0 +1,78 @@
// Module 6: Grid, lexer and parser. 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_06_horizontal_rule", () => {
const b = newBaba({
width: 5,
height: 3,
objects: [
{ kind: "text:BABA", x: 0, y: 0 },
{ kind: "text:IS", x: 1, y: 0 },
{ kind: "text:YOU", x: 2, y: 0 },
{ kind: "baba", x: 3, y: 2 },
{ kind: "text:YOU", x: 0, y: 2 },
{ kind: "text:IS", x: 1, y: 2 },
{ kind: "text:FLAG", x: 2, y: 2 },
],
});
expect(b.rules()).toEqual(["BABA IS YOU", "TEXT IS PUSH"]);
});
test("test_06_crossword_shared_is", () => {
const b = newBaba(emptyLevel(3, 3));
write(b, [
["BABA", 0, 1],
["IS", 1, 1],
["YOU", 2, 1],
["FLAG", 1, 0],
]);
expect(b.rules()).toEqual(["BABA IS YOU", "TEXT IS PUSH"]);
b.place("text:WIN", 1, 2);
expect(b.rules()).toEqual(["BABA IS YOU", "FLAG IS WIN", "TEXT IS PUSH"]);
});
test("test_06_and_distributes", () => {
const b = newBaba(emptyLevel(7, 1));
write(b, [
["BABA", 0, 0],
["AND", 1, 0],
["ROCK", 2, 0],
["IS", 3, 0],
["PUSH", 4, 0],
["AND", 5, 0],
["STOP", 6, 0],
]);
expect(b.rules()).toEqual(["BABA IS PUSH", "BABA IS STOP", "ROCK IS PUSH", "ROCK IS STOP", "TEXT IS PUSH"]);
});
test("test_06_stacked_text_all_combinations", () => {
const b = newBaba(emptyLevel(3, 1));
write(b, [
["BABA", 0, 0],
["ROCK", 0, 0],
["IS", 1, 0],
["YOU", 2, 0],
]);
expect(b.rules()).toEqual(["BABA IS YOU", "ROCK IS YOU", "TEXT IS PUSH"]);
});
test("test_06_default_text_is_push", () => {
const b = newBaba({ width: 3, height: 3, objects: [{ kind: "baba", x: 1, y: 1 }] });
expect(b.rules()).toEqual(["TEXT IS PUSH"]);
});