clone

a browser grid puzzle game where you push words around to rewrite the rules of each level

main
miau-is-true / docs / worlds / crate-works.md
crate-works.md 20.7K rendered
1# Crate Works (world bible)23Pack: [`mods/worlds/crate-works.cjs`](../../mods/worlds/crate-works.cjs), id `crate-works`, 7 levels.45World identity (declared in `manifest.world`; `loadMod` accepts the field today6without validating it, see F-CW8):78```js9world: { glyph: '📦', tagline: 'Every crate goes somewhere. So does every word.', accent: 'push' }10```1112## Theme1314A loading dock after hours. The cat works the night shift: crates have to be15shoved out of doorways, gaps in the dock floor have to be filled before16anything can cross, and the only way out is the truck. The rule tiles are17treated as cargo too. They block corridors, they sink into gaps, and18sometimes the word you need is sitting inside a rule you're still using.1920| Noun | Glyph | Role |21| --- | --- | --- |22| `MIAU` | 🐈 (`faces: 'left'`, profile) | The player |23| `CRATE` | 📦 | Cargo. Usually `PUSH`, sometimes `YOU`, sometimes nothing |24| `TRUCK` | 🚚 | The goal (`WIN`). In one level it is also `PUSH` |25| `GAP` | 🚧 | A hole in the dock floor (`SINK`) |26| `WALL` | slab (the renderer ignores the palette glyph for `WALL`) | Racking. Always `STOP`, always sealed away from the player |2728The glyphs are plain strings except the cat. 🚚 is drawn in profile too, but29it only ever moves by being pushed, and pushing doesn't change facing, so it30doesn't declare `faces`.3132## Mechanic3334Sokoban pushing, played through the core vocabulary only (`YOU PUSH STOP WIN35SINK`, `AND`, and noun-to-noun transforms). Three ideas carry the world:36371. **Gaps are Sokoban goals that eat the box.** Any pushable thing that enters38 a `GAP IS SINK` cell disappears along with the gap and leaves floor behind.39 That makes "push every box into a hole" levels possible, which is the40 closest the core engine gets to real Sokoban.412. **Words are cargo.** Words are always `PUSH`, so they fill gaps too.42 Spending a word removes its rule, so *when* you spend it matters.433. **The goal can move.** `TRUCK IS PUSH AND WIN` means you can never stand on44 the truck. The only way to share a cell with it is to push it onto a second45 `YOU` that is held still by a wall. That's the core-engine version of46 "box on goal".4748## Levels4950Solver: `node tools/solve.mjs mods/worlds/crate-works.cjs --max 2000000`51(all 7 SOLVED, no BYPASS, no TRIVIAL). Cross-checked with a compact52re-implementation of the core rules (differentially tested against53`src/engine.js`) that also verifies the key rule is *necessary*: forbidding54`CRATE IS YOU` makes `in-step` and `back-it-up` unsolvable, and forbidding55`GAP IS CRATE` makes `night-shift` unsolvable.5657| # | id | Title | Category | Board | Teaches | Solver steps | Flags | Intended insight |58| --- | --- | --- | --- | --- | --- | --- | --- | --- |59| 1 | `crates-move` | Crates move | required | 13×7 | `CRATE`, `TRUCK`, `PUSH` | 15 | none | The crate fills the only doorway. Push it through, then walk around it and push `WIN` down into `TRUCK IS _`. |60| 2 | `fill-the-gap` | Fill the gap | required | 12×7 | `GAP`, `SINK` | 17 | none | Every rule is on the far side of the gap line. Sink the crate to make a one-cell bridge, then carry `WIN` into `TRUCK IS _`. |61| 3 | `spend-it-last` | Spend it last | required | 14×5 | sinking words, order | 34 | none | The moat is two gaps deep. You have one crate and one loose word, `PUSH`, and it's holding up `CRATE IS PUSH`. Sink the crate first while it's still pushable, then spend `PUSH` on the second gap. Moving `PUSH` first strands the crate. |62| 4 | `in-step` | In step | required | 11×9 | `AND`, shared `YOU` | 31 | none | Build `CRATE AND MIAU IS YOU`. The crate climbs a zigzag lane to the truck while the cat moves in lockstep in its own room. The cat has to reposition by using moves the crate's walls absorb, and must never shove the sentence it's standing under. |63| 5 | `load-order` | Load order | optional | 12×7 | push order in a tight room | 32 | none | `WIN` starts in front of both crates, next to two gaps. Pushing straight sinks `WIN`. Park `WIN` aside, sink crate one, carry crate two over the first bridge into the second gap, then bring `WIN` across into the vertical `TRUCK IS _`. |64| 6 | `back-it-up` | Back it up | optional | 13×8 | pushable `WIN` | 20 | none | `TRUCK IS PUSH AND WIN` is sealed, so you can't stand on the truck. Put `CRATE` into `_ IS YOU`, then push the truck onto the crate while a wall holds the crate still. The shortest line walks the crate to the truck in lockstep; the longer Sokoban line pushes the truck into the crate's pocket first. Both use the same idea. |65| 7 | `night-shift` | Night shift | challenge | 12×9 | noun transform | 48 | none | The truck is behind a moat two gaps thick and there's no crate. The only loose word is `CRATE`, taken out of `CRATE IS PUSH`. Walk it through the racking (a word-Sokoban with one forced loop) into `GAP IS _`. Every gap becomes a crate, and since `CRATE IS PUSH` is broken those crates don't block you, so you walk straight through. Two non-obvious steps: turn the hazard into cargo, and dismantle the crate rule to get the word. |6667Solutions (from `tools/solve.mjs`, shortest):6869```70crates-move RRRRRRRURRDDRDR71fill-the-gap DRRRRRRDLDDRURRRR72spend-it-last LLLLURULLLLDRRRRRRRRULLLLLLLLLLLLL73in-step ULLUUUDDRUUDDDDRRRRUULDDLUURRUU74load-order UURRRDULLDRLDDRULURRLDDRULURRRRD75back-it-up RRURDDDRUULLDLLLLULL76night-shift UUUURRRLLLDDDDRRRRUUUUUDLLLLUURRRRRRLLDDRRDDDRRR77```7879Arc: 1 to 3 teach push, sink, and "words are cargo", in that order. 4 adds80the second `YOU`. 5 is the pure push-order twist. 6 and 7 combine an earlier81idea with an edit players don't expect: move the goal instead of the player,82and turn the hazard into cargo.8384## Design notes8586- **Sealing the rules costs rows.** Nothing in the core can make a word87 immovable, and walls only block if `WALL IS STOP` stays intact. Every level88 keeps its fixed rules in a sealed "office": a word row with a full wall row89 under it, or the top row packed with words and walls, since the board edge90 stops upward pushes. Two early drafts were broken this way. `load-order` v191 solved in 15 moves by pushing `STOP` up out of `WALL IS STOP` from the room92 below. `spend-it-last` v1 used gaps as row-0 filler and was solved by93 sinking a word into one and then pushing `GAP IS SINK` sideways into the94 next (F-CW2).95- **Pinning tricks used.** A word in row 0 can't be pushed up, and row 0 is96 unreachable if every cell in it is a word or a wall. A word in row 1 with97 walls to its left and right, under a row-0 word, is also pinned. That's how98 `GAP / IS / _` in `night-shift` and `CRATE / IS / PUSH` in `spend-it-last`99 give the player exactly one editable slot.100- **Material counting.** Every pushable thing is gap filler, words included.101 A moat has to be thicker than the number of loose words plus crates, or it102 can be crossed by spending words. `night-shift` relies on this: one loose103 word and a two-deep moat.104- **Accidental "X IS WIN".** The first draft of `crates-move` was won by105 pushing `WIN` onto `CRATE IS PUSH`, which made `CRATE IS WIN`. Any loose106 `WIN` next to a sentence ending in a property is a shortcut risk.107- **Lockstep reads as a puzzle.** In `in-step` the solver found a line where108 the cat uses the crate's blocked moves to reposition. That's the intended109 insight, and it isn't obvious from the board. The hint points at it110 ("A wall holds one still while the other moves").111- **Passable crates look identical to pushable ones.** After `night-shift`112 turns the gaps into crates, those crates have no properties and can be113 walked through, but nothing on the board shows it (F-CW13).114115## Goal feature spec: a real Sokoban goal116117**Problem.** Real Sokoban is won when *every* box is on a goal square. The core118can only express "a YOU shares a cell with a WIN thing". Crate Works gets119around this three ways: holes that eat boxes, moving the WIN thing onto a120parked YOU, and making the crate the YOU. None of them expresses "all boxes121home". Multi-box levels can't have a win state that depends on where the122boxes are.123124**Proposal: a new core property `GOAL`.**125126- Syntax: an ordinary predicate, so `DOCK IS GOAL`, `GAP AND DOCK IS GOAL`, and127 `DOCK IS GOAL AND STOP` all parse with the existing rule code. The tile uses128 the `win` property colour family and is outlined, not filled, so it reads as129 "a win condition" but not as `WIN`.130- **Win check** (end of turn, in `resolveContacts`, after SINK/DEFEAT removal131 and alongside the existing WIN check): the level is won if132 1. at least one `YOU` exists, and133 2. at least one `GOAL` thing exists, and134 3. every `GOAL` thing shares its cell with at least one *other* entity that135 is currently `PUSH` (object or word).136- `WIN` keeps working. Either condition wins.137- Why "covered by a PUSH thing" and not "covered by a CRATE": it has no extra138 parameters, and it composes with rules. Breaking `CRATE IS PUSH` stops139 crates from counting. Words count, because words are always `PUSH`, so a140 word parked on a dock is a legal cover. `BOX IS PUSH` lets any noun become141 cargo.142- Edge cases: the check never passes vacuously (condition 2). A `GOAL` thing143 that is also `PUSH` needs a *second* PUSH entity on it. `SINK` removes144 covers before the check runs. Transforms apply before the check.145- Engine cost: no new state, because it's derived from positions and rules.146 Undo, reset, sharing, and the solver hash need no changes. `settled()`147 should report `goals: { covered, total }` so the HUD or ledger can show148 `DOCK IS GOAL 2/3`.149- Rejected alternative: a level-data predicate such as150 `goal: { every: 'CRATE', on: 'DOCK' }`. It's invisible on the board, so151 players can't read it or edit it. It needs a new metadata schema and a152 separate UI. It also can't take part in rule puzzles, which is the point of153 this game.154155Both drafts below were checked with a prototype of exactly this rule (a156`GOAL` patch on the compact simulator in `/tmp/cw/sim-goal.mjs`). **Drafts157only, not in the pack.** Legend additions: `+` DOCK, `*` CRATE on DOCK, word158`D` DOCK, word `O` GOAL.159160### Draft G1: Pallet row (pure Sokoban, BYPASS on purpose)161162```js163// DRAFT, needs the proposed GOAL property. Solves in 27: LUURRDULLDDRRRRUULLLRDDLLUU164board([165 'MIY.WIS.CIP.',166 '############',167 '#+..#.######',168 '#.$....#####',169 '#.#$#..#DIO#', // DOCK IS GOAL, sealed in the racking170 '#.@...+#####',171 '############',172], { id: 'pallet-row', title: 'Pallet row', category: 'optional',173 hint: 'Every dock wants a crate. Corners are forever.', allowBypass: true });174```175176The first real Sokoban proof. No word needs to move, so the solver reports177BYPASS(27). That's the point of the level, which is why it declares178`allowBypass`.179180### Draft G2: Short one crate (a word is cargo)181182```js183// DRAFT, needs the proposed GOAL property. Solves in 36 (prototype):184// RRRRRURDLLLUULDULLDLDRRRRRRDDRRUURUL185board([186 'MIY.WIS.CIP.',187 '############',188 '#.....#....#',189 '#.$.$.#.+..#',190 '#.@...N.+..#', // three docks, two crates, one loose WIN word in the doorway191 '#.....#.+..#',192 '#######....#',193 '############',194 '...DIO......',195], { id: 'short-one-crate', title: 'Short one crate', category: 'challenge',196 hint: 'Three docks, two crates. Something else on this floor can be pushed.' });197```198199There are two crates for three docks. The third cover is the loose `WIN` word,200which does nothing as a rule here (there's no `X IS` in front of it) but is201`PUSH` like every word. There's a push-order problem too: `WIN` starts in the202doorway, so it goes through first and has to end up on the far dock without203blocking the others.204205## Gates and switches (proposal)206207Provisional. The user may define other semantics. Three draft scenarios below208were run on a prototype (`/tmp/cw/sim-gate.mjs`, a gate patch on the compact209simulator) under the recommended default and under two alternatives210(`ONLY_PUSH`, `CRUSH`). Results are quoted after each scenario.211212### Decisions213214| Question | Choice | Why (scenario) |215| --- | --- | --- |216| Entry or occupancy? | **Occupancy.** A plate is pressed while anything stands on it. | This is the Sokoban-goal question: "a crate sits on the plate" is a state you can see. Entry toggles need a hidden parity that the board doesn't show, and undo then has to restore a counter the player can't see. D1 (door stop) is only readable because the crate on the plate *is* the explanation. |217| What can press? | **Anything:** any object or word, whatever its properties. | D2 (hand-off) is solvable only if a `YOU` crate with no `PUSH` can press. Under `ONLY_PUSH` it is UNSOLVED. "Something is on it" is also the one rule a player can check by looking. |218| When do gates change? | **End of turn**, after movement, transforms, and contacts. Gates never change mid-turn. | With two `YOU`s (D2), same-turn evaluation would depend on the engine's YOU order (projection sort, then id). The cat would pass or bump depending on which YOU the engine happened to move first. End of turn is order-free, and matches how WIN/SINK already resolve. |219| Something is in a closing gate? | **It stays open until the cell is empty** (a jam). No crushing. | D3 (jam) shows the alternative. Under `CRUSH` the one-crate corridor becomes a death trap (UNSOLVED), because the pusher always ends up in the gate as the plate empties. A jam is also a Sokoban tool: a crate works as a door stop. Design consequence: **keep plates at least 2 cells from their own gate**, or a lone YOU can step plate, gate, through and skip the puzzle. |220| Closed gate behaviour | Acts as `STOP` for movement **regardless of rules**. An open gate has no properties. | Gates are mechanisms, not grammar. If gate state came from a rule (`GATE IS STOP`), breaking that rule would open every gate in the level. |221| Linking | **By channel in level data.** `PLATE` and `GATE` objects carry `channel: 'amber' \| 'teal' \| …`, taken from a small fixed palette. The renderer tints the plate ring and the gate bars with the channel colour, and the ledger lists `AMBER 1/1`. | Linking by noun (every PLATE opens every GATE) can't express two circuits. Linking by id is invisible to players. A colour is visible and needs no text. |222| Rules or level data? | **Level data for the link and the state. Nouns stay nouns.** `PLATE` and `GATE` are ordinary objects, so `GATE IS CRATE` still transforms them (the role follows the noun and the channel stays on the entity). | This keeps the world's rule puzzles (transform a gate away, push a word onto a plate) and puts the circuit where the player can see it. A rule form like `PLATE IS SWITCH` / `GATE IS SHUT` can't say *which* plate opens *which* gate without a new grammar. |223224### Draft D1: Door stop (occupancy, anything presses)225226```js227// DRAFT. '_' = PLATE (channel amber), '|' = GATE (channel amber).228// Prototype: 15, LURRURDDURRRRRR (same under ONLY_PUSH and CRUSH).229board([230 'MIY.WIS.CIP.',231 '............',232 'GIK.........',233 '############',234 '#.....#TIN##',235 '#.$.$.|.~.&#', // gate, then a gap, then the truck236 '#.@...######',237 '#..._.######', // plate two cells off the gate row238 '############',239], { id: 'door-stop' });240```241242One crate has to sit on the plate and the other has to fill the gap behind243the gate. The gate is open for as long as the first crate stays on the plate.244BYPASS in the solver sense (no word moves). As shipped content it would need245a rule edit, for example a missing `PUSH`.246247### Draft D2: Hand-off (end-of-turn timing, anything presses)248249```js250// DRAFT. Prototype: 15, LUDLURRRRRURRRR. UNSOLVED under ONLY_PUSH.251board([252 'WIS.TIN....',253 '###########',254 '#.......###',255 '#..MIY..|.&', // the cat's corridor runs through the gate256 '#CA.....###', // push A then C up: CRATE AND MIAU IS YOU257 '#..@....###',258 '###########',259 '#$....._###', // the crate's lane ends on the plate, wall behind it260 '###########',261], { id: 'hand-off' });262```263264After `CRATE AND MIAU IS YOU`, the crate walks right onto the plate (turn26512) while the cat walks its room. The crate then stays on the plate because266the wall at the end of its lane absorbs the move, and the cat crosses. The267crate isn't `PUSH`, so only "anything presses" makes this work. The shortest268route never bumps the closed gate. The fixture's `same-turn-block` probe269(`LLUDRURRRRURRRRR`) covers that case. On turn 13 the crate lands on the plate270while the cat, next to the gate, is refused. The gate opens at the end of the271turn, and the cat crosses on turn 14.272273### Draft D3: Jam (a gate never closes on an occupant)274275```js276// DRAFT. Prototype: 7, RRRRRRR. UNSOLVED under CRUSH (the cat is crushed).277board([278 'MIY.WIS.TIN',279 '###########',280 '#......#..#',281 '#.@.$._|.&#', // plate right next to its own gate282 '#......#..#',283 '###########',284], { id: 'jam' });285```286287There is no `CRATE IS PUSH`, so the crate is inert and the cat walks through288it. (An earlier version of this paragraph said the crate is pushed, which was289wrong. The route is the same.) The cat presses the plate itself (turn 4), and290the gate opens at the end of that turn. It steps into the gate (turn 5): the291plate is now empty but the cell is occupied, so the gate is open and jammed292rather than closing on the cat. When the cat steps out (turn 6), the gate293closes behind it. This also shows the authoring hazard: a plate next to its294own gate lets a lone YOU skip the circuit.295296### Undo, reset, sharing, and solver requirements297298- **Snapshot: no stored gate bit** (superseded, and reconciled 2026-09-24299 with the accepted `add-channel-linked-gates` and300 `add-declarative-mechanic-state-contract`, design D25). An earlier draft of301 this section stored `open: boolean` per gate, on the theory that a jam302 depends on history. It doesn't: a closed gate blocks entry, so **anything303 standing in a gate cell means the gate is being held open.** Gate state is304 therefore derived from the current board at end of turn:305 `pressed(channel)` = any occupant on any plate of that channel;306 `open` = `pressed(channel)` **or** the gate cell is occupied;307 `jammed` = occupied **and not** pressed.308 That covers the start-of-level and transform-back cases too (D25: "state is309 recomputed, and the jam rule applies if occupied"). Plates hold no state310 either. Undo and reset restore positions and nouns, and gate state follows.311- **Authoring.** Object records need an optional `channel` field, and the312 `object()` helper needs a 5th argument or an options bag. Today `object()`313 only takes `(noun, x, y, id)`, so the drafts spread the record by hand. The314 loader should validate `channel` against the fixed palette and warn when a315 channel has plates but no gates, or the reverse.316- **Sharing.** `channel` must survive `exportPack`, `packLink`, and install.317 It's plain data, with no code.318- **Solver.** Because gate state is a pure function of positions and nouns,319 **the state key needs nothing extra.** Two states that look identical *are*320 identical. (The prototype's per-gate key character is superseded. It was321 only needed while gates carried a hidden bit.) The D1 to D3 drafts remain322 useful as regression fixtures for the engine's derived semantics.323- **Regression fixtures.** D1 to D3 are exported as data at324 [`fixtures/gates.json`](fixtures/gates.json), with routes and observations325 re-derived under the derived semantics. None of the three shortest routes326 changed from the stored-bit prototype: D1 `LURRURDDURRRRRR` (15), D2327 `LUDLURRRRRURRRR` (15), D3 `RRRRRRR` (7). Format of each entry:328 - `level`: `{ id, width, height, mechanics, objects }`. Every object has an329 explicit, readable, stable id: `d1-gate-a`, `d1-plate-a`, or330 `d1-<noun>-<x>-<y>` and `d1-word-<text>-<x>-<y>` for everything else, where331 x and y are the starting cell. PLATE and GATE objects carry332 `channel: 'amber'`.333 - `mechanics`: the minimal declaration334 `[{ id: 'gates', type: 'channel-gates', version: 1, channels: ['amber'] }]`.335 `type` names the host mechanic, and `channels` lists the channel palette336 entries the level uses, so a loader can reject a channel with no337 declaration. The mechanic is derived, so it declares no state.338 - `expectedRoute`: the shortest winning route under the derived semantics.339 `routeChangedFromStoredBitPrototype` is `false` for all three.340 - `observations`: the gate and plate projections after turn 0, after every341 turn where any `open`, `jammed`, or `pressed` value changes, and after the342 winning turn. Entries are `{ afterTurn, move, gates: [{ id, open, jammed }],343 plates: [{ id, pressed }], won }`. An entry also gets344 `refusedByClosedGate: [ids]` when a YOU was refused by a closed gate on a345 turn that was otherwise accepted.346 - `probes` (D2 only): extra non-shortest routes that exercise a rule. Each347 probe has the same observation format.348 - The engine currently rejects `channel` and `mechanics`, so these fixtures349 are for the upcoming implementation. They were generated with350 `/tmp/cw/gen-gates.mjs` on the derived prototype `/tmp/cw/sim-gate.mjs`.351- **Events.** `settled()` should report `gates: [{ id, from, to, cause:352 'pressed' | 'released' | 'jammed' }]` so juice and audio can react.