Content (verified — same dial, same 18s, four breads that disagree): 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 stalls at half the rate then accelerates hardest (x3.0 vs white's x2.45) as its water boils off; raisin browns 40% faster and is essentially carbon by 18s. Seven handwritten days then procedural, day persisted. Assets, all generated on-device and free, all reproducible from fixed seeds: - the judge's five expressions (one seed, one description, only the expression phrase varying — he stays the same man while his face falls) - title art, wooden bench, blurred kitchen backdrop - butter dish / peanut butter / MITEY jars as GLBs, normalised on load (recentre, scale to fit, sit on y=0) rather than hand-fixed, so regenerating them doesn't mean re-fixing them The toaster stays procedural: the generated one is prettier but this one has a lever we can drive and a dial that turns. Judgment call, per the brief. Sound is synthesised, no samples: clatter is driven by real Rapier contact-force events and scaled by force, which is the only way a drawer of cutlery can sound like one. Plus lever clunk, knife scrape pitched by pressure, and the stamp. Perf, measured (16.7ms is the 60fps budget): kitchen 0.99ms/step · drawer 0.12ms · drawer while dragging 0.21ms Production build: 2.87MB / 1.0MB gzipped, mostly Rapier's inlined wasm + three. Textures carry colorSpace = SRGBColorSpace explicitly — same class of bug as the albedo gamma fix in M3, and it washes them out silently if you forget. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
128 lines
5.0 KiB
Markdown
128 lines
5.0 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.
|
||
|
||
## 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.
|