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 <[email protected]>
45 lines
1.5 KiB
Rust
45 lines
1.5 KiB
Rust
//! Part II: what each property does when an object moves. The engine plans a move
|
|
//! one step at a time and runs these responses for every step, in this order (the
|
|
//! order of the wiki's movement checks: OPEN/SHUT, then PUSH, then STOP). A new
|
|
//! movement property is one more response here.
|
|
|
|
use crate::engine::puzzle::{Planner, Response, Step};
|
|
|
|
/// The movement responses, in the order the engine runs them.
|
|
pub fn responses() -> Vec<Response> {
|
|
vec![open_shut, push, stop]
|
|
}
|
|
|
|
/// An OPEN object stepping onto a SHUT one (or a SHUT one onto an OPEN one)
|
|
/// destroys both, and that object doesn't block the step. One partner per step:
|
|
/// the oldest.
|
|
fn open_shut(p: &mut Planner, s: Step) -> bool {
|
|
for (mine, theirs) in [("OPEN", "SHUT"), ("SHUT", "OPEN")] {
|
|
if !p.has(s.mover, mine) {
|
|
continue;
|
|
}
|
|
let partner = p.staying_at(s.to).into_iter().find(|&o| p.has(o, theirs));
|
|
if let Some(partner) = partner {
|
|
p.destroy(partner);
|
|
p.destroy(s.mover);
|
|
return true;
|
|
}
|
|
}
|
|
true
|
|
}
|
|
|
|
/// Every PUSH object on the tile ahead must step the same way, or the step fails.
|
|
fn push(p: &mut Planner, s: Step) -> bool {
|
|
for o in p.staying_at(s.to) {
|
|
if p.has(o, "PUSH") && !p.try_move(o) {
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
}
|
|
|
|
/// A STOP object still on the tile ahead blocks the step.
|
|
fn stop(p: &mut Planner, s: Step) -> bool {
|
|
!p.staying_at(s.to).into_iter().any(|o| p.has(o, "STOP"))
|
|
}
|