Files
systemic-game-design-course/course/08-push-chains/lesson.md
T

397 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Module 8: Push chains: validate, then commit
Until now nothing on the board moved. Module 7 compiled the rules from a board that stood still. Now Baba walks, and when Baba walks into a rock that is `PUSH`, the rock moves too, and the rock behind it, and so on along the line. One key press can change many objects at once, and either all of them change or none of them do. This module builds that all-or-nothing move: first plan the whole move without touching the board, then apply the plan.
Part I had a step like this too. In Module 3 a destroyed minion wasn't removed on the spot: it was marked, and the Death Creation Step removed every marked minion together, at a defined point. In Module 5, Assemble checked that the new text really changed before it registered a card. Both separate deciding from doing. This module makes that separation the main point: a move is decided in full before the board changes at all.
You need your Module 7 workspace with all Module 1–7 tests passing, under git.
## Observe
All quotes come from sources recorded in the course's research notes. The Baba Is You Wiki is an unofficial community wiki; the course treats it as the best available description of the game, not as Hempuli's own word.
What `PUSH` does, from the wiki's [PUSH](https://babaiswiki.fandom.com/wiki/PUSH) page:
> If something tries to move into a PUSH object, it will try to move in the same direction to make room.
> This includes other PUSH objects.
So pushes chain. And what happens when the chain can't make room, from the [Esolangs wiki](https://esolangs.org/wiki/Baba_Is_You):
> Actors which move into the object will cause it to move in the direction that the actor entered from. If the object is unable to move, the actor will not perform its move.
The [PUSH](https://babaiswiki.fandom.com/wiki/PUSH) page adds that "PUSH also makes the object "solid" for collision checks like STOP does." The [STOP](https://babaiswiki.fandom.com/wiki/STOP) page: "STOP is a property that prevents most forms of movement from moving into the object." Put together: Baba, three rocks and a wall in a row. Baba pushes the first rock, which must push the second, which must push the third, which can't enter the wall's tile. The third rock doesn't move, so the second doesn't, so the first doesn't, so Baba doesn't.
The same STOP page names exceptions: "Some movement checks also override STOP to allow a movement, including EAT OPEN SHUT WEAK." The [OPEN](https://babaiswiki.fandom.com/wiki/OPEN) page explains the one this module builds:
> If an OPEN object is on, or moved onto, a same-float SHUT object (or vice-versa), both are removed.
> If the movement check succeeds in finding an "unlock", the normal obstruction of movement by STOP PUSH PULL does not apply.
> However, other obstructing objects may still prevent the movement, and this will also prevent the destruction from OPEN SHUT since no movement actually occurs.
> Destruction happens before the moving object actually moves onto the other object, therefore anything it HAS will spawn on the space the object was moving from.
The page also lists the forms of movement that make the check, PUSH among them: "The movement check applies to standard forms of movement, including FEAR AUTO CHILL FALL MOVE NUDGEDOWN PULL PUSH SHIFT YOU YOU2 3D." A pushed key counts as moving. The wiki's [Order of Operations](https://babaiswiki.fandom.com/wiki/Order_of_Operations) gives the order of the checks the game makes "after every movement step": "OPEN SHUT * WEAK EAT are checked simultaneously * SWAP * PUSH PULL are checked. * OPEN SHUT for pushed/pulled objects * WEAK EAT for pushed/pulled objects * Passive SWAP * STOP".
So: Baba, a key that is `PUSH` and `OPEN`, and a door that is `STOP` and `SHUT`, in a row. Baba pushes the key; the key moves into the door; the key is OPEN and the door SHUT, so both are destroyed, and the door's STOP doesn't block. Where does Baba end up? **No source says.** The course infers it: the key's move counted as a movement that wasn't blocked, so Baba's push succeeded and Baba moves one tile, onto the tile where the key was. Baba does not reach the door's tile; that would be a second step. The course's research notes mark this as an inference from the documented rules, and so does this lesson's test.
Last, text. The [TEXT](https://babaiswiki.fandom.com/wiki/TEXT) page lists "TEXT IS PUSH as a base rule", and the [Rule](https://babaiswiki.fandom.com/wiki/Rule) page says "Rules parse when any parsable object changes position or is destroyed". Push a word and the rules change.
## Predict
Write your answers down before you open the discussion, for example in `notes/08.md` in your workspace.
1. Baba, three PUSH rocks and a STOP wall, in a row, with one empty tile beyond the wall. Baba moves toward the wall. What moves?
2. Your first idea for code: walk along the chain from Baba, moving each object as you reach it. What goes wrong in question 1?
3. Baba pushes a `PUSH`+`OPEN` key into a `STOP`+`SHUT` door. What is left on the board, and where is Baba?
4. Same as 3, but `WALL IS STOP` is also written, and a wall sits on the door's tile. What happens?
5. Baba pushes the word `ROCK` into place so that `ROCK IS PUSH` reads down a column. Is a rock pushable during this move? After it?
6. A rock sits on the right edge of the level, and Baba pushes it right. What should happen? Do the sources say?
7. `BABA IS YOU` and `BABA IS PUSH`, and two Babas side by side. You press right. What do you want to happen, and what does your engine do?
<details><summary>Discussion</summary>
1. Nothing. The last rock can't move, so the chain can't, so Baba can't. "If the object is unable to move, the actor will not perform its move", and that applies at every link.
2. You move Baba, then the first rock, then the second, and only at the third do you find the wall. Now two objects have moved and must be put back. Moving from the far end first doesn't help in general: you have to know where the far end is, which means walking the chain first anyway. Any version that changes the board before it knows the answer has to undo, and undo is where bugs live.
3. Baba alone, on the key's former tile. Key and door are destroyed during the move. That Baba lands on the key's former tile, and not on the door's, is the course's inference (see Observe).
4. In the course: nothing moves and nothing is destroyed. The [OPEN](https://babaiswiki.fandom.com/wiki/OPEN) page says that after an unlock "the normal obstruction of movement by STOP PUSH PULL does not apply", and also that "other obstructing objects may still prevent the movement, and this will also prevent the destruction from OPEN SHUT since no movement actually occurs." The course reads "other obstructing objects" as any object on the tile other than the unlocked partner: the wall still blocks the key. That is the course's reading; the page doesn't settle whether a separate STOP object counts. Under that reading, a planner finds the unlock, then discovers the move fails anyway, and forgets the unlock. No Module 8 test sets up this board; the reference handles it by restoring the plan, and you can check yours by hand.
5. Not during: every check in this move uses the rules in force when it started. After: the rules are reparsed once, when movement ends. The [Order of Operations](https://babaiswiki.fandom.com/wiki/Order_of_Operations) lists a "(rule parsing)" right after `domovement()`.
6. The course's answer: the edge of the level blocks like STOP, so the rock doesn't move and neither does Baba. The sources don't say this directly; see "How the real game differs".
7. The sources read for this course don't cover it. If your engine moves the Babas one at a time and the left one goes first, it pushes the right one, and then the right one takes its own step: it moved two tiles. The reference moves the YOU object furthest ahead first, so each moves once. That is a choice; make yours deliberately.
</details>
## Spec
From the course repository, copy the module's tests into your workspace:
```sh
node tools/start-module.mjs 08 ~/my-engine
```
### The harness
Three new functions on `Baba`:
| TypeScript | Rust |
|---|---|
| `move(dir): void` | `move_dir(&mut self, dir: Dir)` (`move` is a Rust keyword) |
| `posOf(id): [number, number] \| null` | `pos_of(&self, id: Id) -> Option<(i32, i32)>` |
| `kindsAt(x, y): string[]` | `kinds_at(&self, x: i32, y: i32) -> Vec<String>` |
- `move(dir)` moves every `YOU` object one tile, `"up"`, `"down"`, `"left"` or `"right"` (Rust: `Dir::Up`, ...), with everything it pushes, then reparses the rules. Nothing else: no conversion, no hazards, no win. That is Module 9's turn.
- `posOf(id)` is where the object is, or `null` (Rust: `None`) once it has been destroyed.
- `kindsAt(x, y)` lists the kinds on a tile as they were placed (`"baba"`, `"text:ROCK"`), sorted.
In Rust, `Dir` is an enum with the variants `Up`, `Down`, `Left`, `Right`, exported from your harness. `course/harness.md` has its definition.
### The tests
The full statements are in `course/08-push-chains/scenarios.md`.
- `test_08_chain_moves_together`: Baba pushes three PUSH rocks; all four shift one tile.
- `test_08_chain_into_stop_nothing_moves`: Predict question 1, exactly.
- `test_08_open_shut_during_push`: Predict question 3. Key and door are destroyed and Baba is on the key's former tile, the course's inference.
- `test_08_text_pushed`: Predict question 5. Text is pushed by the base rule, and `rules()` after the move includes `ROCK IS PUSH`.
- `test_08_level_edge_is_stop`: Predict question 6. A word on the edge can't be pushed off, and Baba can't step off either.
The third test:
```rust
#[test]
fn test_08_open_shut_during_push() {
let mut b = new_baba(empty_level(5, 5));
write(&mut b, &[("BABA", 0, 0), ("IS", 1, 0), ("YOU", 2, 0)]);
write(
&mut b,
&[
("KEY", 0, 1),
("IS", 1, 1),
("PUSH", 2, 1),
("AND", 3, 1),
("OPEN", 4, 1),
],
);
write(
&mut b,
&[
("DOOR", 0, 2),
("IS", 1, 2),
("STOP", 2, 2),
("AND", 3, 2),
("SHUT", 4, 2),
],
);
let baba = b.place("baba", 0, 4);
let key = b.place("key", 1, 4);
let door = b.place("door", 2, 4);
b.move_dir(Dir::Right);
assert_eq!(b.pos_of(key), None);
assert_eq!(b.pos_of(door), None);
// Inferred, not stated by a source: Baba lands on the key's former tile.
assert_eq!(b.pos_of(baba), Some((1, 4)));
assert_eq!(b.kinds_at(2, 4), Vec::<String>::new());
assert_eq!(b.kinds_at(1, 4), vec!["baba"]);
}
```
```ts
test("test_08_open_shut_during_push", () => {
const b = newBaba(emptyLevel(5, 5));
write(b, [
["BABA", 0, 0],
["IS", 1, 0],
["YOU", 2, 0],
]);
write(b, [
["KEY", 0, 1],
["IS", 1, 1],
["PUSH", 2, 1],
["AND", 3, 1],
["OPEN", 4, 1],
]);
write(b, [
["DOOR", 0, 2],
["IS", 1, 2],
["STOP", 2, 2],
["AND", 3, 2],
["SHUT", 4, 2],
]);
const baba = b.place("baba", 0, 4);
const key = b.place("key", 1, 4);
const door = b.place("door", 2, 4);
b.move("right");
expect(b.posOf(key)).toBeNull();
expect(b.posOf(door)).toBeNull();
// Inferred, not stated by a source: Baba lands on the key's former tile.
expect(b.posOf(baba)).toEqual([1, 4]);
expect(b.kindsAt(2, 4)).toEqual([]);
expect(b.kindsAt(1, 4)).toEqual(["baba"]);
});
```
Run `cargo test` or `npm test`. The Module 8 tests fail to compile (Rust) or fail (TypeScript). Modules 1–7 still pass. That is RED.
## Build
Make the five tests pass, and keep Modules 1–7 green. Where code goes:
- **Plan, then commit.** A move has two phases. The first reads the board and builds a plan: which objects step one tile, which are destroyed. It changes nothing. The second applies the plan. If the plan fails, there is nothing to undo.
- **A failed branch leaves no trace.** While planning, an attempt can fail halfway: question 4's key finds its unlock, and then the wall blocks it. In the course's reading, whatever that attempt added to the plan (the destruction of key and door) goes too. Decide how your planner forgets.
- **The edge blocks.** A step that would leave the level fails, like a step into STOP.
- **Rules from before the move.** Every question asked while planning ("is this rock PUSH?") uses the rules in force when the move began. Reparse once, after all movement.
- **Several YOU objects.** Plan and commit them one at a time. Pick an order, and make sure a YOU object that is also PUSH doesn't move twice.
- **One more question.** PUSH, STOP and OPEN/SHUT each say what happens when something moves. Is each of them a branch in your planner, or something the planner runs? The Expansion Drop will bring a new movement property, and you won't be allowed to touch `src/engine/`.
<details><summary>Hint 1 — nudge</summary>
Write a function that answers one question: "can this object step one tile in this direction?" It doesn't move anything. To answer for Baba, it has to ask the same question about the rock ahead, and that one about the next. When it answers yes, you know the whole chain, and only then do you touch the board.
</details>
<details><summary>Hint 2 — concept</summary>
- **The plan is two sets of ids.** The objects that will step, and the objects that will be destroyed. Committing removes the destroyed ones and shifts the rest by one tile. A destroyed object doesn't step, even if it was pushed.
- **"Can this object step?" adds to the plan as it goes.** Put the object in the moving set, then check its step: off the level? Then for the objects on the tile ahead: an OPEN/SHUT partner is destroyed along with the mover and doesn't block; a PUSH object has to be able to step too (ask recursively); a STOP object that is still there blocks. If the answer is no, put the plan back to what it was before this object: copy it before, restore it on failure.
- **"Still there".** An object on the tile ahead that the plan already moves away, or destroys, isn't in the way. That is how a PUSH-and-STOP object gets pushed instead of blocking, and how the destroyed door stops blocking.
- **An object already in the plan** is already handled: the answer for it is yes, and nothing is added. That also stops any cycle.
- **Responses, in order.** If each property's reaction is a function the planner calls for every step, in the wiki's order (OPEN/SHUT, then PUSH, then STOP), the planner itself names none of them.
</details>
<details><summary>Hint 3 — pseudo-code</summary>
```text
move(dir):
you = objects with YOU, furthest ahead in dir first
for each id in you that still exists:
plan = { moving: {}, destroyed: {} }
if try_move(plan, id, dir): commit(plan, dir)
reparse()
try_move(plan, id, dir): // reads the board, never changes it
if id in plan.moving or id in plan.destroyed: return true
to = position(id) + dir
if to is outside the level: return false
saved = copy of plan
plan.moving += id
for each response in responses: // open_shut, push, stop
if not response(plan, step(id, from, to, dir)):
plan = saved
return false
return true
staying_at(plan, tile) = objects on tile, not in plan.moving, not in plan.destroyed
open_shut(plan, s): if s.mover is OPEN and some SHUT o in staying_at(s.to):
plan.destroyed += o, s.mover // and the reverse
return true
push(plan, s): for o in staying_at(s.to) with PUSH: if not try_move(plan, o, s.dir): return false
return true
stop(plan, s): return no o in staying_at(s.to) with STOP
commit(plan, dir): remove plan.destroyed; shift (plan.moving - plan.destroyed) by dir
```
</details>
When your tests pass, compare with the reference engine in `course/08-push-chains/reference/rust/src/` or `course/08-push-chains/reference/ts/src/`: the new `engine/puzzle/movement` (the planner), `engine/puzzle/level` (`move_you` / `moveYou`), and the new `content/movement` (the responses).
Decisions in the reference that the tests don't force:
- **The responses are content.** The engine owns the planner, the edge, the order of the YOU objects and the commit. What `OPEN`/`SHUT`, `PUSH` and `STOP` do is three functions in `src/content/`, which the engine runs for every step in the order content lists them. `YOU` stays in the engine: it picks who moves when the player presses a key.
- **Several YOU objects move furthest-ahead first**, oldest first on a tie, each planned and committed before the next is planned. All of them use the rules from before the first.
- **One partner per OPEN/SHUT step: the oldest** SHUT (or OPEN) object on the tile. The [OPEN](https://babaiswiki.fandom.com/wiki/OPEN) page: "Only one SHUT object is destroyed per OPEN object (and vice versa), based on the first object in update order." The reference's update order is object age.
- **A destroyed object has no properties.** `hasProperty` on it is `false`, and `posOf` is `null`.
> **How the real game differs**
> - **Baba's landing tile is inferred.** No source states where the pusher ends up when the pushed key and the door are destroyed. The course's choice, the key's former tile, follows from the OPEN page's "Destruction happens before the moving object actually moves onto the other object" and from the push counting as unblocked.
> - **The edge of the level.** No source read says in so many words that the edge blocks ordinary movement. The [Rule](https://babaiswiki.fandom.com/wiki/Rule) page lists `LEVEL IS STOP` among the base rules, and the [PULL](https://babaiswiki.fandom.com/wiki/PULL) page says "If LEVEL IS PULL, the overall level can be pulled at the border", which suggests the border is where the level object's STOP applies; that is an inference. The [Esolangs wiki](https://esolangs.org/wiki/Baba_Is_You) says "If an object is to leave the bounds of the level, it is immediately destroyed", about objects that leave by other means. The course has no level object: the edge is a wall in the engine.
> - **Several YOU objects.** The sources read don't say in which order they move. Furthest-ahead first is the course's choice.
> - **Float levels.** OPEN/SHUT needs both objects on the same float level ("a same-float SHUT object", [OPEN](https://babaiswiki.fandom.com/wiki/OPEN)); PUSH and STOP ignore it ("FLOAT does not allow bypassing STOP", [STOP](https://babaiswiki.fandom.com/wiki/STOP)). The course has no `FLOAT` yet, so every object is on the same level.
> - **The rest of the movement checks.** The game also checks WEAK, EAT, SWAP and PULL on every step ([Order of Operations](https://babaiswiki.fandom.com/wiki/Order_of_Operations)), and SAFE and HAS change what OPEN/SHUT destroys and leaves behind ([OPEN](https://babaiswiki.fandom.com/wiki/OPEN)). The course has none of these words.
> - **An object on the tile next to an unlock.** When OPEN/SHUT destroys a mover and its partner, the reference still runs the other checks for that step: a separate STOP object on the tile blocks (and undoes the unlock), and a separate PUSH object there is pushed. That is the course's reading of the OPEN page's "other obstructing objects may still prevent the movement"; the page doesn't say how a separate STOP or PUSH object is treated.
> - **Only YOU moves.** The game's `domovement()` moves objects in stages: YOU first, then MOVE, AUTO and CHILL, then NUDGE, FEAR and SHIFT ([Order of Operations](https://babaiswiki.fandom.com/wiki/Order_of_Operations)). The course has YOU only, and reparses after every move, where the game reparses only when text has changed: "Rules parse when any parsable object changes position or is destroyed, or an object is transformed to text" ([Rule](https://babaiswiki.fandom.com/wiki/Rule)).
## Name it
The pattern is **validate, then commit**: a change to many objects is checked as a whole before anything mutates. The plan is a value: you can build it, inspect it, throw it away. Only a plan that succeeded touches the board. Databases call the same idea a transaction (all of it happens, or none of it), and Module 3's death step is a cousin: mark first, remove together, at one defined point.
The reference's whole check, one object stepping one tile:
```rust
pub fn try_move(&mut self, id: ObjectId) -> bool {
if self.plan.moving.contains(&id) || self.plan.destroyed.contains(&id) {
return true;
}
let level = self.level;
let o = level.board().get(id);
let (dx, dy) = self.dir.delta();
let to = (o.x + dx, o.y + dy);
// The edge of the level blocks every step (the course's choice; see the lesson).
if !level.board().in_bounds(to.0, to.1) {
return false;
}
let before = self.plan.clone();
self.plan.moving.insert(id);
let step = Step {
mover: id,
from: (o.x, o.y),
to,
dir: self.dir,
};
let allowed = level.responses().iter().all(|respond| respond(self, step));
if !allowed {
self.plan = before;
}
allowed
}
```
```ts
tryMove(id: ObjectId): boolean {
if (this.plan.moving.has(id) || this.plan.destroyed.has(id)) return true;
const o = this.level.board.get(id);
const [dx, dy] = delta(this.dir);
const to: Pos = [o.x + dx, o.y + dy];
// The edge of the level blocks every step (the course's choice; see the lesson).
if (!this.level.board.inBounds(...to)) return false;
const before: Plan = { moving: new Set(this.plan.moving), destroyed: new Set(this.plan.destroyed) };
this.plan.moving.add(id);
const step: Step = { mover: id, from: [o.x, o.y], to, dir: this.dir };
const allowed = this.level.responses().every((respond) => respond(this, step));
if (!allowed) this.plan = before;
return allowed;
}
```
Three things make it work. The plan is copied before an attempt and restored if the attempt fails, so a failed branch leaves no trace (Predict question 4). An object already in the plan answers yes at once, so a chain is visited once and recursion ends. And the function never names a property: everything a property does when something moves is a response, and the planner just runs them in order. That order is the wiki's "OPEN SHUT ... PUSH PULL ... STOP", written down in one list in content.
baba-is-auto has the same two phases: `Game::CanMove` checks a move recursively, including "// Check boundary", before `Game::ProcessMove` and `Game::ProcessPush` change anything ([`Game.cpp`](https://github.com/utilForever/baba-is-auto/blob/2305a3d/Sources/baba-is-auto/Games/Game.cpp), from line 1756). Its PUSH, STOP, text and OPEN/SHUT are branches inside `CanMove`; compare that with responses, and think about what each costs when a new word arrives.
**Run the coupling audit** again: `node tools/coupling-audit.mjs ~/my-engine` should print `coupling-audit: ok`. The planner names no noun: it moves whatever the rules say is `YOU` and runs whatever responses content gives it.
## Expansion Drop
A new word arrives: `PULL`. From the wiki's [PULL](https://babaiswiki.fandom.com/wiki/PULL) page: "PULL also makes the object "solid" for collision checks like STOP does", and "If something directly adjacent from a PULL object moves away from it, the PULL object will try to move in the same direction."
That is new behaviour: PUSH looks at the tile ahead of the mover, PULL at the tile behind it. The constraint, as always: **do not edit anything under `src/engine/`**. The harness doesn't change either. Your word list changes, and so may the rest of your content.
1. Commit your workspace and note the commit: `git add -A && git commit -m "module 8 done"`.
2. Copy the expansion test for your track from `course/08-push-chains/expansion/tests/rust/test_08x.rs` or `course/08-push-chains/expansion/tests/ts/test_08x.test.ts` into your workspace's `tests/`. Run it and watch it fail.
3. Add `PULL` to your word list, and make it do what it says.
4. When the tests pass, run the check from the course repository:
```sh
node tools/expansion-check.mjs ~/my-engine HEAD
```
The test, `test_08x_pull_follows`, puts two PULL rocks behind Baba. Baba moves away and both rocks follow, one tile each. Then Baba turns back: the rock is solid, so nothing moves. The full scenario is in `course/08-push-chains/expansion/README.md`.
<details><summary>Discussion: what this drop measures</summary>
If what a property does when something moves is a response the planner runs, `PULL` is one word in the list and one function in content:
```rust
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
}
```
```ts
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;
}
```
That is the reference answer, in `course/08-push-chains/expansion/solution/`: `words.json` with `PULL` added, and the movement responses with `pull` listed after `push`. The pulled rock's own step runs every response too, so the far rock follows the near one with no extra code.
This is the first Part II drop that is code, not data. PULL is new behaviour, so something has to say what it does. The drop measures where that something has to go.
Shapes that fail this drop:
- **Properties as branches in the planner.** If "can this step?" says `if PUSH ... else if STOP ...`, PULL is a third branch, under `src/engine/`. Make the reactions something the planner runs, as a normal commit, then redo the drop.
- **A planner that only lets a reaction see the tile ahead.** If your responses are handed "the objects on the target tile" and nothing else, PULL can't see the object behind the mover. Give a response the step (where from, where to, which way) and a way to ask about any tile.
- **Moving while you walk.** If your engine moves objects as it finds them, a pull that fails halfway leaves objects moved. With a plan, a pull that can't happen is simply never in the plan.
The reference lets a PULL object that can't follow stay behind without holding the mover back. The PULL page doesn't say, and the test doesn't check it.
> **How the real game differs**
> - "If a PULL object is being PULLed by a PULL movement, it can move through solid objects (though will still PUSH things)" ([PULL](https://babaiswiki.fandom.com/wiki/PULL)). In the reference, a pulled object's step runs every response, so STOP blocks it.
> - "If the same PULL object is being PULLed in multiple ways in the same movement stage, the result is based on update order" (same page). The reference's planner moves an object at most once per plan.
If the check fails, fix your engine (a normal commit, outside the drop), then redo the drop from step 1.
</details>
## Further reading
- The Baba Is You Wiki's [PUSH](https://babaiswiki.fandom.com/wiki/PUSH), [STOP](https://babaiswiki.fandom.com/wiki/STOP), [OPEN](https://babaiswiki.fandom.com/wiki/OPEN) and [SHUT](https://babaiswiki.fandom.com/wiki/SHUT) pages: every rule in this module and the ones it leaves out (SAFE, HAS, float levels). Then [PULL](https://babaiswiki.fandom.com/wiki/PULL) for the drop.
- The [Esolangs wiki's Baba Is You page](https://esolangs.org/wiki/Baba_Is_You): the game read as a cellular automaton, with a table of words. Its PUSH, STOP and OPEN/SHUT entries are the shortest statement of this module.
- The Baba Is You Wiki's [Order of Operations](https://babaiswiki.fandom.com/wiki/Order_of_Operations): the movement checks in order, and where movement sits in the turn you build in Module 9.
- baba-is-auto, `Game::CanMove`, `Game::ProcessMove` and `Game::ProcessPush` in [`Sources/baba-is-auto/Games/Game.cpp`](https://github.com/utilForever/baba-is-auto/blob/2305a3d/Sources/baba-is-auto/Games/Game.cpp) (from line 1756): validate, then commit, in C++. It is an example, **not an authority** on the game: it hard-codes text as pushable and its float handling is partial ([`rule-language.md`](https://github.com/utilForever/baba-is-auto/blob/2305a3d/Documents/rule-language.md)).