Natural peanut butter stratifies while it sits. The jar carries the state:
strat 0..1, set per order (smooth 0.75, crunchy 0.9 — chunks sink). The first
unstirred dip takes the oil slick off the top; the ones after it dig into what
the oil left behind. Verified dip trace, no stirring: 0.875, 0.669, 0.462,
0.256. Swirl the knife in the jar (~2.5 circles of angular sweep; holding still
stirs nothing) and every dip is 0.5. Stirring costs time, and time costs toast
warmth — the mechanic pays the same tax as everything else.
Consistency pushes back through the physics that already exist: an oily load
multiplies the yield down (glides at any angle, goes on thin), a dry one
multiplies it up (fights like cold butter, tears). The shader shows it — slick
is dark and shiny, grout is pale and matte.
The judge reads what LANDED, not what the jar was: every deposit remembers the
oil it landed at, and the criterion scores the mass-weighted mean AND stdev.
The stdev is the design catch, found by verification: a slick strip and a grout
strip average to 0.54 — "just right" — and the first cut scored the lazy play
as if the jar had been stirred. Nobody eats the average:
never stirred, strips oil 0.54 ±0.23 "slick here, grout there" C
one slick dip, whole oil 0.87 ±0.00 "a slick"
stirred first oil 0.50 ±0.00 "just right"
Crunchy reuses the particle system wholesale — chunks ride the knife, shed per
stroke-distance, get judged with the same Clark–Evans index ("Peanut chunks —
21 bits, evenly strewn (R 1.20)"), and the rind line bank is genericised via a
{bits} placeholder so he complains correctly about either.
Day 6 is crunchy now ("the PROPER kind"), the ticket warns when a jar has been
sitting, and the JAR bar shows how mixed it is. Five new judge lines, including
"You did not stir the jar. The jar knows. I know."
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
148 lines
6.3 KiB
Markdown
148 lines
6.3 KiB
Markdown
# TOASTSIM
|
||
|
||
Bread goes in. You are judged.
|
||
|
||
A browser game about making toast, where the inputs are analog and the output is
|
||
scored by a man who has been doing this for thirty-one years.
|
||
|
||
```bash
|
||
npm install
|
||
npm run dev # http://localhost:5173
|
||
```
|
||
|
||
## The loop
|
||
|
||
**Order → toast → drawer → spread → verdict → next day.**
|
||
|
||
1. **The ticket** tells you what someone wants: how dark, which spread, how much,
|
||
which utensil, and whether they'll tolerate burnt bits.
|
||
2. **The toaster** has a dial and a lever and no timer. You go by smell.
|
||
3. **The drawer** makes you find the right piece of cutlery in a tangle of
|
||
similar ones — while your toast goes cold.
|
||
4. **The bench** is where the game actually lives. See below.
|
||
5. **The judge** scores nine criteria and has something to say about the worst one.
|
||
|
||
## The mechanic
|
||
|
||
One control — **the knife's angle** (mouse wheel) — and three behaviours fall out
|
||
of it:
|
||
|
||
| angle | contact | what happens |
|
||
|---|---|---|
|
||
| flat | wide, low pressure | spreads — *or tears the bread, if the spread is stiffer than the pressure you're allowed to make* |
|
||
| steep | narrow, high pressure | scrapes spread back off, or lifts char off burnt toast |
|
||
| steep + fast, on bread with nothing left to take | | **gouges** |
|
||
|
||
The trap: pressure comes from steepness, but past the scrape threshold a steep
|
||
knife stops spreading. So a spread can demand more pressure than spreading mode
|
||
can physically give:
|
||
|
||
| situation | yield | angle needed | scrape cliff at 0.62 |
|
||
|---|---|---|---|
|
||
| fridge butter / cold toast | 0.72 | 0.75 | **impossible** |
|
||
| bench butter / cold toast | 0.54 | 0.57 | one click from disaster |
|
||
| fridge butter / fresh toast | 0.36 | 0.38 | fine |
|
||
| soft butter / fresh toast | 0.16 | 0.12 | dream mode |
|
||
|
||
**Cold toast with hard butter cannot be spread at any angle.** The way out isn't
|
||
technique — it's not dawdling in the drawer. That's why the drawer is there. The
|
||
pressure gauge draws both marks so you can watch the gold sit past the red and
|
||
understand exactly why you're losing.
|
||
|
||
Scraping is the other tension: it rescues burnt toast and wrecks evenness doing
|
||
it (char 99.9% → 54.5%, evenness 0.029 → 0.226). The judge notices both.
|
||
|
||
And MITEY is its own skill: spread it on, then take almost all of it back off. He
|
||
wants to see the toast *through* it.
|
||
|
||
Peanut butter comes smooth or crunchy, and both are *natural* — the jar
|
||
separates while it sits. The oil rises; the paste underneath dries out. An
|
||
unstirred first dip is a slick (flows at almost no pressure, goes on thin and
|
||
shiny), the dips after it are grout (fights like cold butter). Swirl the knife
|
||
in the jar — about two and a half circles — to mix it back, and yes, stirring
|
||
costs time, and time costs toast warmth. The judge reads the mass-weighted mean
|
||
*and spread* of the consistency that actually landed: `oil 0.50 ±0.00 — just
|
||
right` versus `oil 0.54 ±0.23 — slick here, grout there`, because a slick strip
|
||
and a grout strip average to "correct" and nobody eats the average. Crunchy
|
||
additionally has chunks, and the chunks are judged like rind (below).
|
||
|
||
Marmalade adds a second axis: the rind. Bits drop off the blade **per unit of
|
||
stroke distance**, so a dabbing knife makes a pile and a moving knife lays a
|
||
trail — and because the shed rate scales with what's left on the blade, one dip
|
||
can't cover the slice. The judge scores the scatter with the Clark–Evans
|
||
nearest-neighbour index (R ≈ 0 one clump, 1 random, above 1 deliberately even)
|
||
and quotes it on the card: a corner dab reads *"one clump (R 0.24)"*, three
|
||
well-placed dips read *"evenly strewn (R 1.17)"*. Scraping picks rind back up,
|
||
so a clump can be argued with rather than started over.
|
||
|
||
## The bread
|
||
|
||
Same dial, same 18 seconds, four breads:
|
||
|
||
| bread | 4s | 9s | 18s | char |
|
||
|---|---|---|---|---|
|
||
| white | 0.22 | 0.54 | 1.08 | 93% |
|
||
| multigrain | 0.20 | 0.52 | 1.06 | 92% |
|
||
| raisin | 0.30 | 0.76 | 1.14 | **99.8%** |
|
||
| sourdough | 0.11 | 0.33 | 0.74 | **14%** |
|
||
|
||
Sourdough's water has to boil off before the crust can brown, so it stalls and
|
||
then accelerates hardest. Raisin bread is sugar: it browns early and burns early.
|
||
|
||
## How it's built
|
||
|
||
- **Vite + TypeScript + three.js**, **Rapier** for the drawer. No framework; the
|
||
UI is a DOM overlay over one WebGL canvas.
|
||
- The slice is the whole game's canvas: an extruded loaf silhouette with four
|
||
128×128 scalar fields — `browning`, `dryness`, `spread`, `damage` — packed into
|
||
one RGBA8 texture per frame and composited by a custom shader. Everything the
|
||
player does writes a field; everything the judge reads is a statistic over one.
|
||
- Cutlery is procedural on purpose. The drawer asks you to tell a dessert fork
|
||
from a dinner fork, and that's only fair if the silhouettes are authored.
|
||
- Physics: compound boxes, never trimeshes. Grabbing is a capped PD spring, so a
|
||
snagged piece fights what's lying on it instead of tunnelling through.
|
||
|
||
### Layout
|
||
|
||
```
|
||
src/
|
||
core/ app + loop, input, seeded rng, 2D field, synthesised audio
|
||
sim/ bread, slice (geometry/fields/shader), toasting, spreads,
|
||
spreading (the star mechanic), cutlery
|
||
scenes/ kitchen (toast + spread), drawer (rapier), judge, props
|
||
game/ game loop, orders, judging rubric, the judge's lines
|
||
ui/ hud, title, cutlery silhouettes
|
||
dev.ts dev-only harness — drives real gestures deterministically
|
||
```
|
||
|
||
### Dev harness
|
||
|
||
The game is driven by mouse gestures over a 3D scene, which makes "does it work"
|
||
hard to answer by inspection. In a dev build, `window.__t` drives the real code
|
||
paths at a chosen `dt`:
|
||
|
||
```js
|
||
__t.toast(10, 6) // lever down, 10s at power 6, pop, land
|
||
__t.spread(9, 0.55) // dip and raster the slice at 0.55 units/sec
|
||
__t.stats() // every field statistic the judge will read
|
||
```
|
||
|
||
It's also the escape hatch for headless browsers that never fire rAF —
|
||
everything calls `app.step(dt)` directly.
|
||
|
||
## Assets
|
||
|
||
Every asset was generated on-device and free via
|
||
[MODELBEAST](../MODELBEAST/AGENTS.md), and is reproducible:
|
||
|
||
```bash
|
||
./scripts/gen-assets.sh # fixed seeds; skips anything already present
|
||
```
|
||
|
||
`flux_local` → `bg_remove_local` → `hunyuan3d_mlx`. The judge's five expressions
|
||
are one fixed seed and one fixed description with only the expression phrase
|
||
varying, which is why he stays the same man while his face falls.
|
||
|
||
The toaster is procedural rather than the generated GLB — it needs a lever we can
|
||
actually drive, and a dial that turns.
|