ai-code-battle/web/src/main.ts
jedarden 4ba39e3aa8 feat(evolver): complete Phase 7 LLM-driven evolution implementation
- Complete autonomous evolution pipeline with island model (4 islands)
- MAP-Elites behavior grid integration for diversity
- LLM ensemble integration (fast + strong model tiers)
- 3-stage validation pipeline (syntax → schema → sandbox smoke test)
- Evaluation arena (10-match mini-tournament per candidate)
- Promotion gate (Nash equilibrium PSRO + MAP-Elites niche fill)
- Retirement policy (auto-retire low-rated bots, population cap)
- Live export to R2 for evolution dashboard
- Enhanced replay viewer with commentary and win probability
- Added series, seasons, and predictions pages

All tests passing. Phase 7 exit criteria met.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 16:38:48 -04:00

251 lines
7.3 KiB
TypeScript

import { ReplayViewer } from './replay-viewer';
import type { Replay } from './types';
// DOM elements
const canvas = document.getElementById('replay-canvas') as HTMLCanvasElement;
const noReplayDiv = document.getElementById('no-replay') as HTMLDivElement;
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const urlInput = document.getElementById('url-input') as HTMLInputElement;
const loadUrlBtn = document.getElementById('load-url-btn') as HTMLButtonElement;
const playBtn = document.getElementById('play-btn') as HTMLButtonElement;
const prevBtn = document.getElementById('prev-btn') as HTMLButtonElement;
const nextBtn = document.getElementById('next-btn') as HTMLButtonElement;
const resetBtn = document.getElementById('reset-btn') as HTMLButtonElement;
const turnDisplay = document.getElementById('turn-display') as HTMLSpanElement;
const totalTurnsSpan = document.getElementById('total-turns') as HTMLSpanElement;
const turnSlider = document.getElementById('turn-slider') as HTMLInputElement;
const speedDisplay = document.getElementById('speed-display') as HTMLSpanElement;
const speedSlider = document.getElementById('speed-slider') as HTMLInputElement;
const fogSelect = document.getElementById('fog-select') as HTMLSelectElement;
const cellSizeSelect = document.getElementById('cell-size-select') as HTMLSelectElement;
const eventLogDiv = document.getElementById('event-log') as HTMLDivElement;
// Info elements
const infoMatchId = document.getElementById('info-match-id') as HTMLElement;
const infoWinner = document.getElementById('info-winner') as HTMLElement;
const infoTurns = document.getElementById('info-turns') as HTMLElement;
const infoReason = document.getElementById('info-reason') as HTMLElement;
// Initialize viewer
let viewer = new ReplayViewer(canvas, { cellSize: 16 });
// Enable controls when replay is loaded
function enableControls(): void {
playBtn.disabled = false;
prevBtn.disabled = false;
nextBtn.disabled = false;
resetBtn.disabled = false;
turnSlider.disabled = false;
noReplayDiv.style.display = 'none';
}
// Update UI state
function updateUI(): void {
turnDisplay.textContent = String(viewer.getTurn());
totalTurnsSpan.textContent = String(viewer.getTotalTurns());
turnSlider.value = String(viewer.getTurn());
// Update play button text
playBtn.textContent = 'Pause';
if (!viewer.getReplay() || viewer.isAtEnd()) {
playBtn.textContent = 'Play';
}
}
// Update event log
function updateEventLog(): void {
const events = viewer.getTurnEvents();
if (events.length === 0) {
eventLogDiv.innerHTML = '<div class="no-replay">No events</div>';
return;
}
eventLogDiv.innerHTML = events.map(e => {
const type = e.type.replace(/_/g, ' ');
return `<div class="event"><span class="event-type">${type}</span></div>`;
}).join('');
}
// Update match info panel
function updateMatchInfo(replay: Replay): void {
infoMatchId.textContent = replay.match_id;
infoTurns.textContent = String(replay.result.turns);
infoReason.textContent = replay.result.reason;
if (replay.result.winner >= 0 && replay.result.winner < replay.players.length) {
infoWinner.textContent = replay.players[replay.result.winner].name;
} else if (replay.result.winner === -1) {
infoWinner.textContent = 'Draw';
} else {
infoWinner.textContent = 'Player ' + replay.result.winner;
}
// Update fog of war options
fogSelect.innerHTML = '<option value="">Disabled (full view)</option>';
replay.players.forEach((player, idx) => {
const option = document.createElement('option');
option.value = String(idx);
option.textContent = player.name;
fogSelect.appendChild(option);
});
}
// Load replay from JSON
function loadReplay(replay: Replay): void {
viewer.loadReplay(replay);
enableControls();
updateMatchInfo(replay);
// Update slider max
turnSlider.max = String(viewer.getTotalTurns() - 1);
updateUI();
updateEventLog();
}
// File input handler
fileInput.addEventListener('change', async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
try {
const text = await file.text();
const replay = JSON.parse(text) as Replay;
loadReplay(replay);
} catch (err) {
alert('Failed to load replay: ' + err);
}
});
// URL load handler
loadUrlBtn.addEventListener('click', async () => {
const url = urlInput.value.trim();
if (!url) return;
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const replay = await response.json() as Replay;
loadReplay(replay);
} catch (err) {
alert('Failed to load replay from URL: ' + err);
}
});
// Playback controls
playBtn.addEventListener('click', () => {
viewer.togglePlay();
});
prevBtn.addEventListener('click', () => {
viewer.setTurn(viewer.getTurn() - 1);
updateUI();
updateEventLog();
});
nextBtn.addEventListener('click', () => {
viewer.setTurn(viewer.getTurn() + 1);
updateUI();
updateEventLog();
});
resetBtn.addEventListener('click', () => {
viewer.pause();
viewer.setTurn(0);
updateUI();
updateEventLog();
});
// Turn slider
turnSlider.addEventListener('input', () => {
viewer.setTurn(parseInt(turnSlider.value, 10));
updateUI();
updateEventLog();
});
// Speed slider
speedSlider.addEventListener('input', () => {
const speed = parseInt(speedSlider.value, 10);
viewer.setSpeed(speed);
speedDisplay.textContent = String(speed);
});
// Fog of war select
fogSelect.addEventListener('change', () => {
const value = fogSelect.value;
viewer.setFogOfWar(value === '' ? null : parseInt(value, 10));
});
// Cell size select
cellSizeSelect.addEventListener('change', () => {
const size = parseInt(cellSizeSelect.value, 10);
const replay = viewer.getReplay();
if (replay) {
viewer = new ReplayViewer(canvas, { cellSize: size });
viewer.onTurnChange = () => { updateUI(); updateEventLog(); };
viewer.onPlayStateChange = (playing) => { playBtn.textContent = playing ? 'Pause' : 'Play'; };
loadReplay(replay);
}
});
// Viewer callbacks
viewer.onTurnChange = () => {
updateUI();
updateEventLog();
};
viewer.onPlayStateChange = (playing) => {
playBtn.textContent = playing ? 'Pause' : 'Play';
};
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (!viewer.getReplay()) return;
switch (e.code) {
case 'Space':
e.preventDefault();
viewer.togglePlay();
break;
case 'ArrowLeft':
e.preventDefault();
viewer.setTurn(viewer.getTurn() - 1);
updateUI();
updateEventLog();
break;
case 'ArrowRight':
e.preventDefault();
viewer.setTurn(viewer.getTurn() + 1);
updateUI();
updateEventLog();
break;
case 'Home':
e.preventDefault();
viewer.setTurn(0);
updateUI();
updateEventLog();
break;
case 'End':
e.preventDefault();
viewer.setTurn(viewer.getTotalTurns() - 1);
updateUI();
updateEventLog();
break;
}
});
console.log('AI Code Battle Replay Viewer initialized');
// Auto-load demo replay
(async () => {
try {
const response = await fetch('/data/demo-replay-v2.json');
if (response.ok) {
const replay = await response.json() as Replay;
loadReplay(replay);
urlInput.value = '/data/demo-replay-v2.json';
}
} catch (e) {
// silently fail - user can load manually
}
})();