feat: Stress it beat for Modules 6-10, Module 11 (Break your own engine), level solver tool
- lessons 8 and 9 updated for the once-per-move rule, per-object negated conversion and the new ordering tests - tools/solve-spellgrid.mjs: breadth-first search over a SpellGrid level with the TypeScript reference; --never-rule and --naive - README, harness.md and the mentor skill know the new beat and module Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
1 parent
e907f17937
commit
4cb0f3a61f
13 files changed
+771
-19
No files matched your search
@@ -337,6 +337,69 @@ If the check fails, fix your engine (a normal commit, outside the drop), then re
|
||||
|
||||
</details>
|
||||
|
||||
## Stress it
|
||||
|
||||
Optional. Each question pushes the design past what this module's tests check. Predict first, then open the discussion.
|
||||
|
||||
1. One row: `BABA IS ROCK IS PUSH`. How many rules does your parser read, and which? Then one row: `BABA AND IS YOU`, and one row: `BABA IS YOU AND`.
|
||||
|
||||
<details><summary>Discussion</summary>
|
||||
|
||||
`BABA IS ROCK IS PUSH` gives two rules in the reference: `BABA IS ROCK` and `ROCK IS PUSH`. Each `IS` reads outwards on its own, and the word `ROCK` is the complement of the first and the subject of the second. Nothing in "start at every `IS`" stops two sentences sharing a word end to end, just as the crossword test shares an `IS` at right angles. Whether the real game reads this row the same way is not in the course's sources; if you care, try it in the game.
|
||||
|
||||
`BABA AND IS YOU` spells nothing: the tile before `IS` holds no noun, so the subject run is empty. `BABA IS YOU AND` is just `BABA IS YOU`: a trailing `AND` continues the run onto a tile with nothing that fits, and the run keeps what it had. These are the cases a grammar written as three lines doesn't mention and a parser has to decide anyway. Write a test for each decision you made, even if the decision was "whatever my loop happened to do".
|
||||
|
||||
</details>
|
||||
|
||||
2. Without running anything: if you slide every object on a board three tiles right and two down (on a level big enough to hold it), do the rules change? If you swap every object's x and y? If you mirror the board left to right?
|
||||
|
||||
<details><summary>Discussion</summary>
|
||||
|
||||
Sliding: no. Nothing in the grammar mentions an absolute position. Swapping x and y: no, and this one is less obvious. Rows become columns and columns rows, but rows read left to right and columns top to bottom, so every sentence still reads in its forward direction. Mirroring: yes. `BABA IS YOU` becomes `YOU IS BABA`, which spells nothing.
|
||||
|
||||
Each "no" is a test you can run on boards nobody wrote. Scatter random words on a random board, compute `rules()`, apply the change, compute again, compare:
|
||||
|
||||
```text
|
||||
rules(board) == rules(slide(board, dx, dy))
|
||||
rules(board) == rules(swapXY(board))
|
||||
```
|
||||
|
||||
This is **metamorphic testing**: you don't know the right answer for a random board, but you know how the answers for two related boards must relate. A metamorphic relation is a necessary property "of the intended functionality" that "must involve multiple executions of the software" ([Metamorphic testing, Wikipedia](https://en.wikipedia.org/wiki/Metamorphic_testing)). The swap relation is a sharp one for this module, because a parser that handles rows and columns in two separate pieces of code can easily be right in one and wrong in the other, and no hand-written test compares them. Module 1's seeded generator is all you need. [Module 11](../11-break-your-engine/lesson.md) collects the relations that hold across the course.
|
||||
|
||||
</details>
|
||||
|
||||
3. Three tiles in a row hold two stacked nouns, one `IS`, and three stacked properties. How many rules? Now *k* words on each of the three tiles of a sentence with one `AND`-free subject and complement. Why did Teikari call stacking "one of the larger undertakings"?
|
||||
|
||||
<details><summary>Discussion</summary>
|
||||
|
||||
Six, then *k* × *k* (and *k* stacked `IS` words would each spell all of them again, as separate rules with different text ids). Add `AND` and the count multiplies along the run. The parser's work grows with the number of rules it emits, and stacked text makes that number a product, not a sum.
|
||||
|
||||
For the course's parser this costs nothing to *write*: Hint 2's "stacking is free" is true because a tile was a list from the start. That is the real lesson of the quote. Teikari was reworking a rule system in which a tile held one word, and "the need to go through all the possible permutations" arrived late. The decision that a tile holds a list is cheap on day one and expensive afterwards, like most decisions about the shape of data.
|
||||
|
||||
</details>
|
||||
|
||||
4. The reference reparses on every change to the board. The real game reparses only "when any parsable object changes position or is destroyed, or an object is transformed to text" ([Rule](https://babaiswiki.fandom.com/wiki/Rule)). Add that optimisation to your engine. How do you convince yourself it never changes a result?
|
||||
|
||||
<details><summary>Discussion</summary>
|
||||
|
||||
The optimisation is a flag: set it when a text object is placed, moved, removed or created; reparse only if it is set. The argument that it is safe is that `parse` reads text objects and nothing else, so a change that touches no text object can't change its output.
|
||||
|
||||
An argument is not a test. The test is to keep both: build one level that always reparses and one that uses the flag, feed them the same random sequence of changes, and compare `rules()` after every change. Two implementations of one specification checking each other is **differential testing**, and it is the cheapest way to make an optimisation safe: the slow, obviously correct version is the oracle for the fast one. (It is also why this course ships its reference in two languages.)
|
||||
|
||||
Then notice what the flag costs. "Reparse on every change" can't be wrong. "Reparse when text changed" is right only as long as every way text can change remembers to set the flag, and Module 9 adds conversion and Module 10 adds text written by a Deathrattle. The reference never took the optimisation, for that reason.
|
||||
|
||||
</details>
|
||||
|
||||
5. Baba Is You compiles its rules from the board. [PuzzleScript](https://www.puzzlescript.net/), a small language for making puzzle games, has the author write the rules down as patterns: "The engine looks for the pattern on the left, and replaces it with the one on the right." ([Rules 101](https://www.puzzlescript.net/Documentation/rules101.html)). Pushing a crate is the rule `[ > Player | Crate ] -> [ > Player | > Crate ]`. What does each design make easy, and what does each make impossible?
|
||||
|
||||
<details><summary>Discussion</summary>
|
||||
|
||||
In PuzzleScript the rules are fixed for the whole game and the board is only data; in Baba Is You the board *is* the rule source. PuzzleScript can express a rule in one line that this course needs a module for (Module 8 is that crate rule), and its author can invent mechanics without touching an engine. It can't let the player change a rule, because rules aren't objects in the level.
|
||||
|
||||
The two share more than they differ on. Both separate the rule table from the code that applies it. Both have to say in what order rules apply: PuzzleScript's documentation is blunt about it, "order does matter - these aren't abstract replacement rules floating around in a vacuum" ([Execution Order](https://www.puzzlescript.net/Documentation/executionorder.html)), which is Module 9 in one sentence. Reading its docs after this module is the fastest way to see which of your design choices were about Baba Is You and which were about rule engines in general.
|
||||
|
||||
</details>
|
||||
|
||||
## Further reading
|
||||
|
||||
- The Baba Is You Wiki's [Rule](https://babaiswiki.fandom.com/wiki/Rule) and [AND](https://babaiswiki.fandom.com/wiki/AND) pages: reading directions, stacking, base rules, when the game reparses, and AND's edge cases. Read the AND table against your parser. Then [TEXT](https://babaiswiki.fandom.com/wiki/TEXT) and [PUSH](https://babaiswiki.fandom.com/wiki/PUSH) for `TEXT IS PUSH` as a base rule.
|
||||
|
||||
Reference in new issue
Block a user