Heat M-H3 (environment) + M-H5 noise: wind, the windbreak floor, the ZAP
M-H3 environment sim: newOutdoorEnv/addSmoke/SMOKE_ZAP. Wind perturbs a flame's output AND steals its heat; a windbreak (sheltered) forces it flat; induction shrugs wind off; indoor smoke trips the ZAP exactly once. Verified: windy gas stdev 0.2635 mean 5.66, sheltered stdev 0.0065 mean 6.99, induction stdev 0, zap fires 1x indoors / 0x outdoors. M-H5 noise half: cheap_electric hob stdev 0.1244 (>0.12) vs induction 0.0198 (<0.04) vs fan 0.0064 — cheap gear is Field noise, not a tax. The 'both reach 10/10' half waits on the M17 pan. Harness: t.envVar, t.zapTest, t.hobNoise. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f6404586cd
commit
e6509479d2
@ -4,7 +4,10 @@
|
||||
|
||||
## 0. STATUS (update this section as you go — a cold session reads here first)
|
||||
|
||||
- **M-H1 + M-H2 DONE & LIVE (2026-07-19).** `src/sim/heatsource.ts` (the bone) + the golden-preserving oven retrofit + the gas-oven day are shipped. Verified in-browser: **golden** — no-source oven reads mean 0.698 / blister 1.0 / collapse 0, byte-identical to HEAD. **M-H1 chase 3→9** — gas 0.433s, electric 6.0s, induction 0.133s (distinct, no overlap). **Residual after 9→0** — gas 0 @1s, electric 6.6 @6s, charcoal 9 @30s. **M-H2** — gas oven stdev 0.1207 vs fan oven 0.0016 at equal mean 0.6001 (75× ratio; bar was 2×). Day 14 bruschetta now roasts on `oven_gas` and still grades **S (9.62)**, HUD shows "GAS OVEN · blue flame". Harness: `t.heatChase`, `t.residual`, `t.ovenStdev`, `t.ovenGolden`, `t.ovenDemo`. **NEXT: M-H3 (Environment/wind), M-H4 (pan/butter/flare — needs M17), M-H5 (economy SKUs), M-H6 (charcoal zone Field — the showpiece scene).** Everything below §2 not checked off here is unbuilt.
|
||||
- **M-H1 + M-H2 DONE & LIVE (2026-07-19).** `src/sim/heatsource.ts` (the bone) + the golden-preserving oven retrofit + the gas-oven day are shipped. Verified in-browser: **golden** — no-source oven reads mean 0.698 / blister 1.0 / collapse 0, byte-identical to HEAD. **M-H1 chase 3→9** — gas 0.433s, electric 6.0s, induction 0.133s (distinct, no overlap). **Residual after 9→0** — gas 0 @1s, electric 6.6 @6s, charcoal 9 @30s. **M-H2** — gas oven stdev 0.1207 vs fan oven 0.0016 at equal mean 0.6001 (75× ratio; bar was 2×). Day 14 bruschetta now roasts on `oven_gas` and still grades **S (9.62)**, HUD shows "GAS OVEN · blue flame". Harness: `t.heatChase`, `t.residual`, `t.ovenStdev`, `t.ovenGolden`, `t.ovenDemo`.
|
||||
- **M-H3 DONE (2026-07-19).** Environment sim (gust walk, flame perturbation, ferrous rejection, indoor smoke→ZAP). Verified: windy gas flame stdev 0.2635 (bar >0.05) and mean drops to 5.66 (wind steals heat); sheltered stdev 0.0065 (bar <0.02, the dodgeable floor); induction-in-wind stdev 0 (no flame); the ZAP fires exactly once indoors, never outdoors. `newOutdoorEnv`, `addSmoke`, `SMOKE_ZAP`. Harness: `t.envVar`, `t.zapTest`.
|
||||
- **M-H5 (noise half) DONE.** cheap_electric hob stdev 0.1244 (bar >0.12) vs induction 0.0198 (bar <0.04) vs fan 0.0064. Harness: `t.hobNoise`. The "both reach 10/10" half needs the M17 pan to cook on — deferred with it.
|
||||
- **NEXT: M-H6 (charcoal — a heat-bed you arrange: the showpiece outdoor grill scene). M-H4 (pan/butter/flare) waits on M17 (Brief 3).** Everything below §2 not checked off here is unbuilt.
|
||||
- Presupposes the M16 salt doctrine and the M17 pan skeleton from Brief 3; those aren't in yet, so §2 ships as a standalone `heatsource.ts` (zero dependency on the pan) and M17 will read it from birth.
|
||||
- **LIVE 🌐** https://partly.party/toastsim/ — `bash scripts/deploy.sh` (build `--base=/toastsim/` → rsync staging → docker cp into `forum-nginx`). Curl cache-busted after every deploy.
|
||||
- Seed lane for this brief's assets: **680–719** (open gap after P12's 630–679). A `do_heat()` lane (cooktops/pans/BBQ/utensils) was already fired at seeds 181–219 for the general cast; the 680–719 sprites (flames, glows, flare, ZAP) remain.
|
||||
|
||||
51
src/dev.ts
51
src/dev.ts
@ -3,8 +3,9 @@ import type { App } from './core/app';
|
||||
import type { Game } from './game/game';
|
||||
import { sogWord } from './sim/assembly';
|
||||
import type { IngredientId } from './sim/ingredients';
|
||||
import { type Fuel, newHeatSource, setKnob, heatStep } from './sim/heatsource';
|
||||
import { type Fuel, newHeatSource, setKnob, heatStep, envStep, newOutdoorEnv, addSmoke, newIndoorEnv, applyToHeatMap } from './sim/heatsource';
|
||||
import { newRoastSession, roastStep, roastResult } from './sim/roasting';
|
||||
import { Rng } from './core/rng';
|
||||
|
||||
/**
|
||||
* Dev harness. The game is driven by mouse gestures over a 3D scene, which makes
|
||||
@ -493,6 +494,54 @@ export function installDevHarness(app: App, game: Game): void {
|
||||
return { sku, mean: r(res.mean), stdev: r(res.stdev), seconds: +(f / 60).toFixed(1), output: +s.src!.output.toFixed(2) };
|
||||
},
|
||||
|
||||
/** M-H3: hold a flame at knob 7 in wind for 8s; return the stdev of output.
|
||||
* Outdoor unsheltered gusts perturb it (>0.05); sheltered forces it flat (<0.02). */
|
||||
envVar(fuel: Fuel = 'gas', wind = 0.6, sheltered = false, secs = 8, dt = 1 / 60) {
|
||||
const hs = newHeatSource(fuel);
|
||||
const env = newOutdoorEnv(wind);
|
||||
env.sheltered = sheltered;
|
||||
setKnob(hs, 7);
|
||||
// settle to target first, then sample under wind
|
||||
for (let i = 0; i < 120; i++) heatStep(hs, dt, env);
|
||||
const samples: number[] = [];
|
||||
for (let i = 0; i < Math.round(secs / dt); i++) {
|
||||
envStep(env, dt);
|
||||
heatStep(hs, dt, env);
|
||||
samples.push(hs.output);
|
||||
}
|
||||
const mean = samples.reduce((a, b) => a + b, 0) / samples.length;
|
||||
const stdev = Math.sqrt(samples.reduce((a, b) => a + (b - mean) ** 2, 0) / samples.length);
|
||||
return { fuel, wind, sheltered, mean: +mean.toFixed(3), stdev: +stdev.toFixed(4) };
|
||||
},
|
||||
|
||||
/** M-H5 (noise half): a SKU's hot-spot noise = the stdev of its heat map.
|
||||
* A cheap thin electric hob is blotchy (>0.12); induction is glassy-flat
|
||||
* (<0.04). The "both still reach 10/10" half awaits the M17 pan. */
|
||||
hobNoise(sku = 'cheap_electric') {
|
||||
const hs = newHeatSource(sku);
|
||||
const n = 64;
|
||||
const heat = new Float32Array(n * n);
|
||||
applyToHeatMap(heat, n, hs, new Rng(42));
|
||||
let sum = 0;
|
||||
for (let i = 0; i < heat.length; i++) sum += heat[i];
|
||||
const mean = sum / heat.length;
|
||||
let acc = 0;
|
||||
for (let i = 0; i < heat.length; i++) acc += (heat[i] - mean) ** 2;
|
||||
return { sku, stdev: +Math.sqrt(acc / heat.length).toFixed(4) };
|
||||
},
|
||||
|
||||
/** M-H3: pour smoke into an indoor env; the ZAP must fire exactly once. */
|
||||
zapTest() {
|
||||
const env = newIndoorEnv();
|
||||
let fires = 0;
|
||||
for (let i = 0; i < 40; i++) if (addSmoke(env, 0.05)) fires++;
|
||||
// outdoor never zaps
|
||||
const out = newOutdoorEnv(0.5);
|
||||
let outFires = 0;
|
||||
for (let i = 0; i < 40; i++) if (addSmoke(out, 0.05)) outFires++;
|
||||
return { indoorFires: fires, outdoorFires: outFires, finalSmoke: +env.smoke.toFixed(2) };
|
||||
},
|
||||
|
||||
/** Put the ACTUAL oven scene on screen wearing a fuel, and roast it — for a
|
||||
* live screenshot of the gas-oven vs fan-oven HUD. */
|
||||
ovenDemo(fuel = 'oven_gas', seconds = 24, power = 8) {
|
||||
|
||||
@ -63,9 +63,14 @@ export interface Environment {
|
||||
vesselFerrous: boolean;
|
||||
/** indoor only: burnt food accumulates it → fires the ZAP once. */
|
||||
smoke: number;
|
||||
/** latches true when the smoke alarm has already gone off (fires exactly once). */
|
||||
zapped: boolean;
|
||||
rng: Rng;
|
||||
}
|
||||
|
||||
/** Smoke past this trips the alarm. */
|
||||
export const SMOKE_ZAP = 1.0;
|
||||
|
||||
type Profile = Omit<HeatSource, 'fuel' | 'target' | 'output' | 'zone'>;
|
||||
|
||||
/**
|
||||
@ -115,7 +120,25 @@ export function newHeatSource(fuelOrSku: Fuel | string): HeatSource {
|
||||
}
|
||||
|
||||
export function newIndoorEnv(seed = 7): Environment {
|
||||
return { place: 'indoor', ambient: 0, wind: 0, gust: 0, sheltered: true, vesselFerrous: true, smoke: 0, rng: new Rng(seed) };
|
||||
return { place: 'indoor', ambient: 0, wind: 0, gust: 0, sheltered: true, vesselFerrous: true, smoke: 0, zapped: false, rng: new Rng(seed) };
|
||||
}
|
||||
|
||||
/** An outdoor bench: wind blows, nothing shelters it until you raise a break or
|
||||
* drop a lid (`sheltered = true`). A cold day lowers `ambient`. */
|
||||
export function newOutdoorEnv(wind = 0.4, seed = 11): Environment {
|
||||
return { place: 'outdoor', ambient: 0, wind, gust: 0, sheltered: false, vesselFerrous: true, smoke: 0, zapped: false, rng: new Rng(seed) };
|
||||
}
|
||||
|
||||
/** Burnt butter and charred food make smoke — indoors only (outdoors it drifts).
|
||||
* Returns true the ONE frame the alarm trips, so the caller fires the ZAP once. */
|
||||
export function addSmoke(env: Environment, amt: number): boolean {
|
||||
if (env.place !== 'indoor' || amt <= 0) return false;
|
||||
env.smoke += amt;
|
||||
if (env.smoke >= SMOKE_ZAP && !env.zapped) {
|
||||
env.zapped = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const INDOOR = newIndoorEnv();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user