import { describe, expect, it } from 'vitest'; import { CORRECT_LINE, MAGGOT_GRACE_MIN, judgeEscalation, maggotOverdue, } from '../../src/scenes/floor/escalation'; import type { DrunkStage } from '../../src/data/types'; import { CUTOFF_REPLY_CLEAN, CUTOFF_REPLY_SCENE, CUTOFF_REPLY_SOFT, ESCALATION_LINES, type EscalationLine, } from '../../src/data/strings/floor'; const STAGES: readonly DrunkStage[] = ['sober', 'tipsy', 'loose', 'messy', 'maggot']; const LINES: readonly EscalationLine[] = ['water', 'done', 'escort']; describe('judgeEscalation matrix', () => { it('returns a coherent result for every (stage, line) pair', () => { for (const stage of STAGES) { for (const line of LINES) { const r = judgeEscalation(stage, line); expect(['clean', 'scene', 'staggerFree']).toContain(r.outcome); expect(r.line).toBe(ESCALATION_LINES[line]); expect(r.line.length).toBeGreaterThan(0); expect(r.reply.length).toBeGreaterThan(0); expect(Object.values(CUTOFF_REPLY_CLEAN) .concat(Object.values(CUTOFF_REPLY_SCENE), Object.values(CUTOFF_REPLY_SOFT))) .toContain(r.reply); expect(r.aggroDelta).toBeGreaterThanOrEqual(0); if (r.eject) expect(r.outcome).toBe('clean'); if (!r.resolved) expect(r.outcome).toBe('staggerFree'); } } }); it('pulls the reply from the bank matching its outcome', () => { for (const stage of STAGES) { for (const line of LINES) { const r = judgeEscalation(stage, line); const bank = r.outcome === 'clean' ? CUTOFF_REPLY_CLEAN : r.outcome === 'scene' ? CUTOFF_REPLY_SCENE : CUTOFF_REPLY_SOFT; expect(r.reply).toBe(bank[stage]); } } }); it('is clean whenever the line matches the correct rung', () => { for (const stage of STAGES) { const r = judgeEscalation(stage, CORRECT_LINE[stage]); expect(r.outcome).toBe('clean'); expect(r.vibeDelta).toBeGreaterThan(0); expect(r.aggroDelta).toBe(0); expect(r.resolved).toBe(true); } }); it('keeps the clean reward modest — never a jackpot for being right', () => { for (const stage of STAGES) { expect(judgeEscalation(stage, CORRECT_LINE[stage]).vibeDelta).toBeLessThanOrEqual(5); } }); }); describe('over-escalating', () => { it('makes a scene with positive aggro on a loose patron', () => { const r = judgeEscalation('loose', 'done'); expect(r.outcome).toBe('scene'); expect(r.aggroDelta).toBeGreaterThan(0); expect(r.vibeDelta).toBeLessThan(0); expect(r.eject).toBe(false); expect(r.resolved).toBe(true); }); it('scales aggro and vibe loss with the distance overreached', () => { const oneRung = judgeEscalation('loose', 'done'); const twoRungs = judgeEscalation('loose', 'escort'); expect(twoRungs.aggroDelta).toBe(oneRung.aggroDelta * 2); expect(twoRungs.vibeDelta).toBe(oneRung.vibeDelta * 2); }); it('costs more than restraint does, so aggression never pays', () => { const scene = judgeEscalation('sober', 'escort'); const soft = judgeEscalation('maggot', 'water'); expect(scene.vibeDelta).toBeLessThan(soft.vibeDelta); }); }); describe('under-escalating', () => { it('lets a maggot stagger free and leaves the infraction open', () => { const r = judgeEscalation('maggot', 'water'); expect(r.outcome).toBe('staggerFree'); expect(r.resolved).toBe(false); expect(r.eject).toBe(false); expect(r.aggroDelta).toBe(0); expect(r.vibeDelta).toBeLessThan(0); }); it('does not eject or resolve when you water a messy patron', () => { const r = judgeEscalation('messy', 'water'); expect(r.outcome).toBe('staggerFree'); expect(r.eject).toBe(false); expect(r.resolved).toBe(false); }); }); describe('ejection', () => { it('escorts a maggot out', () => { const r = judgeEscalation('maggot', 'escort'); expect(r.outcome).toBe('clean'); expect(r.eject).toBe(true); }); it('walks a messy patron when you call them done', () => { expect(judgeEscalation('messy', 'done').eject).toBe(true); }); it('never ejects someone who only needed water', () => { for (const stage of ['sober', 'tipsy', 'loose'] as const) { expect(judgeEscalation(stage, 'water').eject).toBe(false); } }); }); describe('maggotOverdue', () => { it('is false at the grace boundary and true one minute past', () => { expect(maggotOverdue(100, 100 + MAGGOT_GRACE_MIN)).toBe(false); expect(maggotOverdue(100, 100 + MAGGOT_GRACE_MIN + 1)).toBe(true); }); it('is false before the grace window elapses', () => { expect(maggotOverdue(100, 100)).toBe(false); expect(maggotOverdue(100, 110)).toBe(false); }); });