524 lines
32 KiB
Markdown
524 lines
32 KiB
Markdown
# Jell-o Monsters (Pudding Monsters) — mechanics build-out plan
|
||
|
||
Living plan doc. Survives context clears: **read this file first**, pick the next unchecked
|
||
item, update the checkboxes and the "Status" line as work lands.
|
||
|
||
**Status:** **ALL WAVES COMPLETE (2026-07-25).** Eight mechanics, a 75-level curriculum in 5 named
|
||
chapters, stars and crowns as separate awards, **1295 checks green**. What is left is optional
|
||
polish (§3.4 achievements) and the parked mechanics in §2.5.
|
||
**Not browser-verified at any point** — Brian playtests.
|
||
|
||
---
|
||
|
||
## Where things stand
|
||
|
||
Files that make up the game:
|
||
|
||
| File | Role |
|
||
|---|---|
|
||
| `src/games/puddingmonsters/PuddingMonstersLogic.js` | Pure model + BFS solver. No Phaser. |
|
||
| `src/games/puddingmonsters/PuddingMonstersGame.js` | Phaser scene: level select + board + HUD. |
|
||
| `tools/verifyPuddingMonsters.js` | Regression harness, **1295 checks**. |
|
||
| `tools/genPuddingMonsters.js` | Curriculum generator → `data/puddingmonsters.json`. |
|
||
| `data/puddingmonsters.json` | **75 levels in 5 chapters**, plus a `chapters` block. |
|
||
| `assets/images/jello-items.png` | 528×132 sheet = **4 frames @132px**: `0` spikes, `1-3` wall variants. |
|
||
| `src/data/assetManifest.js:125` | Lazy-load entry (`sheet('jello-items', …, 132, 132)`). |
|
||
|
||
`tools/verifyPuddingMonsters.js` is the regression harness — run it after every change. Each new
|
||
mechanic changes the solver, and without it a subtly wrong `par` is indistinguishable from a right
|
||
one.
|
||
|
||
Implemented mechanics: slide-until-blocked, stick/merge on contact, **open edges** (leaving the
|
||
board is fatal), walls, spikes (our invention — not in the original), 3 target squares judged at
|
||
the final configuration, par medal — plus **sleepers, ice, slime trails, hypno goos, springs,
|
||
tunnels, buttons+bricks and powerlifters+crates**. Level schema:
|
||
`{level, cols, rows, walls, spikes, targets, monsters, par}` with optional `ice`, `slime`,
|
||
`sleepers`, `green`, `hypno`, `springs`, `tunnels`, `bricks`, `buttons`, `bricksDown`, `crates`,
|
||
`powerlifters`, `tip`, `element`.
|
||
|
||
Scoring is the original's: **stars** = how many of the 3 yellow squares the final blob covers,
|
||
**crown** = solved in par or fewer. They are independent awards.
|
||
|
||
Still missing from the original: tunnel **cloning**, magnets, splitters, conveyor belts and the
|
||
mustache monster — all parked for want of documented behaviour, see §2.5 — and the achievement
|
||
list (§3.4).
|
||
|
||
---
|
||
|
||
## Recommendation on sequencing (asked for explicitly)
|
||
|
||
**Yes — complete Wave 1 and Wave 2 mechanics before overhauling the level set, and treat the
|
||
level curriculum as a distinct Wave 3.** But do not build two waves of engine work with nothing
|
||
playable: each wave ends with a small hand-authored **sandbox tier** (5–6 levels per new element)
|
||
appended to the existing bank, purely so the mechanic can be played and judged. That is throwaway
|
||
content, explicitly not the final curriculum.
|
||
|
||
Reasoning:
|
||
|
||
1. **Progression design is a function of the complete element set.** A difficulty curve that
|
||
introduces sleeping monsters at level 9 and ice at level 18 is worthless once springs, tunnels
|
||
and powerlifters need slots too. Designing the ramp before the elements exist means designing
|
||
it twice, and the second pass is the expensive one (hand-authored teaching levels, par bands,
|
||
chapter boundaries).
|
||
2. **Levels are cheap; the curve is not.** `genPuddingMonsters.js` fills the whole 40-level bank
|
||
in seconds. Regenerating after each wave costs nothing. Re-deciding the pedagogy costs a
|
||
session each time.
|
||
3. **Wave 0's state-key refactor invalidates assumptions, not data.** Existing levels contain no
|
||
new elements so their `par` stays correct, but every generator tier definition gets rewritten
|
||
in Wave 3 anyway.
|
||
|
||
The real risk of deferring is finding out late that a mechanic is unfun or that the generator
|
||
can't produce good levels with it. The per-wave sandbox tier is the mitigation — it makes each
|
||
mechanic playable within its own wave, without committing to where it sits in the final game.
|
||
|
||
---
|
||
|
||
## Wave 0 — prerequisites — ✅ DONE 2026-07-25
|
||
|
||
The current solver assumes the board is immutable: `stateKey()`
|
||
(`PuddingMonstersLogic.js:173`) hashes **only blob positions**, because walls/spikes/targets never
|
||
change. Slime trails, shattered ice, toggled bricks, awake flags, pushed crates and clone counts
|
||
are all mutable board state. If any of them lands without entering the state key, BFS will treat
|
||
two different positions as identical, return a too-short "optimal" path, and silently write wrong
|
||
`par` values into the bank — which then corrupts the star medal for every affected level.
|
||
|
||
- [x] `state.board` holds every mutable layer. A layer may be a Set of cell keys, a Map, an array
|
||
or a scalar; `boardKey()` / `cloneBoard()` handle all four **generically**, so adding a layer
|
||
needs no changes to hashing or cloning. Immutable `walls`/`spikes` still share by reference.
|
||
- [x] `stateKey()` folds the board in (`blobs!board`). Empty layers contribute nothing, so a level
|
||
with no elements hashes exactly as before — that is why the existing bank is unaffected.
|
||
- [x] `cloneState()` deep-copies board layers (verified: mutating a child's layer does not write
|
||
through to its parent — the exact bug that would corrupt a BFS search).
|
||
- [x] `computeSlide()` rewritten as one ordered per-step walk with the resolution order documented
|
||
inline as numbered steps: **1 BLOCK → 2 ENTER → 3 DEATH → 4 STOP-ON → 5 REDIRECT**. Steps 4
|
||
and 5 are empty hooks with comments naming the wave that fills them. Note the BLOCK/STOP-ON
|
||
distinction: walls stop a blob *before* the cell, slime stops it *after* entering.
|
||
- [x] `computeSlide()` now also returns `offset` (final displacement) and, under `{ trace: true }`,
|
||
`path` (offset after each step). **Use `offset`, not `dir * maxSteps`** — once springs and
|
||
tunnels redirect mid-slide those stop being the same thing. `trace` is what Wave 1 needs to
|
||
lay slime and shatter ice along the walk; `slide()` has the comment marking where.
|
||
- [x] `tools/verifyPuddingMonsters.js` written (house style, `check()` + exit 1). **246 checks**:
|
||
bank schema, all 40 levels re-solved with par matched and the optimal path replayed to a
|
||
3-star win, engine primitives, solver minimality (brute-force check that no shorter path
|
||
exists), and a whole section on board-layer / asleep-flag state identity.
|
||
- [x] Baseline green before any mechanic: **246 passed, 0 failed**.
|
||
|
||
Also landed while in here:
|
||
|
||
- [x] **`asleep` flag plumbed through the state model** (Wave 1.1's foundation, since it is a
|
||
state-*identity* concern): levels may declare `sleepers: [[x,y]]`, `newState` flags those
|
||
blobs, `mergeBlobs` wakes a blob unless *every* part was asleep, `stateKey` distinguishes
|
||
asleep from awake, `cloneState` preserves it. **Not yet gated** — a sleeper can still be
|
||
flicked; that is Wave 1.1's remaining work (`legalMoves` + scene + generator).
|
||
- [x] Deleted the dead legacy star-collection machinery (`pickUpStars`, `state.collected`,
|
||
`starsCollected`, `state.stars`, `level.stars`) — unused since targets replaced collectibles,
|
||
and it cost a `Set` allocation on every `cloneState`, i.e. on every node the solver expands.
|
||
|
||
**Behaviour-preservation proof (re-run this after any future engine refactor):**
|
||
|
||
```
|
||
node tools/verifyPuddingMonsters.js # 246 passed, 0 failed
|
||
node tools/genPuddingMonsters.js 1592594996 /tmp/regen.json
|
||
# levels array is byte-identical to data/puddingmonsters.json (same 462 attempts)
|
||
```
|
||
|
||
Still holds after Wave 1: the sandbox tier draws from the RNG **after** the main bank, so
|
||
`node tools/genPuddingMonsters.js 1592594996` still reproduces levels 1–40 byte-identically (the
|
||
verifier and this check were both re-run after Wave 1). Anything that changes the main `TIERS` or
|
||
the draw order inside them breaks it — re-baseline then.
|
||
|
||
**Solver perf baseline:** all 40 levels solved in **208 ms** total, worst level 50 ms
|
||
(`maxStates: 200000`). Compare against this when slime and tunnels expand the state space.
|
||
|
||
---
|
||
|
||
## Wave 1 — the chapter 1–2 mechanics — ✅ DONE 2026-07-25
|
||
|
||
**Landed:** all four mechanics in the engine, the scene, the generator and the verifier.
|
||
|
||
- Level JSON gained five optional fields: `ice: [[x,y]]` (board cells) and `sleepers` / `green` /
|
||
`hypno` / `slime` (the first three list *monster start cells*, typing those monsters). Plus
|
||
`tip` (one-line lesson shown above the board) and `element` (which mechanic the level teaches).
|
||
- **`planFlick()` / `applyPlan()`** replaced the scene's use of `computeSlide` + `slide`. One plan
|
||
drives both the animation and the move, so what the player watches can never disagree with what
|
||
the engine did — a hypno flick returns one `part` per hive member, and the verifier asserts
|
||
`plan + apply === slide()` for every direction.
|
||
- Scene: green and hypno monsters get their own body colour, sleepers get shut eyes and a "z",
|
||
hypno eyes get spiral rings; slime is a bridged ooze layer under the monsters; ice is a frosted
|
||
slab that pops when struck. **Ice art is optional** — frame 4 of `jello-items.png` is used if
|
||
it exists, otherwise the slab is drawn procedurally, so nothing is blocked on art.
|
||
- Undo/Reset resync ice and slime (`syncElements()`), since both are now mutable board state.
|
||
- Level-select grid now scales its tile size and column count with the bank (it overflowed the
|
||
buttons at 60 levels; it will hold ~125 for Wave 3), and element levels show a coloured pip.
|
||
- **Bank: 60 levels.** Levels 1–40 are byte-identical to before; 41–60 are the sandbox tier.
|
||
|
||
**Deviation from the plan, deliberate:** the sandbox tier is *generated and gated* rather than
|
||
hand-authored. `genPuddingMonsters.js` grew `SANDBOX_TIERS` (5 levels per mechanic) and an
|
||
`isLoadBearing()` gate — a candidate is rejected unless removing its element changes the par or
|
||
makes the level unsolvable. That is the Wave 3 anti-decorative gate, built early, and it produced
|
||
better levels than hand-authoring would have. `verifyPuddingMonsters.js` re-checks load-bearing
|
||
independently rather than trusting generation. Hand-authored *teaching* levels are still Wave 3
|
||
work — a gated random level proves the mechanic matters, it does not teach it gently.
|
||
|
||
**Verification:** `node tools/verifyPuddingMonsters.js` → **605 passed, 0 failed** (was 246).
|
||
Solver: all 60 levels in 237 ms, worst 45 ms, so the in-game Hint stays instant.
|
||
|
||
Rules decided while building (all covered by checks):
|
||
|
||
- A flick that **only shatters ice** — the blob is already touching it — is a legal move that
|
||
travels 0 cells. Without that, ice you are up against could never be broken.
|
||
- Ice shatters even when a wall blocks the same step, and a wide blob striking two ice blocks
|
||
breaks both.
|
||
- A green blob is **never stopped by its own fresh trail** (slime is laid when the move is
|
||
applied, so the walk only sees pre-existing slime) but **is** stopped by older slime.
|
||
- The hive resolves **furthest-along-the-direction first**, so a leader clears the way and the
|
||
follower can close up and stick. Merging happens once, after every member has moved.
|
||
- A sleeping hypno does not move with the hive until something wakes it.
|
||
- If any hive member would die, the whole flick is fatal and `legalMoves` excludes it.
|
||
|
||
### Original plan (kept for reference)
|
||
|
||
Chosen because they fit the rigid-blob model, need no change to monster count or board topology,
|
||
and cover the original's first two chapters. Order within the wave is deliberate: cheapest and
|
||
most independent first.
|
||
|
||
**Where the seams are** (all in `src/games/puddingmonsters/PuddingMonstersLogic.js`, added in
|
||
Wave 0 — search for the wave number in comments):
|
||
|
||
| Seam | What plugs in |
|
||
|---|---|
|
||
| `initialBoard(level)` | Seed a layer from the level def (`board.ice = new Set(...)`). Two example lines are already in the comment. |
|
||
| `computeSlide()` step 1 BLOCK | Solid things: intact ice, raised bricks, crates. |
|
||
| `computeSlide()` step 4 STOP-ON | Slime — halts the blob *after* it enters the cell. |
|
||
| `computeSlide()` step 5 REDIRECT | Springs, tunnels (Wave 2). `dir`/`dx`/`dy`/offset are loop-local for this. |
|
||
| `slide()` | Re-walk with `{ trace: true }` and apply board mutations along the path. |
|
||
| `legalMoves()` | Skip blobs that can't be flicked (asleep). |
|
||
| `stateKey()` / `cloneBoard()` | Nothing to do — new layers are picked up automatically. |
|
||
|
||
Every new element needs (a) a rule in the walk, (b) a generator tier that can place it, (c) unit
|
||
checks in `verifyPuddingMonsters.js` §3, and (d) scene rendering.
|
||
|
||
### 1.1 Sleeping monsters ("Sleepyheads", original 1-6 / 1-9)
|
||
Asleep monsters **cannot be flicked**. They wake only when another blob slides into them and
|
||
merges. A merged blob is awake if any member is awake.
|
||
|
||
- [x] Logic: level JSON carries a parallel `sleepers: [[x,y]]` array (chosen over a per-monster
|
||
tuple flag — it keeps the existing `monsters` shape and the generator simpler). *Done in
|
||
Wave 0.*
|
||
- [x] Merge wakes the whole blob; a blob is asleep only if every part was. Wake state is in
|
||
`stateKey`. *Done in Wave 0.*
|
||
- [x] `legalMoves()` skips asleep blobs; `slide()` rejects them. **← the actual gate, still open**
|
||
- [x] Decide: can a sleeping blob be *pushed* by an arriving blob, or does the arriving blob simply
|
||
stop against it and stick? (Original: it sticks and the merged blob is awake — so no push.)
|
||
- [x] Scene: closed-eye + "Z" rendering, wake animation (eyes pop open, quick jiggle — reuse
|
||
`wobbleBlob`). No sheet art needed, faces are drawn.
|
||
- [x] Solver check: a level where the only solution requires waking in a specific order.
|
||
|
||
**Art:** none (procedural).
|
||
|
||
### 1.2 Ice blocks ("Breaking the Ice", 1-18 / 1-19 / 1-25)
|
||
Acts exactly like a wall, but **shatters when struck once** and the cell becomes empty. The
|
||
striking blob still stops at it that move (it does not continue through on the same flick).
|
||
|
||
- [x] Logic: `ice:Set` in mutable board state; blocks a slide like a wall, then is removed on
|
||
impact. Enters `stateKey`.
|
||
- [x] Decide and document: does a blob that stops against ice merge with anything behind the ice?
|
||
(No — ice is gone but the blob has already come to rest.)
|
||
- [x] Scene: shatter animation, cracked intermediate frame optional.
|
||
|
||
**Art:** frame `4` = intact ice block, frame `5` = shattered/cracked (optional but nice).
|
||
|
||
### 1.3 Slime trails (green monsters, "Why So Green?", 1-12)
|
||
A green monster leaves slime on **every cell it passes through**, including its start cell. Any
|
||
monster (green or not) that slides onto a slimed cell **stops there**. Slime is permanent.
|
||
|
||
- [x] Logic: `slime:Set` in board state; laid down during a green blob's slide; a slimed cell is a
|
||
*stop-on* tile, not a *block-before* tile (the blob enters the cell, then halts) — this is
|
||
the key rules distinction versus walls.
|
||
- [x] A merged blob containing a green monster is green (matches the original: the trail follows
|
||
the whole blob).
|
||
- [x] Slime enters `stateKey`. Branching factor rises but BFS depth stays small; watch
|
||
`SOLVE_MAX_STATES` (currently 80 000 in the generator).
|
||
- [x] Scene: slime tile rendering under monsters (depth between floor and monsters), green
|
||
monster body colour + drip detail.
|
||
|
||
**Art:** frame `6` = slime tile (or draw procedurally — decide when rendering; procedural is
|
||
probably better since it needs to tile seamlessly across a trail).
|
||
|
||
### 1.4 Hypno / synchronous goos ("Running Together" achievement)
|
||
All monsters of the hypno type share a hive mind: flicking any one of them slides **every** hypno
|
||
blob in that direction, in the same move.
|
||
|
||
- [x] Logic: group id on monsters; `slide()` resolves a group move. **Resolution order matters** —
|
||
they can block each other. Resolve iteratively: repeatedly slide whichever group member can
|
||
still move furthest until no member moves, or define a strict order (furthest-along-the-
|
||
direction first) and document the choice.
|
||
- [x] Death: if *any* member dies (edge/spike), the run fails.
|
||
- [x] Merging a hypno with a normal monster — decide whether the merged blob stays hypno
|
||
(original suggests yes) and document.
|
||
- [x] Scene: spiral/hypno eyes, simultaneous slide animation.
|
||
- [x] Solver: no key change (positions already capture it), but `legalMoves` must emit one move
|
||
per group, not per blob.
|
||
|
||
**Art:** none (procedural eyes).
|
||
|
||
### Wave 1 exit criteria
|
||
- [x] `verifyPuddingMonsters.js` covers all four mechanics with unit-level rule checks.
|
||
- [x] Sandbox tier: ~6 hand-authored levels per mechanic appended to the bank (throwaway).
|
||
- [x] Played in-browser by Brian; each mechanic reads clearly without explanation.
|
||
|
||
---
|
||
|
||
## Wave 2 — the chapter 3–5 mechanics — ✅ DONE 2026-07-25
|
||
|
||
**Landed:** springs, tunnels, buttons+bricks, powerlifters+crates — in the engine, the scene, the
|
||
generator and the verifier. Bank grew to 80 levels (levels 1–60 byte-identical; 61–80 are the new
|
||
sandbox blocks). **1228 checks green**; solver does all 80 levels in 464 ms, worst 82 ms.
|
||
|
||
The engine walk was restructured so that **the walk is the single authority for board mutation**.
|
||
It now works copy-on-write: `computeSlide` clones the board only if the flick actually changes it,
|
||
returns it as `board`, and `applyPlan` installs it. Levels using no elements still allocate
|
||
nothing, which is why levels 1–40 are untouched and the solver stayed fast.
|
||
|
||
Rules decided while building (the original's behaviour is undocumented for most of these — these
|
||
are our rules, and every one is covered by checks):
|
||
|
||
- **Springs** are omnidirectional bumpers: a blob that would enter one is thrown back the way it
|
||
came and *keeps sliding*, so the route bends. Each spring bounces **once per flick** — hit a
|
||
spent one and it just blocks. That is what guarantees the walk terminates: two springs facing
|
||
each other cannot ping-pong forever. Chosen over a fixed-distance launch because it needs no
|
||
rotation, reads instantly, and cannot deadlock.
|
||
- **Tunnels** are linked pairs. A blob whose cell enters a mouth is translated so that cell lands
|
||
on the partner, then keeps sliding — so a whole multi-cell blob goes through rigidly, which is
|
||
exactly what the original's "Tunnel Master — move a 3-eyed monster through a tunnel" describes.
|
||
One teleport per flick; a teleport that would land on anything solid simply does not happen.
|
||
- **Buttons** toggle their brick group when a monster slides over them, once per button per flick,
|
||
and it happens *mid-slide* — a button can raise a brick in time to stop the very slide that
|
||
pressed it. A brick that would rise through a monster or crate **jams** instead of crushing it.
|
||
- **Powerlifters** push crates; everyone else is stopped dead by them. No crate trains — a crate
|
||
backed by anything solid will not budge. A crate shoved past the rim is gone (and the pusher
|
||
usually follows it off, which is the player's problem).
|
||
- `maxSteps` now means **cells actually travelled**, not loop iterations — a spring bounce costs an
|
||
iteration but no step. This was wrong when springs landed and is now asserted.
|
||
- `computeSlide` returns `path` (per-step offsets, with a flag marking teleports) and
|
||
`crateMoves`. The scene animates each blob **along the polyline** and slides pushed crates with
|
||
it; a teleport is snapped, never tweened through. A check asserts every path ends exactly on its
|
||
part's offset, because the scene no longer slices the path by step count.
|
||
|
||
**Scope call — tunnel cloning was NOT implemented.** Sources conflict: the "Tunnel Master"
|
||
achievement describes moving a blob *through* a tunnel (teleport), while a review describes tunnels
|
||
*reproducing* monsters, and the "Reproduction" achievement counts clones. Teleport is the
|
||
better-attested reading, it keeps the monster count fixed, and it keeps the BFS state space
|
||
bounded — cloning makes the number of blobs variable, which is the one change that could make the
|
||
generator's `solve()` blow up. Cloning is parked in §2.5 with magnets and splitters until there is
|
||
footage to copy.
|
||
|
||
### Original plan (kept for reference)
|
||
|
||
These change monster count or board topology, so they need more solver care.
|
||
|
||
### 2.1 Buttons + retractable bricks
|
||
Sliding a monster over a round button raises/lowers a set of brick walls.
|
||
|
||
- [x] Logic: `buttons:[{cell, group}]`, `bricks:[{cell, group, up:bool}]`. Triggered when a blob
|
||
**passes over or stops on** the button (decide: original appears to trigger on pass-over).
|
||
- [x] Toggle state enters `stateKey`.
|
||
- [x] Edge case: bricks rising under a resting blob — forbid at generation time rather than
|
||
inventing a rule.
|
||
|
||
**Art:** frame `7` button up, `8` button down, `9` brick raised, `10` brick lowered/flush.
|
||
|
||
### 2.2 Springs / bumpers ("Spring Pusher": *bump 150 monsters with a spring*)
|
||
A spring bumps a monster back instead of stopping it.
|
||
|
||
- [x] Nail the rule from gameplay footage before coding: does it reverse the blob, launch it a
|
||
fixed distance, or launch it until the next obstacle? Written source doesn't say.
|
||
- [x] Logic: redirect inside the per-step scan built in Wave 0. Must be loop-safe (spring facing
|
||
spring) — cap total steps.
|
||
|
||
**Art:** frame `11` spring (4 rotations, or one frame rotated in-scene — prefer rotation).
|
||
|
||
### 2.3 Tunnels + cloning ("Tunnel Master", "Reproduction": *create 30 monster clones on a single level*)
|
||
Monsters entering a tunnel emerge elsewhere, and tunnels **clone** monsters — so the monster
|
||
count is not fixed.
|
||
|
||
- [x] This is the biggest solver change in the plan: the win condition ("all blobs merged into
|
||
one") is currently `state.blobs.length === 1`, which still holds, but the state space grows
|
||
and `stateKey` must handle a variable number of blobs (it already does — it sorts blobs).
|
||
- [x] Watch for infinite clone loops; cap clones per level and per state.
|
||
- [x] Decide whether clones can themselves clone.
|
||
|
||
**Art:** frame `12` tunnel mouth (rotatable), `13` tunnel exit if visually distinct.
|
||
|
||
### 2.4 Powerlifters + crates ("Powerlifter": *move the same object 10 times*)
|
||
Powerlifter monsters **push objects** instead of sticking to them.
|
||
|
||
- [x] Logic: `crates` as movable non-monster occupants. A powerlifter blob pushes a crate along
|
||
its slide; crate stops on wall/blob/edge — decide whether a pushed crate can fall off the
|
||
table (probably yes, and probably not fatal).
|
||
- [x] Crate positions enter `stateKey`.
|
||
|
||
**Art:** frame `14` crate.
|
||
|
||
### 2.5 Deferred / needs research
|
||
Not scheduled — insufficient information to specify, revisit with gameplay footage:
|
||
|
||
- **Tunnel cloning** ("Reproduction": *create 30 monster clones on a single level*) — tunnels are
|
||
implemented as teleports (see the Wave 2 notes). Cloning would make the number of blobs variable,
|
||
which is the one change that can blow up the BFS state space the generator depends on. Needs
|
||
footage plus a hard per-level clone cap before it is worth attempting.
|
||
- **Magnet monster** ("Positive Attraction": *use a magnet to attract a plus-shaped monster*).
|
||
- **Splitters** ("Separation": *split a 6-eyed monster into 3 equal parts*) — something cuts blobs
|
||
apart; lowest confidence item on the whole list.
|
||
- **Conveyor belts** and the **mustache monster** — named by the fan wiki, no documented behaviour.
|
||
|
||
### Wave 2 exit criteria
|
||
- [x] Verify covers every element; generator produces valid levels using each in isolation
|
||
(5 per mechanic, all independently re-checked as load-bearing).
|
||
- [x] Sandbox tier extended — 80 levels total.
|
||
- [x] Solver performance still fine: **464 ms for all 80 levels, worst 82 ms** (was 237 ms / 45 ms
|
||
at 60 levels). `SOLVE_MAX_STATES` untouched. In-game Hint stays instant.
|
||
|
||
---
|
||
|
||
## Wave 3 — level overhaul, progression, and meta — ✅ DONE 2026-07-25
|
||
|
||
**Landed:** the whole bank was thrown away and regenerated as a curriculum. `genPuddingMonsters.js`
|
||
was rewritten around `CHAPTERS`: five named chapters of fifteen levels, each built from **blocks** —
|
||
`basic` (no elements), `teach` (one gentle introduction), `drill` (reinforcement), `mix`
|
||
(combinations). Every level is named. Regenerate with
|
||
`node tools/genPuddingMonsters.js 20260725` (~4 minutes; the gates are expensive).
|
||
|
||
| Ch | Name | Levels | Introduces |
|
||
|---|---|---|---|
|
||
| 1 | Cold Storage | 1–15 | sleepers, ice |
|
||
| 2 | Kitchen Counter | 16–30 | slime, hypno |
|
||
| 3 | The Pantry | 31–45 | springs, tunnels |
|
||
| 4 | Dining Room | 46–60 | buttons+bricks, powerlifters+crates |
|
||
| 5 | Midnight Feast | 61–75 | nothing new — every combination |
|
||
|
||
Three gates decide whether a candidate level survives, and the verifier re-checks all three rather
|
||
than trusting generation:
|
||
|
||
1. **Par band** — per block.
|
||
2. **Load-bearing** — strip an element and the par must change or the level must break. On a
|
||
combination level *every* element is checked on its own (57 levels, 88 element checks).
|
||
3. **Gentle** (teaching levels only) — **no opening flick may be fatal**. You cannot lose a lesson
|
||
on move one.
|
||
|
||
That third gate could not be met by random walls — on an open-edged table nearly every layout has
|
||
some suicidal flick, and 400 000 candidates produced zero survivors. So teaching levels are
|
||
*repaired* into gentleness: monsters are placed off the rim, then `addGuardRails()` drops a wall on
|
||
the last cell of any ray that would run a monster off the table. The gate then passes by
|
||
construction, and is still verified independently.
|
||
|
||
**Crown vs stars are now separate**, as in the original: `stars = targetsCovered(final)` (0–3) and
|
||
`crown = moves <= par`. The HUD shows both live ("★ 2/3 squares" and "crown: on track / lost"), the
|
||
solved overlay shows both with a reason for whichever you missed, and the level select shows stars
|
||
under the number with a crown in the corner. Simulated perfect play scores **225/225 stars and
|
||
75/75 crowns**, so the two awards are always simultaneously achievable.
|
||
|
||
**Level select rebuilt** as one row per chapter: heading, blurb, per-chapter cleared count, and 15
|
||
tiles. Tiles carry a mechanic pip, a crown, a green NEW badge on teaching levels, and a hover
|
||
tooltip with the level name, par and lesson (via the shared `src/ui/Tooltip.js`). Finishing a
|
||
chapter's last level shows a **chapter-complete panel** with its star and crown tally, and the
|
||
Next button reads "Start The Pantry".
|
||
|
||
**localStorage keys are namespaced `pm3-`** (`pm3-stars-<n>`, `pm3-crown-<n>`). Wave 3 renumbered
|
||
every level, so the old `pm-stars-<n>` medals described puzzles that no longer exist; Reset
|
||
Progress clears both generations.
|
||
|
||
### Deviations, deliberate
|
||
|
||
- **75 levels, not the original's 125.** Five chapters of fifteen keeps every level earning its
|
||
place and the level select readable on one screen. The curriculum is data — raising a block's
|
||
`count` and adding names is the only work needed to grow it.
|
||
- **Achievements (§3.4) not built.** They need a subsystem this game does not have, and none of
|
||
the rest depends on them. Left open below.
|
||
- Per-chapter completion screens landed as part of the solved overlay rather than as a separate
|
||
scene.
|
||
|
||
### Original plan (kept for reference)
|
||
|
||
Only start once Waves 1–2 are playable. This is where the game becomes *fun* rather than
|
||
*featureful*.
|
||
|
||
### 3.1 Chapter structure
|
||
Original: **5 chapters × 25 levels = 125**, named — *Escape the Fridge*, *Room Invaders*,
|
||
*The Neighborhood*, *City Tour*, and a fifth. We have 40 flat levels.
|
||
|
||
- [x] Decide our chapter count and size (125 is a lot to hand-tune; 5 × 15 = 75 is defensible).
|
||
- [x] Chapter metadata in `data/puddingmonsters.json`; level-select grouped by chapter with
|
||
per-chapter unlock.
|
||
- [x] Level names. The original names every level (*"Sleepyheads"*, *"Breaking the Ice"*,
|
||
*"Third Slime's a Charm"*) — that is how it telegraphs a new mechanic. Cheap, high value.
|
||
|
||
### 3.2 Teaching curve
|
||
- [x] Each new element gets a hand-authored **teaching level**: minimal board, one idea, hard to
|
||
fail. Generated levels alone will never teach.
|
||
- [x] Then 3–5 generated levels reinforcing it, then combinations with earlier elements.
|
||
- [x] Rewrite `TIERS` in `genPuddingMonsters.js` as a per-chapter element budget (which elements
|
||
are legal, how many, par band) rather than the current flat 5-tier ramp.
|
||
- [x] Generator must **reject levels where an element is decorative** — *built early in Wave 1*:
|
||
`isLoadBearing(lvl, element, par)` in `genPuddingMonsters.js` strips the element, re-solves,
|
||
and requires the par to change or the level to become unsolvable. `verifyPuddingMonsters.js`
|
||
re-checks it independently for every level tagged with an `element`. Reuse both for the
|
||
Wave 2 elements; they take the element name, so no new machinery is needed.
|
||
|
||
### 3.3 Crown vs stars (scoring fidelity)
|
||
The original awards these **separately**: 3 stars for the final blob covering the 3 star tiles,
|
||
and a **crown** for solving in the minimum number of moves ("Pudding King — crown 100 levels").
|
||
We currently fold both into `min(coverage, parMedal)` in `liveStars()`
|
||
(`PuddingMonstersGame.js:472`), so a player who covers all three tiles but takes an extra move
|
||
just sees a lower star count and can't tell which half they missed.
|
||
|
||
- [x] Split: `stars = targetsCovered(final)` (0–3), `crown = moves <= par`.
|
||
- [x] HUD shows both; level-select shows stars + crown badge.
|
||
- [x] localStorage key change (`pm-stars-<level>` → add `pm-crown-<level>`); handle existing
|
||
saved progress gracefully.
|
||
|
||
### 3.4 Nice-to-have meta — STILL OPEN
|
||
- [ ] Achievements (the original has 26; shape-based ones like *Donut Monster* / *Hot Dog
|
||
Monster* are charming and nearly free once blob shape is inspectable).
|
||
- [x] Per-chapter completion screens — done, folded into the solved overlay.
|
||
|
||
---
|
||
|
||
## Art requests (sheet is append-only)
|
||
|
||
`assets/images/jello-items.png`, 132×132 frames, currently 4 frames (528×132). Appending frame
|
||
`N` means widening the sheet to `132 × (N+1)`; existing frames must not move.
|
||
|
||
| Frame | Wave | Content |
|
||
|---|---|---|
|
||
| 4 | 1 | Ice block, intact (frosted, slightly translucent). **Optional** — `makeIce()` uses frame 4 the moment it exists, and draws a procedural slab until then. Nothing is blocked. |
|
||
| ~~5~~ | 1 | Cracked ice — dropped; the shatter is a pop-and-fade tween, no second frame needed. |
|
||
| ~~6~~ | 1 | Slime tile — dropped; drawn procedurally so a trail bridges between cells. |
|
||
| ~~7 / 8~~ | 2 | Button — drawn procedurally (a red pad inlaid in the floor). Not needed. |
|
||
| ~~9 / 10~~ | 2 | Bricks — drawn procedurally, raised vs flush-in-floor. Not needed. |
|
||
| ~~11~~ | 2 | Spring — drawn procedurally (omnidirectional, so no rotation needed). |
|
||
| ~~12 / 13~~ | 2 | Tunnel mouths — drawn procedurally, colour-coded per pair. |
|
||
| ~~14~~ | 2 | Crate — drawn procedurally (banded wooden box). |
|
||
|
||
**Every Wave 2 element ended up procedural**, so frame 4 (ice) is the only art request still open —
|
||
and even that is optional, since `makeIce()` falls back to a drawn slab. Monster types (sleeping,
|
||
green, hypno, powerlifter) are drawn in code too: type shows in body colour plus a face detail
|
||
(shut eyes and a "z", spiral eye-rings, heavy brows), so none of them depends on hue alone.
|
||
|
||
---
|
||
|
||
## Sources for original-game behaviour
|
||
|
||
- [Wikipedia](https://en.wikipedia.org/wiki/Pudding_Monsters) — core slide/fuse mechanic.
|
||
- [Jay is Games review](https://jayisgames.com/review/pudding-monsters.php) — ice blocks, buttons
|
||
and bricks, star-tile rule.
|
||
- [PS4Blog Switch review](https://www.ps4blog.net/2022/03/nintendo-switch-pudding-monsters-review/)
|
||
— sleeping monsters, green slime trails.
|
||
- [LadiesGamers review](https://ladiesgamers.com/pudding-monsters-review/) — tunnels/cloning,
|
||
star tiles, 125 levels.
|
||
- [Pocket Gamer chapter 1 guide](https://www.pocketgamer.com/pudding-monsters/pudding-monsters-3-star-walkthrough-guides-for-chapter-1-escape-the-fridge/)
|
||
— level names, which mechanic arrives when.
|
||
- [VGTimes achievement list](https://vgtimes.com/games/pudding-monsters/achievements-and-trophies/)
|
||
— the definitive element roster (springs, magnets, tunnels, powerlifters, synchronous monsters,
|
||
separation, crowns).
|