164 lines
6.4 KiB
TypeScript
164 lines
6.4 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { SeededRNG } from '../../src/core/SeededRNG';
|
|
import { PATDOWN_CLEAR, PATDOWN_MISSED } from '../../src/data/strings/floor';
|
|
import {
|
|
foundLine,
|
|
layoutPockets,
|
|
scorePatDown,
|
|
POCKET_COUNT,
|
|
type Pocket,
|
|
} from '../../src/scenes/floor/patDown';
|
|
import type { Patron } from '../../src/data/types';
|
|
|
|
const patronWith = (contraband: string[]): Patron => ({
|
|
id: 'p1',
|
|
dollSeed: 42,
|
|
archetype: 'punter',
|
|
age: 24,
|
|
idCard: { name: 'Shazza Nguyen', dob: '2002-03-04', expiry: '2029-03-04', photoSeed: 42 },
|
|
outfit: [],
|
|
intoxication: 0.3,
|
|
tolerance: 0.5,
|
|
flags: contraband.length > 0 ? { contraband } : {},
|
|
});
|
|
|
|
const stream = (seed: number) => new SeededRNG(seed).stream('patdown');
|
|
const binAll = (pockets: Pocket[]): Pocket[] => pockets.map((p) => ({ ...p, binned: p.itemId !== undefined }));
|
|
|
|
describe('layoutPockets', () => {
|
|
it('returns POCKET_COUNT pockets with unique ids at distinct positions', () => {
|
|
const pockets = layoutPockets(patronWith(['flask']), stream(1));
|
|
expect(pockets).toHaveLength(POCKET_COUNT);
|
|
expect(new Set(pockets.map((p) => p.id)).size).toBe(POCKET_COUNT);
|
|
expect(new Set(pockets.map((p) => `${p.x},${p.y}`)).size).toBe(POCKET_COUNT);
|
|
});
|
|
|
|
it('keeps pockets inside the 64x96 outline and clear of each other', () => {
|
|
const pockets = layoutPockets(patronWith([]), stream(2));
|
|
for (const p of pockets) {
|
|
expect(p.x).toBeGreaterThanOrEqual(0);
|
|
expect(p.y).toBeGreaterThanOrEqual(0);
|
|
expect(p.x).toBeLessThanOrEqual(64);
|
|
expect(p.y).toBeLessThanOrEqual(96);
|
|
}
|
|
for (let i = 0; i < pockets.length; i++) {
|
|
for (let j = i + 1; j < pockets.length; j++) {
|
|
const a = pockets[i]!;
|
|
const b = pockets[j]!;
|
|
const clear = Math.abs(a.x - b.x) >= 12 || Math.abs(a.y - b.y) >= 12;
|
|
expect(clear, `${a.label} overlaps ${b.label}`).toBe(true);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('places every contraband id in a distinct pocket', () => {
|
|
const items = ['flask', 'kebab', 'budgie'];
|
|
const pockets = layoutPockets(patronWith(items), stream(3));
|
|
const filled = pockets.filter((p) => p.itemId !== undefined);
|
|
expect(filled).toHaveLength(items.length);
|
|
expect(new Set(filled.map((p) => p.id)).size).toBe(items.length);
|
|
expect(filled.map((p) => p.itemId).sort()).toEqual([...items].sort());
|
|
});
|
|
|
|
it('leaves an empty-handed patron with no items and nothing revealed or binned', () => {
|
|
const pockets = layoutPockets(patronWith([]), stream(4));
|
|
expect(pockets.every((p) => p.itemId === undefined)).toBe(true);
|
|
expect(pockets.every((p) => !p.revealed && !p.binned)).toBe(true);
|
|
});
|
|
|
|
it('places only what fits when there is more contraband than pockets', () => {
|
|
const items = ['flask', 'kebab', 'budgie', 'vapes', 'marker', 'snags', 'tinnies', 'goonSack'];
|
|
const pockets = layoutPockets(patronWith(items), stream(5));
|
|
expect(pockets.filter((p) => p.itemId !== undefined)).toHaveLength(POCKET_COUNT);
|
|
});
|
|
|
|
it('is deterministic for the same seed and divergent across seeds', () => {
|
|
const items = ['flask', 'kebab', 'budgie'];
|
|
const a = layoutPockets(patronWith(items), stream(7)).map((p) => p.itemId);
|
|
const b = layoutPockets(patronWith(items), stream(7)).map((p) => p.itemId);
|
|
expect(a).toEqual(b);
|
|
|
|
const seeds = [11, 12, 13, 14, 15, 16].map((s) =>
|
|
layoutPockets(patronWith(items), stream(s))
|
|
.map((p) => p.itemId ?? '-')
|
|
.join(),
|
|
);
|
|
expect(new Set(seeds).size).toBeGreaterThan(1);
|
|
});
|
|
});
|
|
|
|
describe('scorePatDown', () => {
|
|
it('rewards a full sweep: every item found, positive vibe, no aggro', () => {
|
|
const patron = patronWith(['flask', 'kebab']);
|
|
const result = scorePatDown(patron, binAll(layoutPockets(patron, stream(21))));
|
|
expect(result.foundIds.sort()).toEqual(['flask', 'kebab']);
|
|
expect(result.missedIds).toEqual([]);
|
|
expect(result.vibeDelta).toBe(4);
|
|
expect(result.aggroDelta).toBe(0);
|
|
expect(result.summary).not.toContain(PATDOWN_MISSED);
|
|
});
|
|
|
|
it('leaks vibe for items left in the pockets when time runs out', () => {
|
|
const patron = patronWith(['flask', 'kebab']);
|
|
const result = scorePatDown(patron, layoutPockets(patron, stream(22)));
|
|
expect(result.foundIds).toEqual([]);
|
|
expect(result.missedIds.sort()).toEqual(['flask', 'kebab']);
|
|
expect(result.vibeDelta).toBe(-3);
|
|
expect(result.summary).toBe(PATDOWN_MISSED);
|
|
expect(result.eject).toBe(false);
|
|
});
|
|
|
|
it('mixes found and missed, and still flags the miss', () => {
|
|
const patron = patronWith(['flask', 'kebab']);
|
|
const pockets = layoutPockets(patron, stream(23)).map((p) => ({
|
|
...p,
|
|
binned: p.itemId === 'flask',
|
|
}));
|
|
const result = scorePatDown(patron, pockets);
|
|
expect(result.foundIds).toEqual(['flask']);
|
|
expect(result.missedIds).toEqual(['kebab']);
|
|
expect(result.vibeDelta).toBe(0.5);
|
|
expect(result.summary).toContain(PATDOWN_MISSED);
|
|
});
|
|
|
|
it('ejects on a binned severity-3 item but not a severity-1 one', () => {
|
|
const baggie = patronWith(['baggie']);
|
|
expect(scorePatDown(baggie, binAll(layoutPockets(baggie, stream(31)))).eject).toBe(true);
|
|
|
|
const kebab = patronWith(['kebab']);
|
|
const kebabResult = scorePatDown(kebab, binAll(layoutPockets(kebab, stream(32))));
|
|
expect(kebabResult.eject).toBe(false);
|
|
expect(kebabResult.foundIds).toEqual(['kebab']);
|
|
});
|
|
|
|
it('does not eject for a severity-3 item that was never binned', () => {
|
|
const patron = patronWith(['baggie']);
|
|
const result = scorePatDown(patron, layoutPockets(patron, stream(33)));
|
|
expect(result.eject).toBe(false);
|
|
expect(result.missedIds).toEqual(['baggie']);
|
|
});
|
|
|
|
it('charges aggro and reads PATDOWN_CLEAR when they had nothing on them', () => {
|
|
const patron = patronWith([]);
|
|
const result = scorePatDown(patron, layoutPockets(patron, stream(41)));
|
|
expect(result.foundIds).toEqual([]);
|
|
expect(result.missedIds).toEqual([]);
|
|
expect(result.aggroDelta).toBe(1.5);
|
|
expect(result.vibeDelta).toBe(0);
|
|
expect(result.eject).toBe(false);
|
|
expect(result.summary).toBe(PATDOWN_CLEAR);
|
|
});
|
|
});
|
|
|
|
describe('foundLine', () => {
|
|
it('uses the scripted line for a known item', () => {
|
|
expect(foundLine('budgie')).toContain('budgie');
|
|
});
|
|
|
|
it('falls back gracefully for an unknown id', () => {
|
|
const line = foundLine('a-second-budgie');
|
|
expect(typeof line).toBe('string');
|
|
expect(line.length).toBeGreaterThan(0);
|
|
});
|
|
});
|