import { describe, expect, it } from 'vitest'; import { OVERFLOW_EXTRA_INTOX, POUR_CLEAN_FROM, POUR_OVER_FROM, SERVE_DRINK_STEP, canOpenTab, cardLineup, isRsaBreach, judgeCall, judgePour, pourAdjust, } from '../../src/scenes/floor/barShift'; import { SeededRNG } from '../../src/core/SeededRNG'; import { WATER_SOBER_STEP } from '../../src/scenes/floor/hazards'; import { BAR_ORDER_LINES } from '../../src/data/strings/floor'; import type { DrunkStage } from '../../src/data/types'; const STAGES: readonly DrunkStage[] = ['sober', 'tipsy', 'loose', 'messy', 'maggot']; describe('isRsaBreach', () => { it('draws the line at messy — loose is the decide band, same as the floor cut-off', () => { expect(isRsaBreach('sober')).toBe(false); expect(isRsaBreach('tipsy')).toBe(false); expect(isRsaBreach('loose')).toBe(false); expect(isRsaBreach('messy')).toBe(true); expect(isRsaBreach('maggot')).toBe(true); }); }); describe('judgeCall / serve', () => { it('a legal pour pays vibe and adds a real drink', () => { const out = judgeCall('serve', 'tipsy'); expect(out).toMatchObject({ vibe: 1, breach: false, sendsHome: false }); expect(out.intoxDelta).toBe(SERVE_DRINK_STEP); }); it('a breach pour STILL pays its vibe up front — the price arrives later', () => { for (const stage of ['messy', 'maggot'] as const) { const out = judgeCall('serve', stage); expect(out.vibe).toBe(1); expect(out.breach).toBe(true); expect(out.intoxDelta).toBe(SERVE_DRINK_STEP); } }); it('never sends anyone home — a served patron stays a problem', () => { for (const stage of STAGES) expect(judgeCall('serve', stage).sendsHome).toBe(false); }); }); describe('judgeCall / water', () => { it('the good catch: loose and up take the cup and improve', () => { for (const stage of ['loose', 'messy', 'maggot'] as const) { const out = judgeCall('water', stage); expect(out.vibe).toBe(1); expect(out.aggro).toBeUndefined(); expect(out.intoxDelta).toBe(-WATER_SOBER_STEP); } }); it('watering the sober is a snub — they ordered a beer', () => { for (const stage of ['sober', 'tipsy'] as const) { const out = judgeCall('water', stage); expect(out.aggro).toBe(1); expect(out.vibe).toBeUndefined(); } }); it('is never a breach — water cannot be an offence', () => { for (const stage of STAGES) expect(judgeCall('water', stage).breach).toBe(false); }); }); describe('judgeCall / cutoff', () => { it('always sends them home, whoever they were', () => { for (const stage of STAGES) expect(judgeCall('cutoff', stage).sendsHome).toBe(true); }); it('right on a breach-stage patron: a scene, but the room is relieved', () => { const out = judgeCall('cutoff', 'maggot'); expect(out).toMatchObject({ vibe: 1, aggro: 1 }); }); it('early on someone upright: they tell the whole queue about you', () => { const out = judgeCall('cutoff', 'tipsy'); expect(out).toMatchObject({ vibe: -1, aggro: 2 }); }); it('pours nothing either way', () => { for (const stage of STAGES) expect(judgeCall('cutoff', stage).intoxDelta).toBe(0); }); }); describe('judgePour', () => { it('short below the line, clean in the band, overflow past it', () => { expect(judgePour(0)).toBe('short'); expect(judgePour(POUR_CLEAN_FROM - 0.01)).toBe('short'); expect(judgePour(POUR_CLEAN_FROM)).toBe('clean'); expect(judgePour(POUR_OVER_FROM - 0.01)).toBe('clean'); expect(judgePour(POUR_OVER_FROM)).toBe('overflow'); expect(judgePour(1)).toBe('overflow'); }); }); describe('pourAdjust', () => { const base = judgeCall('serve', 'tipsy'); it('a clean pour changes nothing', () => { expect(pourAdjust(base, 'clean')).toEqual(base); }); it('a short pour: no thanks, a look, half a drink', () => { const out = pourAdjust(base, 'short'); expect(out.vibe).toBeUndefined(); expect(out.aggro).toBe(1); expect(out.intoxDelta).toBe(SERVE_DRINK_STEP / 2); }); it('overflow: the room approves, the patron wears more of it', () => { const out = pourAdjust(base, 'overflow'); expect(out.vibe).toBe(2); expect(out.intoxDelta).toBeCloseTo(SERVE_DRINK_STEP + OVERFLOW_EXTRA_INTOX); }); it('the breach flag rides the SERVE, not the pour', () => { const breach = judgeCall('serve', 'maggot'); expect(pourAdjust(breach, 'short').breach).toBe(true); expect(pourAdjust(breach, 'overflow').breach).toBe(true); }); }); describe('tabs', () => { it('sober people pay as they go', () => { expect(canOpenTab('sober')).toBe(false); for (const s of ['tipsy', 'loose', 'messy', 'maggot'] as const) expect(canOpenTab(s)).toBe(true); }); it('cardLineup always contains the right card, plus up to two strangers', () => { const rng = new SeededRNG(11).stream('t'); for (let i = 0; i < 20; i++) { const lineup = cardLineup('Jai Doyle', ['Mia Chen', 'Lou Reed', 'Jai Doyle', 'Sam Fox'], rng); expect(lineup).toContain('Jai Doyle'); expect(lineup.length).toBe(3); expect(new Set(lineup).size).toBe(3); } }); it('the right card is not always slot one', () => { const rng = new SeededRNG(11).stream('t'); const slots = new Set(); for (let i = 0; i < 30; i++) { slots.add(cardLineup('A', ['B', 'C', 'D', 'E'], rng).indexOf('A')); } expect(slots.size).toBeGreaterThan(1); }); it('survives an empty jar — a lineup of one is still a lineup', () => { const rng = new SeededRNG(3).stream('t'); expect(cardLineup('A', [], rng)).toEqual(['A']); }); }); describe('BAR_ORDER_LINES', () => { it('every stage has lines — the order IS the sobriety test', () => { for (const stage of STAGES) expect(BAR_ORDER_LINES[stage].length).toBeGreaterThan(0); }); });