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,61 @@
|
||||
//! 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 and PULL, 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, pull, 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 PULL object on the tile ahead blocks the step, as STOP does. A PULL object
|
||||
/// directly behind the mover tries to follow it; if it can't, it stays behind and
|
||||
/// the mover still moves.
|
||||
fn pull(p: &mut Planner, s: Step) -> bool {
|
||||
if p.staying_at(s.to).into_iter().any(|o| p.has(o, "PULL")) {
|
||||
return false;
|
||||
}
|
||||
let (dx, dy) = s.dir.delta();
|
||||
let behind = (s.from.0 - dx, s.from.1 - dy);
|
||||
for o in p.staying_at(behind) {
|
||||
if p.has(o, "PULL") {
|
||||
p.try_move(o); // whether it follows doesn't decide the mover's step
|
||||
}
|
||||
}
|
||||
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"))
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// 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 and PULL, then STOP). A new
|
||||
// movement property is one more response here.
|
||||
|
||||
import { puzzle } from "../engine";
|
||||
|
||||
type Planner = puzzle.Planner;
|
||||
type Step = puzzle.Step;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
function openShut(p: Planner, s: Step): boolean {
|
||||
for (const [mine, theirs] of [
|
||||
["OPEN", "SHUT"],
|
||||
["SHUT", "OPEN"],
|
||||
]) {
|
||||
if (!p.has(s.mover, mine)) continue;
|
||||
const partner = p.stayingAt(s.to).find((o) => p.has(o, theirs));
|
||||
if (partner !== undefined) {
|
||||
p.destroy(partner);
|
||||
p.destroy(s.mover);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Every PUSH object on the tile ahead must step the same way, or the step fails. */
|
||||
function push(p: Planner, s: Step): boolean {
|
||||
return p.stayingAt(s.to).every((o) => !p.has(o, "PUSH") || p.tryMove(o));
|
||||
}
|
||||
|
||||
/**
|
||||
* A PULL object on the tile ahead blocks the step, as STOP does. A PULL object
|
||||
* directly behind the mover tries to follow it; if it can't, it stays behind and
|
||||
* the mover still moves.
|
||||
*/
|
||||
function pull(p: Planner, s: Step): boolean {
|
||||
if (p.stayingAt(s.to).some((o) => p.has(o, "PULL"))) return false;
|
||||
const [dx, dy] = puzzle.delta(s.dir);
|
||||
for (const o of p.stayingAt([s.from[0] - dx, s.from[1] - dy])) {
|
||||
if (p.has(o, "PULL")) p.tryMove(o); // whether it follows doesn't decide the mover's step
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** A STOP object still on the tile ahead blocks the step. */
|
||||
function stop(p: Planner, s: Step): boolean {
|
||||
return !p.stayingAt(s.to).some((o) => p.has(o, "STOP"));
|
||||
}
|
||||
|
||||
/** The movement responses, in the order the engine runs them. */
|
||||
export function responses(): puzzle.Response[] {
|
||||
return [openShut, push, pull, stop];
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
[
|
||||
{ "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" },
|
||||
{ "name": "OPEN", "role": "property" },
|
||||
{ "name": "SHUT", "role": "property" },
|
||||
{ "name": "PULL", "role": "property" }
|
||||
]
|
||||
Reference in new issue
Block a user