Module 9 lesson, scenarios, tests (8, both tracks), reference engines (TURN phase list, conversion, content-registered hazards, win/lost), WEAK Expansion Drop, harness.md M9 semantics, research notes (WEAK page, baba-is-auto Game::MovePlayer, X IS NOT X placement reading). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
30 KiB
Module 9: The turn pipeline
In Module 8, a key press moved things and nothing else happened. This module makes it a whole turn. Things move, the rules are read again, objects turn into other objects, hazards destroy what they touch, and the level is won or lost. Every one of those steps reads the board the step before it left, so the order is part of the rules. The same board, played in a different order, ends differently. This module writes the order down once, as a list of phases, and runs it.
Part I had this too. In Module 3, the end of the outermost Phase ran a fixed sequence of steps: the Death Creation Step, then the Death Phase, then the check again. In Module 4 the sequence grew: "After the outermost Phase ends, Hearthstone does an Aura Update (Health/Attack), then does the Death Creation Step", and another Aura Update follows (Advanced rulebook). In both games, the order of the steps is a decision written down once, in one place.
This is the last Baba Is You module. Module 10 joins the two halves of the course.
You need your Module 8 workspace with all Module 1–8 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.
The wiki's Order of Operations page opens with this:
Within a turn, most properties and verbs take place exactly once. Conditions process when the game checks for what objects have a rule, which is usually right before it uses it.
Then it lists the turn. Shortened, in the course's words:
- Movement (
domovement()): YOU first, then MOVE, AUTO and CHILL, then NUDGE, FEAR and SHIFT. - Rule parsing.
- Conversions: REVERT, and "object transformations via IS and WRITE".
- Rule parsing.
- A second movement (
moveblock()): FOLLOW, BACK, TELE, SHIFT. Then SELECT, the stack limit, and FALL. - Rule parsing.
- Status (
statusblock()): directions, TURN, FLOAT. Then FALL again. - Rule parsing.
- The level block (
levelblock()). block(): SINK, BOOM, WEAK, HOT/MELT, DEFEAT, OPEN/SHUT, EAT, MAKE, then BONUS, END and WIN, then out-of-bounds deletion and CRASH.- Rule parsing.
On block(), the page says: "Deletion occurs after each step that removes objects, but all objects destroyed in the same step are destroyed simultaneously." And the Rule page says when "rule parsing" actually does anything: "Rules parse when any parsable object changes position or is destroyed, or an object is transformed to text. Between parsings, active rules are simply stored."
What the hazards do. All quotes are from the wiki's property pages:
- SINK: "SINK is a property that causes an object to destroy all overlapping same-float objects along with itself when there is at least one overlapping object."
- HOT: "HOT is a property that causes the object to destroy any same-float MELT objects it contacts." and "An object that is both HOT and MELT destroys itself."
- DEFEAT: "If a DEFEAT object intersects with or is a YOU (or YOU2 or 3D), the YOU object will be destroyed."
- FLOAT: "EAT and properties BONUS BOOM DEFEAT END HOLD HOT MELT OPEN SHUT SHIFT SINK TELE WEAK WIN require that the interacting objects are both FLOAT or both not FLOAT."
And winning:
- WIN: "If an object is both WIN and YOU YOU2 3D, the level is won."
- Also from WIN: "WIN is checked very late in the process: any other removal of either object involved will occur first and prevent WIN from WINning."
- DEFEAT: "If an object is both WIN and DEFEAT, then DEFEAT takes priority and the YOU object is destroyed."
Last, conversions. The IS page: "BABA IS BABA prevents BABA from transforming into any object", and "BABA IS NOT BABA causes BABA to vanish in a puff of logic." The NOT page adds that X IS NOT X "negates X IS X, it enables other X IS Y rules, which can result in transformation in addition to the disappearance." Module 7 put these rules in force. This module carries them out.
Now put a board together. BABA IS YOU, LAVA IS HOT, and BABA IS MELT written down a column, with its MELT lying on a lava tile. Baba steps onto the lava and, in the same step, pushes MELT off the end of its sentence. Movement comes first and HOT/MELT comes late, in block(), with several "(rule parsing)" points in between. By the time HOT/MELT runs, BABA IS MELT is gone. Baba survives. No page shows this exact board. The course's research notes mark the outcome as inferred from the documented order, and so does this module's test.
Predict
Write your answers down before you open the discussion, for example in notes/09.md in your workspace.
- The lava board above. Does Baba survive? Would it if your turn checked hazards before reparsing?
BABA ISis written with an empty tile after it. Baba pushes the wordROCKinto that tile. Do the Babas become rocks this turn, or next turn?BABA IS BABAandBABA IS ROCKare both on the board. What does a turn do to Baba?KEY IS NOT KEYandKEY IS ROCKare both on the board. What is on the key's tile after a turn?BABA IS YOU,BABA IS FLOAT,SKULL IS DEFEAT. Baba steps onto a skull. What happens?- Baba pushes a rock into water, and
WATER IS SINK. What is left? FLAG IS WIN AND DEFEAT. Baba steps onto the flag. Won, lost, or neither?- In your engine, where does the order of the turn live? If you had to move hazards before conversion, how many places would you edit?
Discussion
-
Yes. The rules are parsed right after movement, so
BABA IS MELTno longer exists when HOT/MELT runs. That is inferred from the order, not stated by a source. A turn that checks hazards with the rules from before the move destroys Baba. The rules didn't change; the order did. -
This turn. Conversion comes after the parse that follows movement, so the rule the move completed is already in force. The Babas move first, then become rocks where they stand. (With no
ROCK IS YOU, nothing isYOUany more, and the level is lost.) -
Nothing.
BABA IS BABAblocks every conversion out of Baba. Module 7's resolution pass already tookBABA IS ROCKout of the active rules. -
A rock, and no key.
KEY IS NOT KEYdeletes the key. It also cancelsKEY IS KEY, so nothing blocksKEY IS ROCK, and a rock appears where the key was: "transformation in addition to the disappearance". Module 7's Predict question 3 asked which comes first. In the course they happen in the same step: the key is deleted, and its rock is made as a new object. -
Nothing. Baba is FLOAT and the skull isn't, so they are on different float levels, and DEFEAT needs "the interacting objects are both FLOAT or both not FLOAT".
-
Only Baba. The pushed rock lands on the water, and SINK destroys "all overlapping same-float objects along with itself". Baba was never on the water's tile.
-
Lost. DEFEAT runs before WIN, and "any other removal of either object involved will occur first and prevent WIN from WINning". Baba is destroyed, so no YOU object is left to win.
-
If the order is spread across functions that call each other (move, which reparses and then calls convert, which calls hazards), moving a phase means untangling those calls. If it is a list, it is one edit to the list. Question 1 shows why that matters: the game's order is a long list (see "How the real game differs"), and the course builds a short version of it.
Spec
From the course repository, copy the module's tests into your workspace:
node tools/start-module.mjs 09 ~/my-engine
The harness
Three new functions on Baba:
| TypeScript | Rust |
|---|---|
step(dir): void |
step(&mut self, dir: Dir) |
won(): boolean |
won(&self) -> bool |
lost(): boolean |
lost(&self) -> bool |
step(dir)plays one whole turn, in the course's order: Movement → Parse → Conversion → Parse → Hazards → Win, then one last parse, so thatrules()matches the board. Movement is Module 8's, without its reparse. Module 8'smove(dir)stays as it was.won()is true once a turn's win check has found aYOUobject that isWIN, or that shares its tile with aWINobject on the same float level. It stays true.lost()is true when no object isYOU.
What the phases do:
- Conversion. An object named by
X IS Y, whereYis a noun, becomes aYwhere it stands, with the same id.X IS Xblocks it. An object named byX IS NOT Xis deleted. Every object is decided from the board as movement left it. - Hazards, in this order, each one against the objects sharing a tile on the same float level, and each one destroying everything it finds at once, before the next runs:
- SINK destroys every object it shares a tile with, and itself.
- HOT destroys MELT objects, and an object that is both destroys itself.
- DEFEAT destroys YOU objects, and an object that is both destroys itself.
- Win runs after every hazard.
New words: HOT, MELT, SINK, DEFEAT, FLOAT.
The tests
The full statements are in course/09-turn-pipeline/scenarios.md.
test_09_lava_saved_by_reparse: Predict question 1. Baba stands on the lava and survives. The outcome is the course's inference.test_09_mid_turn_conversion: Predict question 2. Both Babas are rocks after the turn, on the tiles they moved to.test_09_identity_lock_blocks_conversion: Predict question 3, with a key underKEY IS ROCKto show that conversion works.test_09_self_negation_deletes: Predict question 4.test_09_float_separates_hazards: Predict question 5, with a key that isYOUand notFLOATstepping onto a second skull. The key is destroyed.test_09_sink_destroys_both: Predict question 6.test_09_you_and_win:BABA IS YOUandBABA IS WIN. A turn wins the level; placing the text doesn't.test_09_defeat_beats_win: Predict question 7.
The first test:
#[test]
fn test_09_lava_saved_by_reparse() {
let mut b = new_baba(empty_level(6, 5));
write(&mut b, &[("BABA", 0, 0), ("IS", 1, 0), ("YOU", 2, 0)]);
write(&mut b, &[("LAVA", 0, 1), ("IS", 1, 1), ("HOT", 2, 1)]);
write(&mut b, &[("BABA", 4, 2), ("IS", 4, 3)]);
let melt_text = b.place("text:MELT", 4, 4);
let lava = b.place("lava", 4, 4);
let baba = b.place("baba", 3, 4);
assert!(b.has_property(baba, "MELT"));
b.step(Dir::Right);
assert_eq!(b.pos_of(baba), Some((4, 4)));
assert_eq!(b.pos_of(melt_text), Some((5, 4)));
// Inferred from the order of operations, not stated by a source: Baba survives.
assert_eq!(b.pos_of(lava), Some((4, 4)));
assert_eq!(b.kinds_at(4, 4), vec!["baba", "lava"]);
assert!(!b.rules().contains(&"BABA IS MELT".to_string()));
assert!(!b.lost());
}
test("test_09_lava_saved_by_reparse", () => {
const b = newBaba(emptyLevel(6, 5));
write(b, [
["BABA", 0, 0],
["IS", 1, 0],
["YOU", 2, 0],
]);
write(b, [
["LAVA", 0, 1],
["IS", 1, 1],
["HOT", 2, 1],
]);
write(b, [
["BABA", 4, 2],
["IS", 4, 3],
]);
const meltText = b.place("text:MELT", 4, 4);
const lava = b.place("lava", 4, 4);
const baba = b.place("baba", 3, 4);
expect(b.hasProperty(baba, "MELT")).toBe(true);
b.step("right");
expect(b.posOf(baba)).toEqual([4, 4]);
expect(b.posOf(meltText)).toEqual([5, 4]);
// Inferred from the order of operations, not stated by a source: Baba survives.
expect(b.posOf(lava)).toEqual([4, 4]);
expect(b.kindsAt(4, 4)).toEqual(["baba", "lava"]);
expect(b.rules()).not.toContain("BABA IS MELT");
expect(b.lost()).toBe(false);
});
Run cargo test or npm test. The Module 9 tests fail to compile (Rust) or fail (TypeScript). Modules 1–8 still pass. That is RED.
Build
Make the eight tests pass, and keep Modules 1–8 green. Where code goes:
- The turn is a list. Write the order down once, as a list of phases, and have
stepwalk it. Movement doesn't call parsing, and parsing doesn't call conversion. Each phase is called by the turn. - Parse between phases. Module 8's
movereparsed at the end. In the turn, parsing is a phase of its own. It runs after movement and again after conversion, and each phase uses the rules from the parse before it. Hazards and the win check share one set of rules: a hazard that destroys text doesn't change the rules until the turn's last parse. - Conversion: decide, then change. Decide what every object becomes from the board as it stands, then apply all of it. Otherwise
BABA IS ROCKandROCK IS BABAtogether would turn Baba into a rock and the new rock straight back into Baba. An object keeps its id and its tile and changes its kind. Remember Module 7:X IS Xlocks, andX IS NOT Xdeletes. - Hazards: destroy together, then the next. "All objects destroyed in the same step are destroyed simultaneously." Collect a hazard's victims first, then remove them. An object that is HOT and MELT destroys itself, and a second MELT object on its tile goes with it. Remove the first as soon as you find it, and the second no longer has HOT company, so it survives.
- Float levels. Every hazard and the win check ask the same question: which other objects share this tile, on this float level? Answer it in one place.
- Win last, lost any time. The win check runs after the hazards, so a destroyed YOU can't win.
lost()just asks whether anything is YOU now. - One more question. In Module 8, each movement property was something the planner ran. Is each hazard a branch in your hazards phase, or something the phase runs? The Expansion Drop brings a new hazard, and you won't be allowed to touch
src/engine/.
Hint 1 — nudge
Write step as a loop over a constant list: [movement, parse, conversion, parse, hazards, win, parse]. Then write each phase as a function that does one thing to the board, using the rules it finds there. The tests then mostly check one phase each.
Hint 2 — concept
- Split Module 8's
move. Movement without the reparse becomes the first phase, andmovebecomes "movement, then parse", so the Module 8 tests keep passing. - Conversion is a plan too. For each object, list the active rules that name it. If one of them is
X IS X, the object is locked. If one isX IS NOT X, it is deleted. Otherwise eachX IS YwithYa noun is a target. Record the changes, then apply them: remove, or change the kind in place. - A hazard is a function from the level to a list of ids. SINK: every SINK object with company on its tile, plus the company. HOT/MELT: every MELT object that is HOT or has HOT company. DEFEAT: every YOU object that is DEFEAT or has DEFEAT company. The hazards phase runs them in order and removes each list before running the next.
- "Company" means same float level. One function,
sharing(id), returns the other objects on the tile whose FLOAT matches this one's. The hazards and the win check all use it. - Win is: some YOU object is WIN, or has WIN company.
Hint 3 — pseudo-code
TURN = [movement, parse, conversion, parse, hazards, win, parse]
step(dir):
for phase in TURN: run phase
conversion():
changes = []
for o in objects:
rules = active rules whose subject names o
if some rule in rules is X IS X: continue // locked
deleted = some rule in rules is X IS NOT X
into = distinct noun complements of X IS Y in rules, other than o's own kind
if deleted or into: changes += (o, deleted, into)
for (o, deleted, into) in changes:
if deleted: remove o; place each of into on o's tile
else: o.kind = into[0]; place each of into[1..] on o's tile
sharing(id) = other objects on id's tile with has(FLOAT) == has(id, FLOAT)
hazards():
for hazard in content.hazards: // sink, hot_melt, defeat
remove every id in hazard(level)
sink(level) = for s with SINK: if sharing(s) not empty: s and sharing(s)
hot_melt(level) = m with MELT where m is HOT or some o in sharing(m) is HOT
defeat(level) = y with YOU where y is DEFEAT or some o in sharing(y) is DEFEAT
win(): if some y with YOU is WIN or has some WIN o in sharing(y): won = true
lost(): no object is YOU
When your tests pass, compare with the reference engine in course/09-turn-pipeline/reference/rust/src/ or course/09-turn-pipeline/reference/ts/src/. Look at the new engine/puzzle/turn (the phases and TURN), engine/puzzle/level (step, convert, sharing, the hazards phase and the win check), and the new content/hazards.
Decisions in the reference that the tests don't force:
- The turn is a constant in the engine.
TURNlists the phases andstepwalks it. The order is the engine's because it is the game's rules for a turn, not any one word's. - The hazards are content. SINK, HOT/MELT and DEFEAT are three functions in
src/content/. The engine runs them in the order content lists them, which is the wiki'sblock()order, and removes each one's victims before running the next. This is the same shape as Module 8's movement responses. - The engine knows
YOU,WINandFLOAT.YOUpicks who moves (Module 8).WINis the engine's own end-of-turn check.FLOATis in the engine because it isn't a hazard. It decides which objects share a tile at all, and the win check needs that as much as the hazards do ("a same-float YOU YOU2 3D object", WIN). If each hazard filtered by FLOAT itself, a new hazard could forget to, and the engine's win check would need the rule anyway. So it lives in one place,sharing, and hazards can't get it wrong. - Several conversions. "if you write A IS B and A IS C simultaneously in the same turn ... then you get both a B and a C for each A" (Advanced rulebook). The object becomes the first target and keeps its id; each other target is a new object on its tile.
X IS NOT Xdeletes in the conversion step, together with the conversions. The deleted object's other targets still appear, as new objects. No source gives the deletion its own place in the turn; see "How the real game differs".won()stays true. In the game, winning ends the level. In the course,stepkeeps playing if you call it again, andwon()stays true.- The last parse. The course's turn ends with a parse, as the game's does, so
rules()afterstepmatches the board. No phase reads it until the next turn.
How the real game differs
The order. The course's turn is a labelled simplification. Side by side, the course on the left and the game, from Order of Operations, on the right:
Course Game Movement (YOU) Movement: YOU, then MOVE/AUTO/CHILL, NUDGE, FEAR, SHIFT, with OPEN/SHUT, WEAK/EAT, SWAP, PUSH/PULL and STOP checked on every step Parse Rule parsing Conversion (IS, and X IS NOT X) Conversions: REVERT, IS, WRITE ("X IS ALL takes the lowest priority") Parse Rule parsing Second movement: FOLLOW, BACK, TELE, SHIFT; then SELECT, the stack limit, FALL Rule parsing Status: directions, TURN, FLOAT; then FALL Rule parsing Level block Hazards: SINK, HOT/MELT, DEFEAT block(): SINK, BOOM, WEAK, HOT/MELT, DEFEAT, OPEN/SHUT, EAT, MAKEWin BONUS, END, WIN (the tail of block()), then out-of-bounds deletion, CRASHParse Rule parsing When parsing happens. The game reparses only "when any parsable object changes position or is destroyed, or an object is transformed to text" (Rule). The course reparses at every Parse phase. The result is the same; the game does less work.
FLOAT updates once per turn. "Whether an object is FLOATing or not updates once per turn, or at object creation (including transforms)" (FLOAT), in the status block. The course reads FLOAT from the rules in force whenever a hazard asks.
Words the course doesn't have. BOOM, WEAK (the Expansion Drop), EAT, MAKE, BONUS and END in
block(); SAFE, which protects from SINK ("Overlapping objects are destroyed (unless SAFE)", SINK) and from MELT (MELT); HAS, "checked after every standard destruction" (Order of Operations); and the overlap half of OPEN/SHUT ("If an OPEN object is on ... a same-float SHUT object", OPEN), which the course checks only during movement.Exceptions to DEFEAT beating WIN. The research notes record three, from the WIN page: an object that is YOU, DEFEAT, WIN and SAFE wins; a YOU object respawned by HAS can still win in the same turn; and
LEVEL IS WIN AND DEFEATwins. The course has no SAFE, HAS or LEVEL.Conversions. The course converts only into nouns that name their own things: no
X IS TEXT, noX IS ALL, no REVERT or WRITE. In the game, a stacked noun inA IS Bparses more than once, which duplicates objects (research notes, from the Advanced rulebook). The course makes one object per distinct target.Where
X IS NOT Xdeletes. The NOT page says X IS NOT X "makes an object disappear" and enables "transformation in addition to the disappearance", but no source read gives the deletion its own place in the turn. Putting it in the conversion step is the course's reading.The lava board is inferred. Baba surviving follows from the documented order. No source shows it.
lost()is the course's definition, "no YOU object remains". No source read describes what the game does when nothing is YOU.
Name it
The pattern is a phase pipeline: an ordered list of phases, each of which reads the state the previous one left. The list is data, so the order is visible in one place and changes in one place. Each phase does one job and doesn't call the next. The bugs this prevents are ordering bugs: a hazard that runs before the rules are re-read, a win that is checked before a DEFEAT. In a pipeline, each of those is a position in a list that you can read.
The reference's turn:
/// The turn, in order.
pub const TURN: [Phase; 7] = [
Phase::Movement,
Phase::Parse,
Phase::Conversion,
Phase::Parse,
Phase::Hazards,
Phase::Win,
Phase::Parse,
];
/// Play one turn: the phases of `TURN`, in order.
pub fn step(&mut self, dir: Dir) {
for phase in TURN {
match phase {
Phase::Movement => self.movement(dir),
Phase::Parse => self.reparse(),
Phase::Conversion => self.convert(),
Phase::Hazards => self.run_hazards(),
Phase::Win => self.check_win(),
}
}
}
/** The turn, in order. */
export const TURN: readonly Phase[] = ["movement", "parse", "conversion", "parse", "hazards", "win", "parse"];
/** Play one turn: the phases of `TURN`, in order. */
step(dir: Dir): void {
for (const phase of TURN) {
switch (phase) {
case "movement":
this.movement(dir);
break;
case "parse":
this.reparse();
break;
case "conversion":
this.convert();
break;
case "hazards":
this.runHazards();
break;
case "win":
this.checkWin();
break;
}
}
}
Two levels of list are at work. The engine owns the turn, and content owns what happens inside the hazards phase: a list of hazards, each a function from the level to the objects it destroys. Module 8 had the same split. The engine owned the planner, and content owned the responses.
baba-is-auto writes its turn the same way, as a fixed run of calls in Game::MovePlayer (Game.cpp, line 495):
ProcessPlayerMove, thenParseRules,ProcessDirectionPropertiesandProcessMoveProperty.ParseRules,ProcessTransformations, andParseRulesagain.- The hazards:
ProcessSink,ProcessWeak,ProcessHotMelt,ProcessDefeatandProcessOpenShut. CheckPlayState, which sets won or lost, and a lastParseRules.
Each hazard there is a named function in the engine, and none of them checks FLOAT. Its own rule-language.md says its FLOAT "is not applied to the other overlap/effect properties". Compare that with hazards as content, and think about what each costs when a new hazard arrives. As in Module 8, baba-is-auto is an example, not an authority.
Run the coupling audit again: node tools/coupling-audit.mjs ~/my-engine should print coupling-audit: ok. The turn names no noun. Conversion turns whatever the rules name into whatever they say.
Expansion Drop
A new word arrives: WEAK. From the wiki's WEAK page: "If a WEAK object overlaps any same-float object, it is destroyed." It runs among the hazards: "Destruction by WEAK happens after SINK BOOM, but before other overlap checks."
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.
- Commit your workspace and note the commit:
git add -A && git commit -m "module 9 done". - Copy the expansion test for your track from
course/09-turn-pipeline/expansion/tests/rust/test_09x.rsorcourse/09-turn-pipeline/expansion/tests/ts/test_09x.test.tsinto your workspace'stests/. Run it and watch it fail. - Add
WEAKto your word list, and make it do what it says. - When the tests pass, run the check from the course repository:
node tools/expansion-check.mjs ~/my-engine HEAD
The test, test_09x_weak_breaks, has two WEAK rocks. Baba steps onto one, which breaks; the other, alone on its tile, stays. The full scenario is in course/09-turn-pipeline/expansion/README.md.
Discussion: what this drop measures
If each hazard is a function the hazards phase runs, WEAK is one word in the list and one function in content, listed after sink:
/// A WEAK object that shares its tile with anything is destroyed.
fn weak(level: &Level) -> Vec<ObjectId> {
with(level, "WEAK")
.into_iter()
.filter(|&id| !level.sharing(id).is_empty())
.collect()
}
/** A WEAK object that shares its tile with anything is destroyed. */
function weak(level: Level): ObjectId[] {
return withProperty(level, "WEAK").filter((id) => level.sharing(id).length > 0);
}
That is the reference answer, in course/09-turn-pipeline/expansion/solution/: words.json with WEAK added, and the hazards with weak between sink and hot_melt. Because sharing already applies float levels, weak gets them for free. Two WEAK objects on one tile both break, with no extra code.
Shapes that fail this drop:
- Hazards as branches in the turn. If the hazards phase says
if SINK ... if MELT ... if DEFEAT ..., WEAK is a fourth branch, undersrc/engine/. Make hazards something the phase runs, as a normal commit, then redo the drop. - Destroying while scanning. If a hazard removes objects as it finds them, two WEAK rocks on one tile go wrong. The first has company and is removed; then the second is alone, and survives. The WEAK page says "Multiple same-float WEAK objects overlapping each other are all destroyed." Collect, then remove.
- Float level in each hazard. If every hazard filters by FLOAT itself, WEAK has to remember to. This test doesn't catch it if it forgets. The next FLOAT puzzle would.
How the real game differs
- WEAK also breaks on collision: "If a WEAK object attempts to move into a solid object and fails (e.g. STOP PULL or blocked PUSH), regardless of float levels, it is destroyed", and "If an object attempts to move into a same-float WEAK solid object that is not PUSHable, the movement is not blocked" (WEAK). Those are movement checks, which would be movement responses in the course. The drop leaves them out.
- BOOM runs between SINK and WEAK in the game. The course has no BOOM.
If the check fails, fix your engine (a normal commit, outside the drop), then redo the drop from step 1.
Further reading
- The Baba Is You Wiki's Order of Operations: the whole turn. Read it with this module's turn beside it and mark each line the course leaves out.
- The wiki's SINK, HOT, MELT, DEFEAT, WIN and FLOAT pages: every hazard in this module, and the exceptions it leaves out (SAFE, HAS, LEVEL). Then WEAK for the drop.
- baba-is-auto,
Game::MovePlayerinSources/baba-is-auto/Games/Game.cpp(line 495), and the hazard functions andCheckPlayStateafter it. It is a turn as a fixed list of calls, in C++. It is an example, not an authority on the game: its own docs list where it diverges (rule-language.md).