THE CARD: the recipe stops being narrative and becomes law
The multi-cycle timer doctrine from the atlas, first station: the air
fryer's cursed card ('180° until colour · SHAKE · 190° to golden') is
now DATA — deg1, deg2, and a half-time shake — and the sim keeps the
ledger to judge it by: heat-weighted seconds at every knob position,
split into pre-shake and post-shake phases, plus the mean browning at
each shake so 'half-time' is a real place (0.15–0.5) and not a vibe.
A new row on the scorecard, The Card: 40% ran phase one at its degrees,
40% phase two at its, 20% shook in the window. Ignore it entirely and
the detail reads 'no shake. the card wept'; right temps but a mistimed
shake is its own sentence. Procedural wing days now deal varied cards
(170/180 into 190/200).
The best bug of the batch: the verifier caught MY demo driver setting
knob 8 for the finish — that is 200°, and the card says 190°. 'roughly
the card, squinting.' The judge was right and the demonstrator now
obeys: 'as written', 9.5/10.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
29f6b25c5b
commit
782971ee83
@ -710,7 +710,7 @@ export function installDevHarness(app: App, game: Game): void {
|
||||
await F(33); // ~0.5 charge: firm, nothing jumps
|
||||
window.dispatchEvent(new KeyboardEvent('keyup', { code: 'KeyF' }));
|
||||
await F(4);
|
||||
s.knob = 8; // 190 to golden
|
||||
s.knob = 7; // 190 to golden — the card is the LAW
|
||||
tap('Space'); // back in
|
||||
g = 0;
|
||||
while (mean() < 0.66 && g++ < 60 * 90) await F(1);
|
||||
|
||||
@ -647,6 +647,32 @@ export function judgeAirfryer(r: import('../sim/airfryer').FryerResult, order: O
|
||||
weight: 1.1,
|
||||
detail: r.steamed > 2.5 ? 'that is a steamer wearing a fryer badge' : r.steamed > 0.6 ? 'crowded for a while — it shows' : 'every wing breathed',
|
||||
},
|
||||
{
|
||||
key: 'frycard',
|
||||
group: 'fryer',
|
||||
label: 'The Card',
|
||||
...(() => {
|
||||
const deg1 = order.airfryer?.deg1 ?? 180;
|
||||
const deg2 = order.airfryer?.deg2 ?? 190;
|
||||
const k1 = Math.round((deg1 - 120) / 10);
|
||||
const k2 = Math.round((deg2 - 120) / 10);
|
||||
const near = (m: Record<number, number>, k: number) => (m[k] ?? 0) + ((m[k - 1] ?? 0) + (m[k + 1] ?? 0)) * 0.5;
|
||||
const tsum = (m: Record<number, number>) => Object.values(m).reduce((a, b) => a + b, 0);
|
||||
const p1 = tsum(r.preTime) > 1 ? near(r.preTime, k1) / tsum(r.preTime) : 0;
|
||||
const p2 = tsum(r.postTime) > 1 ? near(r.postTime, k2) / tsum(r.postTime) : 0;
|
||||
const halfTime = r.shakes.some((b) => b >= 0.15 && b <= 0.5);
|
||||
const score = clamp01(p1 * 0.4 + p2 * 0.4 + (halfTime ? 0.2 : 0));
|
||||
const detail = r.shakes.length === 0
|
||||
? 'no shake. the card wept'
|
||||
: !halfTime
|
||||
? p1 > 0.7 && p2 > 0.7 ? 'right temps, wrong moment to shake' : 'shaken, but the temps ignored the card'
|
||||
: p1 > 0.7 && p2 > 0.7
|
||||
? `${deg1}° then ${deg2}°, shaken at half-time — as written`
|
||||
: p2 < 0.3 ? `never made the ${deg2}° finish` : 'roughly the card, squinting';
|
||||
return { score, detail };
|
||||
})(),
|
||||
weight: 0.9,
|
||||
},
|
||||
{
|
||||
key: 'frycount',
|
||||
group: 'bench',
|
||||
|
||||
@ -119,8 +119,9 @@ export interface Order {
|
||||
/** Eggs benedict: toast the bread, then poach the egg and land it ON the
|
||||
* toast — one plate, two crafts, one verdict. */
|
||||
benedict?: { fuel: string };
|
||||
/** THE AIR FRYER: arrangement is the skill. The card is the law. */
|
||||
airfryer?: { count: number; card: string };
|
||||
/** THE AIR FRYER: arrangement is the skill. The card is the LAW — two
|
||||
* temperature phases with a shake between, and the judge checks the ledger. */
|
||||
airfryer?: { count: number; card: string; deg1: number; deg2: number };
|
||||
}
|
||||
|
||||
export const BROWNING_NAMES: [number, string][] = [
|
||||
@ -505,7 +506,7 @@ export const DAYS: Order[] = [
|
||||
butterSoftness: 0.5,
|
||||
who: 'the wing man',
|
||||
text: 'Six wings in the air fryer. If they touch, they steam — and warm is not fried. Give every wing its air, SHAKE at half-time for the pale faces, and count them out: what jumps the rack is mine to look at.',
|
||||
airfryer: { count: 6, card: 'THE CARD: 180\u00b0 until they colour \u00b7 drawer OUT, SHAKE \u00b7 190\u00b0 to golden \u00b7 all six present' },
|
||||
airfryer: { count: 6, deg1: 180, deg2: 190, card: 'THE CARD: 180\u00b0 until they colour \u00b7 drawer OUT, SHAKE \u00b7 190\u00b0 to golden \u00b7 all six present' },
|
||||
},
|
||||
];
|
||||
|
||||
@ -539,7 +540,9 @@ function proceduralAirfryer(day: number, rand: () => number): Order {
|
||||
pickOf(rand, ['the wing man', 'the lunch rush', 'a regular', 'someone new']),
|
||||
`${count} wings, air-fried. Air between every piece, a proper SHAKE at half-time, nothing on my floor.`,
|
||||
);
|
||||
o.airfryer = { count, card: `THE CARD: 180\u00b0 until colour \u00b7 SHAKE \u00b7 190\u00b0 to golden \u00b7 all ${count} present` };
|
||||
const deg1 = 170 + Math.floor(rand() * 2) * 10; // 170 or 180
|
||||
const deg2 = 190 + Math.floor(rand() * 2) * 10; // 190 or 200
|
||||
o.airfryer = { count, deg1, deg2, card: `THE CARD: ${deg1}\u00b0 until colour \u00b7 SHAKE \u00b7 ${deg2}\u00b0 to golden \u00b7 all ${count} present` };
|
||||
return o;
|
||||
}
|
||||
|
||||
|
||||
@ -47,6 +47,12 @@ export interface FryerSession {
|
||||
steamed: number;
|
||||
/** Shakes that went badly (pieces out). */
|
||||
lostCount: number;
|
||||
/** Heat-weighted seconds spent at each knob BEFORE the first shake... */
|
||||
preTime: Record<number, number>;
|
||||
/** ...and after it — the card judges the two phases separately. */
|
||||
postTime: Record<number, number>;
|
||||
/** Mean browning at each shake — 'half-time' is a real place. */
|
||||
shakes: number[];
|
||||
rng: Rng;
|
||||
}
|
||||
|
||||
@ -63,7 +69,7 @@ export function newFryerSession(count = 6, seed = 20260721): FryerSession {
|
||||
lost: false,
|
||||
});
|
||||
}
|
||||
return { pieces, knob: 0, heat: 0, drawerOpen: true, seconds: 0, steamed: 0, lostCount: 0, rng };
|
||||
return { pieces, knob: 0, heat: 0, drawerOpen: true, seconds: 0, steamed: 0, lostCount: 0, preTime: {}, postTime: {}, shakes: [], rng };
|
||||
}
|
||||
|
||||
export function setFryerKnob(s: FryerSession, k: number): void {
|
||||
@ -113,6 +119,10 @@ export function fryStep(s: FryerSession, dt: number): void {
|
||||
const over = Math.max(0, smother - SMOTHER_AT) / (1 - SMOTHER_AT);
|
||||
const steamFactor = 1 - over * 0.85; // crowded = wet heat, no crisp
|
||||
if (over > 0) s.steamed += over * dt * heatN * 2;
|
||||
// The card's ledger: where the heat actually went, phase by phase.
|
||||
const k = Math.round(s.knob);
|
||||
const bucket = s.shakes.length ? s.postTime : s.preTime;
|
||||
bucket[k] = (bucket[k] ?? 0) + dt * heatN;
|
||||
for (let i = 0; i < s.pieces.length; i++) {
|
||||
const p = s.pieces[i];
|
||||
if (p.lost) continue;
|
||||
@ -147,6 +157,8 @@ export function shake(s: FryerSession, strength: number): { redistributed: boole
|
||||
p.y = Math.max(PIECE_R, Math.min(TRAY - PIECE_R, p.y + (s.rng.next() - 0.5) * 0.3 * st));
|
||||
p.pale *= 1 - st * 0.8; // the pale face turns to the wind
|
||||
}
|
||||
const liveNow = s.pieces.filter((p) => !p.lost);
|
||||
s.shakes.push(liveNow.length ? liveNow.reduce((a, p) => a + p.browning, 0) / liveNow.length : 0);
|
||||
s.seconds += 2;
|
||||
return { redistributed: true, jumped };
|
||||
}
|
||||
@ -159,6 +171,9 @@ export interface FryerResult {
|
||||
lost: number;
|
||||
served: number;
|
||||
seconds: number;
|
||||
preTime: Record<number, number>;
|
||||
postTime: Record<number, number>;
|
||||
shakes: number[];
|
||||
}
|
||||
|
||||
export function fryerResult(s: FryerSession): FryerResult {
|
||||
@ -175,6 +190,9 @@ export function fryerResult(s: FryerSession): FryerResult {
|
||||
lost: s.lostCount,
|
||||
served: live.length,
|
||||
seconds: s.seconds,
|
||||
preTime: s.preTime,
|
||||
postTime: s.postTime,
|
||||
shakes: s.shakes,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user