The near-game, treated with respect (masterplan P3). sim/poach.ts (pure, rides the heat-source bone): the pot chases the knob on the fuel's lag and the poach wants the SHIVER (5.8-7.4) — cooler rags the white on the way in, a rolling boil (>8.2) tears it the whole time. The VORTEX is the reamer-twist at pot scale: swirl builds it, it DECAYS (swirl then drop, not swirl then admire), and the strength at the drop is the judged form. Two-layer set: white first, the yolk only firms once the white is mostly there — lift in the window (white >=0.85, yolk <=0.35) or you've boiled an egg the long way. Then DRAIN on the slotted spoon. Stirring a dropped egg is vandalism (rags it directly). scenes/poach.ts: WHEEL flame, circular-drag swirl (real angular sweep), SPACE drop, L lift, hold-to-drain, ENTER serves; vortex streaks that spin and fade, shiver-vs-boil bubbles, the white gathering tighter as it sets, a hard yolk going pale. judgePoach: THE POACH (White/Wobble/Form) + The Drain; the wobble line is the dossier's: 'It should tremble. Like you, right now.' Day 21 authored + poach in the endless rotation (electric pot past day 27). Verified with REAL input: shiver + 91% vortex + lift-at-tremble + drain = 9.9/10 'it TREMBLES — perfect', 'one neat comet (vortex 91%)'; still water into a rolling boil, overstayed, undrained = 3.6/10, 'a pot of rags', 'it does not move at all'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
185 lines
6.4 KiB
TypeScript
185 lines
6.4 KiB
TypeScript
import { type HeatSource, newHeatSource, setKnob, heatStep } from './heatsource';
|
|
|
|
/**
|
|
* THE POACH — almost a whole game, and treated like one.
|
|
*
|
|
* Four acts, none skippable:
|
|
*
|
|
* THE WATER — the pot chases the knob on the fuel's lag (the heat-source
|
|
* bone). The poach wants the SHIVER — that band where the surface
|
|
* trembles but nothing rolls. Too cool and the white disperses into
|
|
* rags; a rolling boil and it tears the egg apart while it cooks.
|
|
* THE VORTEX — swirl the water first. A good vortex wraps the white around
|
|
* the yolk as it lands; drop into still water and the white walks off
|
|
* on its own. The vortex DECAYS — swirl, then drop, not swirl and
|
|
* admire.
|
|
* THE DROP — from low, into the eye of the spin. Height splats it.
|
|
* THE SET — white sets first, then the yolk starts to firm. Lift when the
|
|
* white is set and the yolk still trembles; every second past that is
|
|
* a second toward a boiled egg in disguise. Then DRAIN it on the
|
|
* spoon — nobody wants poach water on the plate.
|
|
*
|
|
* The wobble test at the pass reads yolkSet: the judge presses it, on camera.
|
|
* "It should tremble. Like you, right now."
|
|
*
|
|
* Pure — no three.js — the harness poaches headless.
|
|
*/
|
|
|
|
/** The shiver: where a poach wants the water. Above BOIL it tears. */
|
|
export const SHIVER_LO = 5.8;
|
|
export const SHIVER_HI = 7.4;
|
|
export const BOIL_AT = 8.2;
|
|
|
|
/** White is SET past here (snotty below ~0.6). */
|
|
export const WHITE_SET = 0.85;
|
|
/** Yolk still trembles below here; past it you've boiled an egg in disguise. */
|
|
export const YOLK_TREMBLE = 0.35;
|
|
|
|
/** How fast the vortex dies once you stop stirring. */
|
|
const VORTEX_DECAY = 0.09;
|
|
|
|
export interface PoachSession {
|
|
src: HeatSource;
|
|
/** Vortex strength 0..1 — built by swirling, decays on its own. */
|
|
vortex: number;
|
|
dropped: boolean;
|
|
lifted: boolean;
|
|
/** 0 raw .. 1 fully set. */
|
|
whiteSet: number;
|
|
/** 0 liquid .. 1 hard. The wobble test reads this. */
|
|
yolkSet: number;
|
|
/** White lost to rags — still water, a high drop, or a rolling boil. */
|
|
rags: number;
|
|
/** Poach water still clinging — drain on the spoon before the pass. */
|
|
cling: number;
|
|
/** Vortex strength at the moment of the drop (the judged number). */
|
|
vortexAtDrop: number;
|
|
tempAtDrop: number;
|
|
seconds: number;
|
|
}
|
|
|
|
export function newPoachSession(fuel = 'gas'): PoachSession {
|
|
return {
|
|
src: newHeatSource(fuel),
|
|
vortex: 0,
|
|
dropped: false,
|
|
lifted: false,
|
|
whiteSet: 0,
|
|
yolkSet: 0,
|
|
rags: 0,
|
|
cling: 1,
|
|
vortexAtDrop: 0,
|
|
tempAtDrop: 0,
|
|
seconds: 0,
|
|
};
|
|
}
|
|
|
|
export function setPotKnob(s: PoachSession, k: number): void {
|
|
setKnob(s.src, k);
|
|
}
|
|
|
|
/** Stir: add angular sweep (radians this frame) into the vortex. */
|
|
export function swirl(s: PoachSession, sweep: number): void {
|
|
if (s.dropped) {
|
|
// Stirring a dropped egg is vandalism — it rags the white directly.
|
|
s.rags = Math.min(1, s.rags + Math.abs(sweep) * 0.06);
|
|
return;
|
|
}
|
|
s.vortex = Math.min(1, s.vortex + Math.abs(sweep) * 0.055);
|
|
}
|
|
|
|
/**
|
|
* Drop the egg from `height` (0 rim-low .. 1 show-off). The vortex catches the
|
|
* white; still water and altitude both cost you form, immediately.
|
|
*/
|
|
export function dropEgg(s: PoachSession, height = 0): void {
|
|
if (s.dropped) return;
|
|
s.dropped = true;
|
|
s.vortexAtDrop = s.vortex;
|
|
s.tempAtDrop = s.src.output;
|
|
s.rags = Math.min(1, s.rags + (1 - s.vortex) * 0.45 + height * 0.4);
|
|
// Cold water at the drop lets the white walk before any heat can catch it.
|
|
if (s.src.output < SHIVER_LO) s.rags = Math.min(1, s.rags + (SHIVER_LO - s.src.output) * 0.12);
|
|
}
|
|
|
|
/** Lift it out on the spoon. The set stops; the drain begins. */
|
|
export function liftEgg(s: PoachSession): void {
|
|
if (s.dropped) s.lifted = true;
|
|
}
|
|
|
|
/** Hold it on the spoon: cling drains away. Patience, again. */
|
|
export function drainStep(s: PoachSession, dt: number): void {
|
|
if (s.lifted) s.cling = Math.max(0, s.cling - dt / 4);
|
|
}
|
|
|
|
export function poachStep(s: PoachSession, dt: number): void {
|
|
heatStep(s.src, dt);
|
|
s.vortex = Math.max(0, s.vortex - VORTEX_DECAY * dt);
|
|
const t = s.src.output;
|
|
if (s.dropped && !s.lifted) {
|
|
// The white sets fastest in the shiver; cooler is slower (and it rags on
|
|
// the way in); a boil sets it fast but TEARS it the whole time.
|
|
const inBand = t >= SHIVER_LO && t <= SHIVER_HI;
|
|
const rate = inBand ? 0.055 : t > SHIVER_HI ? 0.07 : t > 4 ? 0.03 : 0.012;
|
|
s.whiteSet = Math.min(1, s.whiteSet + rate * dt);
|
|
if (t > BOIL_AT) s.rags = Math.min(1, s.rags + (t - BOIL_AT) * 0.045 * dt);
|
|
// The yolk only starts once the white is mostly there.
|
|
if (s.whiteSet > 0.75) s.yolkSet = Math.min(1, s.yolkSet + 0.035 * (inBand ? 1 : 1.4) * dt);
|
|
}
|
|
s.seconds += dt;
|
|
}
|
|
|
|
export interface PoachResult {
|
|
/** Set, not snotty. */
|
|
whiteScore: number;
|
|
/** The wobble: liquid yolk full marks, hard yolk none. */
|
|
yolkScore: number;
|
|
/** One neat comet, not a pot of rags. */
|
|
formScore: number;
|
|
/** Drained on the spoon, or served with its own puddle. */
|
|
drainScore: number;
|
|
whiteSet: number;
|
|
yolkSet: number;
|
|
rags: number;
|
|
cling: number;
|
|
vortexAtDrop: number;
|
|
wobbleWord: string;
|
|
}
|
|
|
|
const clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v);
|
|
|
|
export function poachResult(s: PoachSession): PoachResult {
|
|
const whiteScore = !s.dropped ? 0 : clamp01((s.whiteSet - 0.55) / (WHITE_SET - 0.55));
|
|
const yolkScore = !s.dropped ? 0 : clamp01(1 - Math.max(0, s.yolkSet - YOLK_TREMBLE) / 0.4);
|
|
const formScore = clamp01(1 - s.rags * 1.3);
|
|
const drainScore = clamp01(1 - s.cling * 0.9);
|
|
const wobbleWord =
|
|
!s.dropped ? 'there is no egg'
|
|
: s.yolkSet < 0.15 && s.whiteSet < WHITE_SET ? 'it collapses — raw inside'
|
|
: s.yolkSet <= YOLK_TREMBLE ? 'it TREMBLES — perfect'
|
|
: s.yolkSet < 0.6 ? 'a stiff little nod'
|
|
: 'it does not move at all';
|
|
return {
|
|
whiteScore,
|
|
yolkScore,
|
|
formScore,
|
|
drainScore,
|
|
whiteSet: s.whiteSet,
|
|
yolkSet: s.yolkSet,
|
|
rags: s.rags,
|
|
cling: s.cling,
|
|
vortexAtDrop: s.vortexAtDrop,
|
|
wobbleWord,
|
|
};
|
|
}
|
|
|
|
/** The water's diegetic tell — never a thermometer. */
|
|
export function waterWord(s: PoachSession): string {
|
|
const t = s.src.output;
|
|
if (t < 3) return 'still water';
|
|
if (t < SHIVER_LO) return 'warming — not yet';
|
|
if (t <= SHIVER_HI) return 'a gentle SHIVER — now';
|
|
if (t <= BOIL_AT) return 'simmering hard';
|
|
return 'ROLLING — it will tear';
|
|
}
|