fertig-classic-games/docs/puddingmonsters-mechanics-p...

524 lines
32 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.

# 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** (56 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 140 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 12 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 140 are byte-identical to before; 4160 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 35 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 160 byte-identical; 6180 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 140 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 | 115 | sleepers, ice |
| 2 | Kitchen Counter | 1630 | slime, hypno |
| 3 | The Pantry | 3145 | springs, tunnels |
| 4 | Dining Room | 4660 | buttons+bricks, powerlifters+crates |
| 5 | Midnight Feast | 6175 | 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)` (03) 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 12 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 35 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)` (03), `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).