From d21c621485e63325258fd7b98a3267ee5c4fbf8e Mon Sep 17 00:00:00 2001 From: jedarden Date: Mon, 25 May 2026 23:16:42 -0400 Subject: [PATCH] =?UTF-8?q?test(web):=20add=20Director=20Mode=20unit=20tes?= =?UTF-8?q?ts=20per=20=C2=A716.10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive test coverage for the Director Mode (Adaptive Auto-Speed Playback) implementation, verifying the action density formula and speed mapping match the plan specification exactly. Tests cover: - Action density calculation (deaths×3.0 + captures×5.0 + energy×0.5 + spawns×1.0 + delta_win_prob×10.0) - Speed mapping (0→16x, 0.1-1.0→8x, 1.0-3.0→4x, 3.0-5.0→2x, 5.0+→1x) - Speed schedule computation with target duration scaling - Win probability delta calculation All 16 tests pass, confirming the Director Mode implementation in director.ts correctly implements §16.10 of the plan. Closes: bf-1p5y Co-Authored-By: Claude Opus 4.7 --- web/src/components/director.test.ts | 254 ++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 web/src/components/director.test.ts diff --git a/web/src/components/director.test.ts b/web/src/components/director.test.ts new file mode 100644 index 0000000..59a5bdf --- /dev/null +++ b/web/src/components/director.test.ts @@ -0,0 +1,254 @@ +/** + * Unit tests for Director Mode (§16.10) + * Verifies action density calculation and speed mapping match the plan. + */ + +import { describe, it, expect } from 'vitest'; +import { + computeActionDensity, + densityToSpeed, + computeSpeedSchedule, + computeAllDensities, + type ActionDensity, +} from './director'; +import type { ReplayTurn, Replay } from '../types'; + +describe('Director Mode (§16.10)', () => { + describe('computeActionDensity', () => { + it('should calculate density 0 for empty turn', () => { + const turn: ReplayTurn = { + bots: [], + energy: [], + cores: [], + scores: [], + energy_held: [], + }; + const result = computeActionDensity(turn, null); + expect(result.density).toBe(0); + expect(result.deaths).toBe(0); + expect(result.captures).toBe(0); + expect(result.energyCollected).toBe(0); + expect(result.spawns).toBe(0); + }); + + it('should weight deaths × 3.0 per plan formula', () => { + const turn: ReplayTurn = { + bots: [], + energy: [], + cores: [], + scores: [], + energy_held: [], + events: [ + { type: 'combat_death', details: {} }, + { type: 'combat_death', details: {} }, + ], + }; + const result = computeActionDensity(turn, null); + expect(result.deaths).toBe(2); + expect(result.density).toBe(2 * 3.0); + }); + + it('should weight captures × 5.0 per plan formula', () => { + const turn: ReplayTurn = { + bots: [], + energy: [], + cores: [], + scores: [], + energy_held: [], + events: [ + { type: 'core_captured', details: {} }, + ], + }; + const result = computeActionDensity(turn, null); + expect(result.captures).toBe(1); + expect(result.density).toBe(1 * 5.0); + }); + + it('should weight energy_collected × 0.5 per plan formula', () => { + const turn: ReplayTurn = { + bots: [], + energy: [], + cores: [], + scores: [], + energy_held: [], + events: [ + { type: 'energy_collected', details: {} }, + { type: 'energy_collected', details: {} }, + ], + }; + const result = computeActionDensity(turn, null); + expect(result.energyCollected).toBe(2); + expect(result.density).toBe(2 * 0.5); + }); + + it('should weight spawns × 1.0 per plan formula', () => { + const turn: ReplayTurn = { + bots: [], + energy: [], + cores: [], + scores: [], + energy_held: [], + events: [ + { type: 'bot_spawned', details: {} }, + ], + }; + const result = computeActionDensity(turn, null); + expect(result.spawns).toBe(1); + expect(result.density).toBe(1 * 1.0); + }); + + it('should weight delta_win_prob × 10.0 per plan formula', () => { + const turn: ReplayTurn = { + bots: [], + energy: [], + cores: [], + scores: [], + energy_held: [], + }; + const winProb = [ + [0.5, 0.5], + [0.4, 0.6], // delta = 0.2 total + ]; + const result = computeActionDensity(turn, null, winProb, 1); + expect(result.deltaWinProb).toBeCloseTo(0.2, 10); + expect(result.density).toBeCloseTo(0.2 * 10.0, 10); + }); + + it('should sum all components per plan formula', () => { + const turn: ReplayTurn = { + bots: [], + energy: [], + cores: [], + scores: [], + energy_held: [], + events: [ + { type: 'combat_death', details: {} }, // +3.0 + { type: 'core_captured', details: {} }, // +5.0 + { type: 'energy_collected', details: {} }, // +0.5 + { type: 'bot_spawned', details: {} }, // +1.0 + ], + }; + const winProb = [ + [0.5, 0.5], + [0.3, 0.7], // delta = 0.4 total → +4.0 + ]; + const result = computeActionDensity(turn, null, winProb, 1); + expect(result.density).toBe(3.0 + 5.0 + 0.5 + 1.0 + 4.0); + }); + }); + + describe('densityToSpeed', () => { + it('should map 0 → 16x (nothing happening)', () => { + expect(densityToSpeed(0)).toBe(16); + }); + + it('should map 0.1–1.0 → 8x (minor activity)', () => { + expect(densityToSpeed(0.1)).toBe(8); + expect(densityToSpeed(0.5)).toBe(8); + expect(densityToSpeed(1.0)).toBe(4); // boundary + }); + + it('should map 1.0–3.0 → 4x (moderate)', () => { + expect(densityToSpeed(1.0)).toBe(4); + expect(densityToSpeed(2.0)).toBe(4); + expect(densityToSpeed(3.0)).toBe(2); // boundary + }); + + it('should map 3.0–5.0 → 2x (significant)', () => { + expect(densityToSpeed(3.0)).toBe(2); + expect(densityToSpeed(4.0)).toBe(2); + expect(densityToSpeed(5.0)).toBe(1); // boundary + }); + + it('should map 5.0+ → 1x (critical)', () => { + expect(densityToSpeed(5.0)).toBe(1); + expect(densityToSpeed(10.0)).toBe(1); + expect(densityToSpeed(100.0)).toBe(1); + }); + }); + + describe('computeSpeedSchedule', () => { + it('should generate one speed per turn', () => { + const densities: ActionDensity[] = [ + { density: 0, deaths: 0, captures: 0, energyCollected: 0, spawns: 0, deltaWinProb: 0 }, + { density: 6, deaths: 2, captures: 0, energyCollected: 0, spawns: 0, deltaWinProb: 0 }, + { density: 0.5, deaths: 0, captures: 0, energyCollected: 1, spawns: 0, deltaWinProb: 0 }, + ]; + const schedule = computeSpeedSchedule(densities, 60); + expect(schedule).toHaveLength(3); + // Raw speeds are scaled to match target duration + // Raw: 16x, 1x, 8x → times: 31.25ms, 500ms, 62.5ms → total ~594ms + // Target 60s means scaling factor ~100x, so speeds get clamped + expect(schedule[0]).toBeGreaterThanOrEqual(1); + expect(schedule[0]).toBeLessThanOrEqual(16); + expect(schedule[1]).toBeGreaterThanOrEqual(1); + expect(schedule[1]).toBeLessThanOrEqual(16); + expect(schedule[2]).toBeGreaterThanOrEqual(1); + expect(schedule[2]).toBeLessThanOrEqual(16); + }); + + it('should scale speeds to match target duration', () => { + const densities: ActionDensity[] = Array(100).fill({ + density: 0, + deaths: 0, + captures: 0, + energyCollected: 0, + spawns: 0, + deltaWinProb: 0, + }); + const schedule = computeSpeedSchedule(densities, 60); + // All turns have density 0, so all should be fast (16x) + // With 100 turns at 16x, total time should be ~100 * (500/16) ≈ 3.1s + // But target is 60s, so speeds will be scaled down significantly + expect(schedule.every(s => s >= 1 && s <= 16)).toBe(true); + }); + }); + + describe('computeAllDensities', () => { + it('should compute density for each turn', () => { + const replay: Replay = { + match_id: 'test', + players: [], + map: { rows: 10, cols: 10, walls: [], cores: [], energy_nodes: [] }, + config: {} as any, + turns: [ + { bots: [], energy: [], cores: [], scores: [], energy_held: [], events: [] }, + { + bots: [], + energy: [], + cores: [], + scores: [], + energy_held: [], + events: [{ type: 'combat_death', details: {} }], + }, + ], + result: { turns: 2, winner: 0, reason: 'test', scores: [0, 0] }, + }; + const densities = computeAllDensities(replay); + expect(densities).toHaveLength(2); + expect(densities[0].density).toBe(0); + expect(densities[1].density).toBe(3.0); // one death + }); + + it('should use win_prob for delta calculation', () => { + const replay: Replay = { + match_id: 'test', + players: [], + map: { rows: 10, cols: 10, walls: [], cores: [], energy_nodes: [] }, + config: {} as any, + turns: [ + { bots: [], energy: [], cores: [], scores: [], energy_held: [] }, + { bots: [], energy: [], cores: [], scores: [], energy_held: [] }, + ], + win_prob: [ + [0.5, 0.5], + [0.3, 0.7], // delta = 0.4 + ], + result: { turns: 2, winner: 0, reason: 'test', scores: [0, 0] }, + }; + const densities = computeAllDensities(replay); + expect(densities[1].deltaWinProb).toBeCloseTo(0.4, 10); + expect(densities[1].density).toBeCloseTo(4.0, 10); // 0.4 * 10 + }); + }); +});