- scenes/floor/sweep.ts: the closing sweep extracted behind an accessor host. Scene 2100 -> 1981 lines, behaviour verified identical. - data/roleBeats.ts: 18 beats across 6 roles x 3 nights, landing in the venue group chat. The glassie is spoken about; the HoS is spoken to. - dollPlan(p, pose, frame): 4-frame idle on the queue pose. NOT pre-rendered sheets — those would hide the outfit tells the dress code convicts on, which design 4.1 forbids; the pre-render path must go UNDER the outfit rects. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
85 lines
3.6 KiB
TypeScript
85 lines
3.6 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { IDLE_FRAMES, dollPlan } from '../src/patrons/dollPlan';
|
|
import { generatePatron, _resetPatronSerial } from '../src/patrons/generator';
|
|
import { SeededRNG } from '../src/core/SeededRNG';
|
|
import { violations, activeRules } from '../src/rules/dressCode';
|
|
|
|
const NIGHT = new Date('2026-07-18T00:00:00');
|
|
|
|
describe('idle frames (design §4.1 must survive animation)', () => {
|
|
it('every frame draws the SAME set of outfit pixels, only displaced', () => {
|
|
// The dress code convicts on things the doll draws. If a frame dropped or
|
|
// recoloured a rect, the player could be judged on a tell that blinked.
|
|
_resetPatronSerial();
|
|
const rng = new SeededRNG(31);
|
|
for (let i = 0; i < 40; i++) {
|
|
const p = generatePatron({ rng, nightDate: NIGHT }, 120);
|
|
const base = dollPlan(p, 'queue', 0);
|
|
for (let f = 1; f < IDLE_FRAMES; f++) {
|
|
const frame = dollPlan(p, 'queue', f);
|
|
expect(frame.rects.length, `p${i} f${f} rect count`).toBe(base.rects.length);
|
|
// Same colours, same sizes, in the same order — only x/y may move.
|
|
for (let r = 0; r < base.rects.length; r++) {
|
|
expect(frame.rects[r]!.colour).toBe(base.rects[r]!.colour);
|
|
expect(frame.rects[r]!.w).toBe(base.rects[r]!.w);
|
|
expect(frame.rects[r]!.h).toBe(base.rects[r]!.h);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
it('a frame never moves anything more than a pixel or two', () => {
|
|
_resetPatronSerial();
|
|
const rng = new SeededRNG(7);
|
|
for (let i = 0; i < 30; i++) {
|
|
const p = generatePatron({ rng, nightDate: NIGHT }, 200);
|
|
const base = dollPlan(p, 'queue', 0);
|
|
for (let f = 1; f < IDLE_FRAMES; f++) {
|
|
const frame = dollPlan(p, 'queue', f);
|
|
for (let r = 0; r < base.rects.length; r++) {
|
|
expect(Math.abs(frame.rects[r]!.x - base.rects[r]!.x)).toBeLessThanOrEqual(2);
|
|
expect(Math.abs(frame.rects[r]!.y - base.rects[r]!.y)).toBeLessThanOrEqual(1);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
it('the feet stay on the floor — nobody hovers', () => {
|
|
_resetPatronSerial();
|
|
const p = generatePatron({ rng: new SeededRNG(5), nightDate: NIGHT }, 60);
|
|
const base = dollPlan(p, 'queue', 0);
|
|
const feet = (plan: typeof base): number => Math.max(...plan.rects.map((r) => r.y + r.h));
|
|
for (let f = 1; f < IDLE_FRAMES; f++) {
|
|
expect(feet(dollPlan(p, 'queue', f))).toBe(feet(base));
|
|
}
|
|
});
|
|
|
|
it('what the rules judge is unchanged by which frame is showing', () => {
|
|
_resetPatronSerial();
|
|
const rng = new SeededRNG(99);
|
|
for (let i = 0; i < 40; i++) {
|
|
const p = generatePatron({ rng, nightDate: NIGHT }, 240);
|
|
// violations read outfit DATA, which frames never touch — this pins that
|
|
// the animation stayed on the render side of the contract.
|
|
const before = violations(p, 240).map((r) => r.id);
|
|
dollPlan(p, 'queue', 2);
|
|
expect(violations(p, 240).map((r) => r.id)).toEqual(before);
|
|
}
|
|
expect(activeRules(240).length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('frame 0 is exactly what every existing caller already got', () => {
|
|
_resetPatronSerial();
|
|
const p = generatePatron({ rng: new SeededRNG(12), nightDate: NIGHT }, 100);
|
|
expect(dollPlan(p, 'queue', 0)).toEqual(dollPlan(p, 'queue'));
|
|
});
|
|
|
|
it('the other poses ignore frames — the ID photo does not breathe', () => {
|
|
_resetPatronSerial();
|
|
const p = generatePatron({ rng: new SeededRNG(3), nightDate: NIGHT }, 100);
|
|
for (const pose of ['idPhoto', 'floorTopDown'] as const) {
|
|
expect(dollPlan(p, pose, 2)).toEqual(dollPlan(p, pose, 0));
|
|
}
|
|
});
|
|
});
|