import { describe, expect, it } from 'vitest'; import type { EntityState, SimSnapshot } from '../contracts'; import { computeStrain, STRAIN_HORIZON_SECONDS } from './strain'; function snap(bandwidth: Partial, entities: Partial[] = []): SimSnapshot { return { tick: 0, paused: false, entities: entities.map((e, i) => ({ id: i, def: 'x', pos: { x: 0, y: 0 }, dir: 0, recipe: null, progress: 0, inputBuf: {}, outputBuf: {}, jammed: null, heat: 0, ...e, })), beltItems: [], bandwidth: { gen: 0, draw: 0, stored: 0, brownout: false, ...bandwidth }, shippedTotal: {}, activeCommission: null, commissionProgress: {}, }; } describe('the NOTHING IS WRONG era stays sterile', () => { it('is inert with no snapshot at all', () => { expect(computeStrain(undefined)).toEqual({ tremor: 0, fever: 0 }); }); it('does not leak strain into a fresh factory (gen=draw=stored=0)', () => { // The boot state. Nothing built, nothing drawing: dread here would be a lie. expect(computeStrain(snap({}))).toEqual({ tremor: 0, fever: 0 }); }); it('stays calm for a healthy factory running a surplus', () => { expect(computeStrain(snap({ gen: 100, draw: 40, stored: 500 }))).toEqual({ tremor: 0, fever: 0 }); }); it('stays calm in surplus even with an empty reserve', () => { // Nothing is draining, so there is nothing to dread — banked reserve is irrelevant. expect(computeStrain(snap({ gen: 100, draw: 100, stored: 0 })).tremor).toBe(0); }); it('stays calm with cold machines', () => { expect(computeStrain(snap({ gen: 10, draw: 5 }, [{ heat: 0 }, { heat: 0 }])).fever).toBe(0); }); }); describe('tremor tracks seconds-of-reserve, not raw stored', () => { it('is silent while the reserve outlasts the horizon', () => { // deficit 10/s, 200 banked = 20s of runway: further out than the horizon. expect(computeStrain(snap({ gen: 0, draw: 10, stored: 200 })).tremor).toBe(0); }); it('is total when the reserve is gone and the draw continues', () => { expect(computeStrain(snap({ gen: 0, draw: 10, stored: 0 })).tremor).toBe(1); }); it('sits halfway at half the horizon', () => { const stored = 10 * (STRAIN_HORIZON_SECONDS / 2); // deficit 10/s -> horizon/2 seconds left expect(computeStrain(snap({ gen: 0, draw: 10, stored })).tremor).toBeCloseTo(0.5, 5); }); it('rises monotonically as the reserve drains', () => { let prev = -1; for (let stored = 200; stored >= 0; stored -= 10) { const t = computeStrain(snap({ gen: 0, draw: 10, stored })).tremor; expect(t).toBeGreaterThanOrEqual(prev); prev = t; } expect(prev).toBe(1); }); it('reads the same dread from a big factory as a small one at equal runway', () => { // A megafactory bleeding 1000/s with 4000 banked is in exactly as much trouble as a // shack bleeding 10/s with 40 banked. Seconds, not units, is the honest measure. const big = computeStrain(snap({ gen: 0, draw: 1000, stored: 4000 })).tremor; const small = computeStrain(snap({ gen: 0, draw: 10, stored: 40 })).tremor; expect(big).toBeCloseTo(small, 5); }); it('never leaves 0..1, even absurdly overdrawn', () => { const t = computeStrain(snap({ gen: 0, draw: 1e9, stored: -50 })).tremor; expect(t).toBeGreaterThanOrEqual(0); expect(t).toBeLessThanOrEqual(1); }); }); describe('fever reads the machines that actually run hot', () => { it('is not diluted by a sea of cold belts', () => { // One decoder at 0.9 surrounded by 50 cold belts is a fever, not a rounding error. const entities = [{ heat: 0.9 }, ...Array.from({ length: 50 }, () => ({ heat: 0 }))]; expect(computeStrain(snap({}), ).fever).toBe(0); expect(computeStrain(snap({}, entities)).fever).toBeCloseTo(0.9, 5); }); it('averages across the hot machines', () => { expect(computeStrain(snap({}, [{ heat: 0.4 }, { heat: 0.8 }])).fever).toBeCloseTo(0.6, 5); }); it('stays within 0..1 for a fully scrammed factory', () => { const f = computeStrain(snap({}, [{ heat: 1, scrammed: true }, { heat: 1 }])).fever; expect(f).toBe(1); }); });