glytch/fktry/src/screen/strain.ts
type-two 4803b6a1e7 [screen] signal strain: dread measured in seconds of reserve
Per the v3 units ruling, stored is bandwidth-seconds and draw-gen is bandwidth
per second, so stored/(draw-gen) is literally how long until the lights go out.
Tremor ramps over the last 8 seconds. That reads correct at every factory size
for free: 1000/s with 4000 banked feels exactly as doomed as 10/s with 40.

A factory in surplus is always calm, which is also what keeps the boot state
(gen=draw=stored=0) sterile by construction rather than by special case.
Fever averages only the machines that run hot, so one glowing decoder is not
drowned by a sea of cold belts.

Pure and snapshot-shaped so the sterile guarantee is a test, not a promise.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 18:20:50 +10:00

67 lines
2.4 KiB
TypeScript

/**
* Signal strain — the continuous half of THE SCREEN's vocabulary.
*
* Events give edges (brownout on/off); the snapshot gives quantities. Strain is what the
* factory feels BEFORE the seizure: the reserve draining toward empty, the decoders running
* hot. Strain is weather; the corruption ledger is climate. It stays deliberately subordinate.
*
* Pure and snapshot-shaped on purpose: no DOM, no GL, so the "healthy factory stays sterile"
* guarantee is a unit test rather than a promise.
*/
import type { SimSnapshot } from '../contracts';
export interface Strain {
/** 0..1 tremor/desync judder as the bandwidth reserve runs out */
tremor: number;
/** 0..1 slow feverish shimmer from aggregate machine heat */
fever: number;
}
export const NO_STRAIN: Strain = { tremor: 0, fever: 0 };
/**
* How much runway counts as calm. Reserve deeper than this reads as healthy; dread ramps up
* as the factory's remaining seconds fall toward zero.
*/
export const STRAIN_HORIZON_SECONDS = 8;
/** Machines idling at a trace of heat are not a fever. */
const HEAT_FLOOR = 0.01;
const clamp01 = (x: number) => Math.min(1, Math.max(0, x));
/**
* Read-only peek at the snapshot (contracts v3). The snapshot is never retained: everything
* needed collapses to two scalars here and now.
*/
export function computeStrain(snap?: SimSnapshot): Strain {
if (!snap) return NO_STRAIN;
// --- tremor: seconds of reserve remaining at the current deficit.
// Per the v3 units ruling, `stored` is bandwidth-SECONDS while gen/draw are bandwidth per
// second — so stored/deficit is literally "how long until the lights go out".
const { gen, draw, stored } = snap.bandwidth;
const deficit = draw - gen;
let tremor = 0;
if (deficit > 0) {
const secondsLeft = Math.max(0, stored) / deficit;
tremor = 1 - clamp01(secondsLeft / STRAIN_HORIZON_SECONDS);
}
// A factory in surplus is calm no matter how little it has banked — that is what keeps
// strain out of the NOTHING IS WRONG era, where gen and draw are both zero.
// --- fever: how hot are the machines that actually run hot.
// Averaging across every entity would drown a glowing decoder in a sea of cold belts.
let sum = 0;
let hot = 0;
for (const e of snap.entities) {
if (e.heat > HEAT_FLOOR) {
sum += e.heat;
hot++;
}
}
const fever = hot > 0 ? clamp01(sum / hot) : 0;
return { tremor, fever };
}