import { describe, expect, it } from 'vitest'; import { mulberry32, type Rng } from './seed'; import { pickSpecial, SPECIALS, SPECIAL_SPAWN_CHANCE } from './specials'; describe('pickSpecial (v0.37.0, ISS-073)', () => { it('returns null when the chance draw fails and consumes exactly one draw', () => { let draws = 0; const rng: Rng = () => { draws += 1; return 0.9; }; // ≥ SPECIAL_SPAWN_CHANCE → no special expect(pickSpecial(rng)).toBeNull(); expect(draws).toBe(1); }); it('picks among the registry keys when the chance draw passes', () => { const seen = new Set(); for (let i = 0; i < 2_000; i++) { let call = 0; // First call is the chance draw (< chance → roll); later calls sweep [0, 1) as index draws. const rng: Rng = () => (call++ === 0 ? 0.1 : i / 2_000); const s = pickSpecial(rng); if (s !== null) seen.add(s); } expect([...seen].sort()).toEqual(Object.keys(SPECIALS).sort()); // every key reachable }); it('never returns a status whose entry was removed; an empty registry consumes no draws', () => { const withoutPriority = Object.fromEntries(Object.entries(SPECIALS).filter(([k]) => k !== 'priority')); for (let i = 0; i < 500; i++) { expect(pickSpecial(mulberry32(i), withoutPriority)).not.toBe('priority'); // removing an entry disables it } let draws = 0; const rng: Rng = () => { draws += 1; return 0.0; }; // would pass the chance draw if consulted expect(pickSpecial(rng, {})).toBeNull(); expect(draws).toBe(0); }); it('rolls a special for roughly SPECIAL_SPAWN_CHANCE of packages (fixed RNG)', () => { const rng = mulberry32(42); let hits = 0; for (let i = 0; i < 10_000; i++) if (pickSpecial(rng) !== null) hits++; expect(hits / 10_000).toBeGreaterThan(SPECIAL_SPAWN_CHANCE - 0.05); expect(hits / 10_000).toBeLessThan(SPECIAL_SPAWN_CHANCE + 0.05); }); });