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

@@ -0,0 +1,48 @@
//! Content: the cards and the Baba Is You words. Only this directory (and the card data in `cards/`) may name
//! specific cards, or specific Baba Is You nouns.
//!
//! From Module 5 every card is a JSON file in `cards/` at the crate root, in the
//! course's card format (`course/schema/card.schema.json`). The functions that
//! were card effects in Modules 2–4 are gone: the engine interprets the data.
//! `start-module` copies the course's card files into `cards/`; add your own there.
use std::path::Path;
use std::sync::OnceLock;
use crate::engine::CardDef;
/// Every card in `cards/`, in file-name order. Read once per test run.
pub fn cards() -> Vec<CardDef> {
static CARDS: OnceLock<Vec<CardDef>> = OnceLock::new();
CARDS
.get_or_init(|| load_dir(&Path::new(env!("CARGO_MANIFEST_DIR")).join("cards")))
.clone()
}
/// Parse every `.json` file in `dir` as a card definition.
pub fn load_dir(dir: &Path) -> Vec<CardDef> {
let entries = std::fs::read_dir(dir)
.unwrap_or_else(|e| panic!("cannot read card directory {}: {e}", dir.display()));
let mut paths: Vec<_> = entries
.map(|entry| entry.expect("card directory entry").path())
.filter(|p| p.extension().and_then(|x| x.to_str()) == Some("json"))
.collect();
paths.sort();
paths
.iter()
.map(|path| {
let text = std::fs::read_to_string(path)
.unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
serde_json::from_str(&text)
.unwrap_or_else(|e| panic!("{} is not a valid card: {e}", path.display()))
})
.collect()
}
/// Part II: the words of Baba Is You the parser can read, besides the engine's own
/// (`IS`, `AND`, `NOT`, `TEXT`). Nouns and properties are data: a new word is one
/// more entry in `words.json` in this directory.
pub fn words() -> Vec<crate::engine::puzzle::WordDef> {
serde_json::from_str(include_str!("words.json"))
.expect("src/content/words.json is not a word list")
}
@@ -0,0 +1,15 @@
[
{ "name": "BABA", "role": "noun" },
{ "name": "ROCK", "role": "noun" },
{ "name": "WALL", "role": "noun" },
{ "name": "FLAG", "role": "noun" },
{ "name": "LAVA", "role": "noun" },
{ "name": "KEY", "role": "noun" },
{ "name": "DOOR", "role": "noun" },
{ "name": "SKULL", "role": "noun" },
{ "name": "WATER", "role": "noun" },
{ "name": "YOU", "role": "property" },
{ "name": "WIN", "role": "property" },
{ "name": "PUSH", "role": "property" },
{ "name": "STOP", "role": "property" }
]