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:
Lucas WintherandClaude Opus 5.5 committed 2026-10-29 04:05:42 +01:00
1 parent d993457772
commit bd80cefbef
64 files changed
+6933

No files matched your search

@@ -0,0 +1,124 @@
//! A level in play: the board, the words content provides, and the rules compiled
//! from them. Every change to the board recompiles the rules from scratch: parse,
//! then resolve. Every question about what an object is goes through `has_property`.
//!
//! A move (Module 8) is planned for each `YOU` object, then committed; the rules are
//! reparsed once, after all of them.
use super::grid::{Board, Kind, LevelDef, ObjectId};
use super::movement::{Dir, Plan, Planner, Response};
use super::parse::{parse, Rule};
use super::resolve::resolve;
use super::word::{Lexicon, WordDef};
#[derive(Debug, Clone)]
pub struct Level {
board: Board,
lexicon: Lexicon,
responses: Vec<Response>,
rules: Vec<Rule>,
active: Vec<Rule>,
}
/// The property that makes an object move when the player moves.
pub const YOU: &str = "YOU";
impl Level {
/// A level from its definition, the words content provides, and content's
/// movement responses, in the order they are to run.
pub fn new(def: &LevelDef, words: &[WordDef], responses: Vec<Response>) -> Level {
let mut level = Level {
board: Board::new(def.width, def.height),
lexicon: Lexicon::new(words),
responses,
rules: vec![],
active: vec![],
};
for o in &def.objects {
level.board.place(Kind::parse(&o.kind), o.x, o.y);
}
level.reparse();
level
}
pub fn place(&mut self, kind: &str, x: i32, y: i32) -> ObjectId {
let id = self.board.place(Kind::parse(kind), x, y);
self.reparse();
id
}
pub fn board(&self) -> &Board {
&self.board
}
/// The movement responses, in the order they run.
pub fn responses(&self) -> &[Response] {
&self.responses
}
/// Every rule the text spells, as parsed: negated rules and duplicates included.
pub fn rules(&self) -> &[Rule] {
&self.rules
}
/// The rules in force after the resolution pass.
pub fn active_rules(&self) -> &[Rule] {
&self.active
}
/// Does object `id` have `property`? Yes if an active rule gives it to a noun
/// that names the object and no active negated rule takes it from one. A
/// destroyed object has no properties.
pub fn has_property(&self, id: ObjectId, property: &str) -> bool {
let Some(object) = self.board.find(id) else {
return false;
};
let kind = &object.kind;
let about_it: Vec<&Rule> = self
.active
.iter()
.filter(|r| r.complement == property && self.lexicon.names(&r.subject, kind))
.collect();
about_it.iter().any(|r| !r.negated) && !about_it.iter().any(|r| r.negated)
}
/// Move every `YOU` object one tile in `dir`, then reparse. The `YOU` objects
/// move one at a time, the one furthest ahead in `dir` first (oldest first on a
/// tie); each one's move is planned in full, then committed, before the next is
/// planned. Every move uses the rules in force before the first.
pub fn move_you(&mut self, dir: Dir) {
let (dx, dy) = dir.delta();
let mut you: Vec<_> = self
.board
.objects()
.filter(|o| self.has_property(o.id, YOU))
.map(|o| (-(o.x * dx + o.y * dy), o.id))
.collect();
you.sort();
for (_, id) in you {
if self.board.find(id).is_none() {
continue; // destroyed by an earlier move
}
let mut planner = Planner::new(self, dir);
if planner.try_move(id) {
let plan = planner.into_plan();
self.commit(&plan, dir);
}
}
self.reparse();
}
fn commit(&mut self, plan: &Plan, dir: Dir) {
for &id in &plan.destroyed {
self.board.remove(id);
}
for &id in plan.moving.difference(&plan.destroyed) {
self.board.shift(id, dir.delta());
}
}
fn reparse(&mut self) {
self.rules = parse(&self.board, &self.lexicon);
self.active = resolve(&self.rules);
}
}