ai-code-battle/web/src/components/director.test.ts
jedarden d21c621485 test(web): add Director Mode unit tests per §16.10
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 <noreply@anthropic.com>
2026-05-25 23:16:42 -04:00

254 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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.11.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.03.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.05.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
});
});
});