not-tonight/tests/floor/djShift.test.ts
2026-07-21 02:17:45 +10:00

95 lines
3.1 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
BUILD_FIZZLE_BARS,
DROP_CLEAN_MS,
DROP_LATE_MS,
DROP_PAYOUT,
REQUEST_HIT,
TIP_AMOUNTS,
TIP_OFFER_CHANCE,
judgeDrop,
msToNearestBar,
rollTip,
} from '../../src/scenes/floor/djShift';
import { SeededRNG } from '../../src/core/SeededRNG';
import { DJ_REQUESTS } from '../../src/data/strings/floor';
describe('judgeDrop', () => {
it('rewards the bar line, forgives near misses, punishes the clang', () => {
expect(judgeDrop(0)).toBe('clean');
expect(judgeDrop(DROP_CLEAN_MS)).toBe('clean');
expect(judgeDrop(-DROP_CLEAN_MS)).toBe('clean');
expect(judgeDrop(DROP_CLEAN_MS + 1)).toBe('late');
expect(judgeDrop(-DROP_LATE_MS)).toBe('late');
expect(judgeDrop(DROP_LATE_MS + 1)).toBe('clang');
});
it('early and late are the same offence — the room only hears wrong', () => {
for (const d of [50, 200, 500]) expect(judgeDrop(d)).toBe(judgeDrop(-d));
});
});
describe('msToNearestBar', () => {
const BEAT = 500; // 120bpm
it('is zero exactly on the bar', () => {
expect(msToNearestBar(0, 0, BEAT)).toBe(0);
});
it('measures behind the last bar early in the bar', () => {
expect(msToNearestBar(0, 120, BEAT)).toBe(120);
expect(msToNearestBar(1, 0, BEAT)).toBe(500);
});
it('measures ahead (negative) late in the bar', () => {
expect(msToNearestBar(3, 400, BEAT)).toBe(-(4 * BEAT - (3 * BEAT + 400)));
expect(msToNearestBar(3, 499, BEAT)).toBe(-1);
});
it('a mid-bar drop is maximally wrong from either side', () => {
expect(Math.abs(msToNearestBar(2, 0, BEAT))).toBe(2 * BEAT);
});
});
describe('the shift is priced, not graded', () => {
it('a clean drop pays, a clang costs — and neither is free', () => {
expect(DROP_PAYOUT.clean.vibe).toBeGreaterThan(0);
expect(DROP_PAYOUT.clang.vibe).toBeLessThan(0);
expect(DROP_PAYOUT.clang.aggro).toBeGreaterThan(0);
expect(DROP_PAYOUT.late.vibe).toBeLessThan(DROP_PAYOUT.clean.vibe);
});
it('a build cannot be held forever', () => {
expect(BUILD_FIZZLE_BARS).toBeLessThanOrEqual(8);
});
});
describe('booth requests', () => {
it('every scripted request has odds, and every odds entry has a script', () => {
const scriptIds = DJ_REQUESTS.map((r) => r.id);
expect(new Set(scriptIds).size).toBe(scriptIds.length);
for (const id of scriptIds) {
expect(REQUEST_HIT[id], `no odds for '${id}'`).toBeGreaterThan(0);
expect(REQUEST_HIT[id]).toBeLessThan(1); // nothing is a sure thing at the booth
}
for (const id of Object.keys(REQUEST_HIT)) {
expect(scriptIds).toContain(id);
}
});
});
describe('the tip economy', () => {
it('rollTip: sometimes cash, always a real amount, never help for the song', () => {
const rng = new SeededRNG(7).stream('tips');
let offers = 0;
for (let i = 0; i < 2000; i++) {
const tip = rollTip(rng);
if (tip === null) continue;
offers++;
expect(TIP_AMOUNTS).toContain(tip);
}
// Roughly TIP_OFFER_CHANCE of requests wave cash.
expect(offers / 2000).toBeGreaterThan(TIP_OFFER_CHANCE - 0.06);
expect(offers / 2000).toBeLessThan(TIP_OFFER_CHANCE + 0.06);
});
});