import { describe, expect, it } from 'vitest'; import { BUTTERFINGERS, PUBLICAN, RSA_STRAIGHT, simulateBarShift, type BarMetrics } from './floorShift'; import { CAREFUL, GREEDY, SHOWPONY, simulateNight, type NightMetrics } from './economy'; // The floor economy, as numbers — the twin of economy.test.ts, which only ever // measured doorgirl nights. Eleven floor systems shipped in a day against zero // bot coverage; this is the contract they answer to now. // // What the first run of this file found, for the record: // - the red carpet was an uncapped hype ratchet. Three tolerant patrons parked // all night rode hype to the x3 ceiling, and since Meters scales vibe GAINS // by hype, post-11PM vibe-pin went 0.27 -> 0.71 against a documented <0.5. // Fixed by CARPET_TUNING.hypePerStandCap (the guard queue theatre already // had). The rest of the carpet's shape turned out sound — see 'the fuse'. const SEEDS = [4207, 4208, 4209, 7, 99]; const avg = (xs: number[]): number => xs.reduce((a, b) => a + b, 0) / xs.length; const sum = (xs: number[]): number => xs.reduce((a, b) => a + b, 0); const bar = (p: typeof RSA_STRAIGHT): BarMetrics[] => SEEDS.map((s) => simulateBarShift(s, p)); const barWatched = (p: typeof RSA_STRAIGHT): BarMetrics[] => SEEDS.map((s) => simulateBarShift(s, p, { inspectorWatching: true })); const door = (p: typeof CAREFUL): NightMetrics[] => SEEDS.map((s) => simulateNight(s, p)); const sumBar = (runs: BarMetrics[]): string => `${runs[0]!.policy}: end=${runs.map((r) => `${r.endReason}@${r.endClockMin}`).join(',')} ` + `orders=${avg(runs.map((r) => r.orders)).toFixed(0)} serve=${avg(runs.map((r) => r.serves)).toFixed(0)} ` + `water=${avg(runs.map((r) => r.waters)).toFixed(0)} cut=${avg(runs.map((r) => r.cutoffs)).toFixed(0)} ` + `breach=${avg(runs.map((r) => r.breaches)).toFixed(1)} strikes=${avg(runs.map((r) => r.heatStrikes)).toFixed(1)} ` + `short=${avg(runs.map((r) => r.shortPours)).toFixed(0)} over=${avg(runs.map((r) => r.overPours)).toFixed(0)} ` + `tabs=${avg(runs.map((r) => r.tabsOpened)).toFixed(1)} vibeMin=${avg(runs.map((r) => r.vibeMin)).toFixed(0)} ` + `vibePin=${avg(runs.map((r) => r.vibePinnedShare)).toFixed(2)} aggroMax=${avg(runs.map((r) => r.aggroMax)).toFixed(0)} ` + `hype=${avg(runs.map((r) => r.hypePeak)).toFixed(1)} crowd=${avg(runs.map((r) => r.crowdAvg)).toFixed(0)}`; const sumDoor = (runs: NightMetrics[]): string => `${runs[0]!.policy}: end=${runs.map((r) => `${r.endReason}@${r.endClockMin}`).join(',')} ` + `admit=${avg(runs.map((r) => r.admitted)).toFixed(0)} parked=${avg(runs.map((r) => r.carpetParked)).toFixed(1)} ` + `storms=${avg(runs.map((r) => r.carpetStorms)).toFixed(1)} entrances=${avg(runs.map((r) => r.carpetEntrances)).toFixed(1)} ` + `hype=${avg(runs.map((r) => r.hypePeak)).toFixed(2)} hypePin=${avg(runs.map((r) => r.hypePinnedShare)).toFixed(2)} ` + `vibePin=${avg(runs.map((r) => r.vibePinnedShare)).toFixed(2)} aggroMax=${avg(runs.map((r) => r.aggroMax)).toFixed(0)}`; describe('the red carpet (bot nights, 5 seeds)', () => { const careful = door(CAREFUL); const showpony = door(SHOWPONY); const greedy = door(GREEDY); it('prints the current shape (baseline visibility)', () => { console.log('\n' + [careful, showpony, greedy].map(sumDoor).join('\n')); expect(showpony[0]!.carpetParked).toBeGreaterThan(0); }); it('theatre pays — working the velvet beats not working it', () => { expect(avg(showpony.map((r) => r.hypePeak))).toBeGreaterThan(avg(careful.map((r) => r.hypePeak)) + 0.3); }); it('but hype NEVER pins at the ceiling — the ratchet stays dead', () => { // THE regression guard. This is the band the carpet broke on arrival: hype // is a pressure, not a currency you accumulate until the night stops caring. for (const runs of [careful, showpony, greedy]) { expect(avg(runs.map((r) => r.hypePinnedShare)), `${runs[0]!.policy} pinned hype`).toBeLessThan(0.02); } }); it('vibe survives the carpet — a hyped room is not a dead meter', () => { // Skilled carpet play legitimately buys a better room than careful play, // so this band sits above the door's 0.5 — but the meter must still move. expect(avg(showpony.map((r) => r.vibePinnedShare))).toBeLessThan(0.65); expect(Math.min(...showpony.map((r) => r.vibeMin))).toBeLessThan(60); }); it('the fuse is real — hold them to the limit and they walk', () => { // Parking is only a gamble if the walk-off actually arrives. GREEDY holds // every archetype near its patience; most of them leave. expect(sum(greedy.map((r) => r.carpetStorms))).toBeGreaterThan(0); expect(avg(greedy.map((r) => r.carpetStorms))).toBeGreaterThan(avg(greedy.map((r) => r.carpetEntrances))); }); it('and greed is paid for in aggro and in throughput', () => { expect(avg(greedy.map((r) => r.aggroMax))).toBeGreaterThan(avg(careful.map((r) => r.aggroMax)) * 1.5); // The rope is busy being theatre: fewer people actually get through it. expect(avg(greedy.map((r) => r.admitted))).toBeLessThan(avg(showpony.map((r) => r.admitted))); }); it('reading the archetype is the skill — patience is not uniform', () => { // SHOWPONY parks only the tolerant and recalls inside their fuse: no storms. // GREEDY parks anyone and holds: many. Same verb, different reading. expect(sum(showpony.map((r) => r.carpetStorms))).toBe(0); }); }); describe('the bar economy (bot shifts, 5 seeds)', () => { const straight = bar(RSA_STRAIGHT); const publican = bar(PUBLICAN); const butter = bar(BUTTERFINGERS); it('prints the current shape (baseline visibility)', () => { console.log('\n' + [straight, publican, butter, barWatched(PUBLICAN)].map(sumBar).join('\n')); expect(straight[0]!.orders).toBeGreaterThan(0); }); it('the legal shift survives the night, every seed', () => { for (const r of straight) expect(r.endReason).toBe('clock'); }); it('and is never punished for it — playing RSA straight earns no heat', () => { // If the lawful bartender collected strikes, the rule would be a trap // rather than a tension. Cutting people off has to be SAFE, just costly. expect(sum(straight.map((r) => r.heatStrikes))).toBe(0); expect(sum(straight.map((r) => r.breaches))).toBe(0); }); it('serving the cooked is a real risk — the RSA line has teeth', () => { expect(sum(publican.map((r) => r.breaches))).toBeGreaterThan(0); expect(sum(publican.map((r) => r.heatStrikes))).toBeGreaterThan(0); expect(avg(publican.map((r) => r.heatStrikes))).toBeGreaterThan(avg(straight.map((r) => r.heatStrikes))); }); it('the pour is a skill that costs you — a bad hand is felt in the room', () => { // Same legal calls, worse hands: short pours land and the room resents it. expect(avg(butter.map((r) => r.shortPours))).toBeGreaterThan(avg(straight.map((r) => r.shortPours)) * 2); expect(avg(butter.map((r) => r.aggroMax))).toBeGreaterThan(avg(straight.map((r) => r.aggroMax)) * 1.4); }); it('the bar is not a vibe printer', () => { // A night of pouring must not pin the room the way the old door did. for (const runs of [straight, publican, butter]) { expect(avg(runs.map((r) => r.vibePinnedShare)), `${runs[0]!.policy} pinned vibe`).toBeLessThan(0.3); } }); it('the room actually fills — the shift has customers to get wrong', () => { // Guards the harness itself: a bar sim serving an empty room proves nothing. expect(avg(straight.map((r) => r.crowdAvg))).toBeGreaterThan(10); expect(avg(straight.map((r) => r.orders))).toBeGreaterThan(15); }); });