130 lines
4.7 KiB
TypeScript
130 lines
4.7 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
// The functions under test are pure, but they live in modules that subclass
|
|
// Phaser.Scene, and Phaser touches `window` at import time under the node
|
|
// environment. Stubbing the module is the only way to reach them from a test.
|
|
vi.mock('phaser', () => ({ default: { Scene: class {} } }));
|
|
|
|
import { freshNightState } from '../src/core/meters';
|
|
import { DAZZA_SIGNOFF } from '../src/data/strings/door';
|
|
import { MAX_HEAT_STRIKES, shouldRecordStrike, type NightLog } from '../src/scenes/door/NightScene';
|
|
import { nightCash, signoffFor } from '../src/scenes/door/NightSummaryScene';
|
|
import type { NightState } from '../src/data/types';
|
|
|
|
const log = (over: Partial<NightLog> = {}): NightLog => ({
|
|
vibeHistory: [50],
|
|
verdicts: [],
|
|
admitted: 0,
|
|
denied: 0,
|
|
tested: 0,
|
|
pattedDown: 0,
|
|
hypePeak: 1,
|
|
heatStrikes: [],
|
|
endReason: 'clock',
|
|
seed: 1,
|
|
role: 'DOOR',
|
|
wage: 180,
|
|
...over,
|
|
});
|
|
|
|
const stateWith = (...reasons: string[]): NightState => {
|
|
const s = freshNightState('voltage', 0, 80);
|
|
for (const reason of reasons) s.heatStrikes.push({ reason });
|
|
return s;
|
|
};
|
|
|
|
describe('nightCash', () => {
|
|
it('pays a flat wage for an uneventful night', () => {
|
|
expect(nightCash(log(), stateWith())).toBe(180);
|
|
});
|
|
|
|
it('pays $2 per admit on top of the wage', () => {
|
|
const clean = stateWith();
|
|
expect(nightCash(log({ admitted: 10 }), clean)).toBe(nightCash(log(), clean) + 20);
|
|
expect(nightCash(log({ admitted: 40 }), clean)).toBeGreaterThan(
|
|
nightCash(log({ admitted: 10 }), clean),
|
|
);
|
|
});
|
|
|
|
it('pays a hype bonus that scales with peak hype, not with final hype', () => {
|
|
const clean = stateWith();
|
|
const base = nightCash(log(), clean);
|
|
expect(nightCash(log({ hypePeak: 2 }), clean)).toBe(base + 90);
|
|
expect(nightCash(log({ hypePeak: 3 }), clean)).toBe(base + 180);
|
|
});
|
|
|
|
it('deducts $60 for every heat strike', () => {
|
|
const l = log({ admitted: 20, hypePeak: 2 });
|
|
const none = nightCash(l, stateWith());
|
|
expect(nightCash(l, stateWith('over capacity'))).toBe(none - 60);
|
|
expect(nightCash(l, stateWith('over capacity', 'minor admitted'))).toBe(none - 120);
|
|
});
|
|
|
|
it('floors at zero — a catastrophic night pays nothing, it does not invoice you', () => {
|
|
const strikes = Array.from({ length: 20 }, (_, i) => `offence ${i}`);
|
|
expect(nightCash(log(), stateWith(...strikes))).toBe(0);
|
|
});
|
|
|
|
it('returns a whole number even when hype is fractional', () => {
|
|
for (const hypePeak of [1, 1.3, 1.75, 2.4, 3]) {
|
|
const cash = nightCash(log({ hypePeak, admitted: 7 }), stateWith());
|
|
expect(Number.isInteger(cash)).toBe(true);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('signoffFor', () => {
|
|
it('gives the harshest line at two strikes no matter how good the room felt', () => {
|
|
expect(signoffFor(100, 2)).toBe(DAZZA_SIGNOFF[0]);
|
|
expect(signoffFor(100, 3)).toBe(DAZZA_SIGNOFF[0]);
|
|
// one strike is survivable — a great room still earns praise
|
|
expect(signoffFor(100, 1)).not.toBe(DAZZA_SIGNOFF[0]);
|
|
});
|
|
|
|
it('never moves to an earlier line as vibe rises (no strikes)', () => {
|
|
for (const strikes of [0, 1]) {
|
|
let lowest = 0;
|
|
for (let vibe = 0; vibe <= 100; vibe++) {
|
|
const idx = DAZZA_SIGNOFF.indexOf(signoffFor(vibe, strikes));
|
|
expect(idx).toBeGreaterThanOrEqual(lowest);
|
|
lowest = idx;
|
|
}
|
|
}
|
|
});
|
|
|
|
it('actually varies with vibe rather than always saying the same thing', () => {
|
|
expect(signoffFor(0, 0)).not.toBe(signoffFor(100, 0));
|
|
});
|
|
|
|
it('returns a real signoff line across the whole 0..100 band, endpoints included', () => {
|
|
for (let vibe = 0; vibe <= 100; vibe++) {
|
|
const line = signoffFor(vibe, 0);
|
|
expect(line.length).toBeGreaterThan(0);
|
|
expect(DAZZA_SIGNOFF).toContain(line);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('shouldRecordStrike', () => {
|
|
it('matches CONTRACTS.md §3: max 3 strikes', () => {
|
|
expect(MAX_HEAT_STRIKES).toBe(3);
|
|
});
|
|
|
|
it('records a strike the night has not seen before', () => {
|
|
expect(shouldRecordStrike(stateWith(), { reason: 'minor admitted' })).toBe(true);
|
|
expect(shouldRecordStrike(stateWith('over capacity'), { reason: 'minor admitted' })).toBe(true);
|
|
});
|
|
|
|
it('stops recording once the licence is already gone', () => {
|
|
const full = stateWith('a', 'b', 'c');
|
|
expect(full.heatStrikes).toHaveLength(MAX_HEAT_STRIKES);
|
|
expect(shouldRecordStrike(full, { reason: 'd' })).toBe(false);
|
|
});
|
|
|
|
it('treats a persistent breach as one offence, not one per patron', () => {
|
|
const s = stateWith('over capacity');
|
|
expect(shouldRecordStrike(s, { reason: 'over capacity' })).toBe(false);
|
|
expect(shouldRecordStrike(s, { reason: 'over capacity', deferred: true })).toBe(false);
|
|
});
|
|
});
|