A particulate spread. The jelly is ordinary rheology; the rind is discrete solids that ride the knife and drop off per unit of stroke DISTANCE, not per second — a dabbing knife makes a pile, a moving knife lays a trail, and since the shed rate scales with what's left on the blade, one dip cannot cover the slice. Even rind takes several dips placed with intent. Scraping picks bits back up (60% survive the blade), so a clump can be argued with. The judge scores the scatter with the Clark–Evans nearest-neighbour index over the bread's own area, and quotes it: R near 0 is one clump, 1 is random, above 1 is deliberate. Verified end to end on day 8: dab in a corner 8 bits "one clump (R 0.24)" drags grade to B three placed dips 24 bits "evenly strewn (R 1.17)" A, 8.9/10 First cut shed per-second; even careful strokes dumped the whole load in the first half-second and scored "clumped (R 0.50)". Distance-based shedding is what makes evenness the reward for technique rather than luck. Rind renders as instanced chunks parented to the slice mesh, so it rides the pop, the landing, and the pedestal turn. Points, not a field: the judge wants nearest-neighbour statistics, and those want points. Same sRGB trap as M3, new spot: the instanced material's colour was passed as raw floats, which the renderer reads as linear — candied orange rendered as pale butter. setRGB(..., SRGBColorSpace). New content: day 8 handwritten (Deidre wants a piece in every bite), marmalade in the procedural pool, five rind-district lines for the judge, and a marmalade jar GLB generated on-device like everything else, with a plain-glass fallback while it loads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
137 lines
5.6 KiB
Markdown
137 lines
5.6 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.
|
||
|
||
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.
|