diff --git a/engine/bot_http.go b/engine/bot_http.go index 5ab6ea8..8a3ef53 100644 --- a/engine/bot_http.go +++ b/engine/bot_http.go @@ -80,8 +80,10 @@ type MoveResponse struct { // DebugInfo contains optional debug telemetry from the bot. type DebugInfo struct { - Reasoning string `json:"reasoning,omitempty"` - Targets []DebugTarget `json:"targets,omitempty"` + Reasoning string `json:"reasoning,omitempty"` + Targets []DebugTarget `json:"targets,omitempty"` + Values map[string]interface{} `json:"values,omitempty"` + Heatmap *DebugHeatmap `json:"heatmap,omitempty"` } // DebugTarget represents a debug target marker. @@ -91,6 +93,12 @@ type DebugTarget struct { Priority float64 `json:"priority"` } +// DebugHeatmap represents a 2D grid overlay for visualization. +type DebugHeatmap struct { + Name string `json:"name"` // e.g., "threat", "influence" + Data [][]float64 `json:"data"` // 2D array of values (row-major) +} + // GetMoves sends the game state to the bot and returns its moves. // Implements BotInterface. func (b *HTTPBot) GetMoves(state *VisibleState) ([]Move, error) { diff --git a/web/public/replay-schema-v1.json b/web/public/replay-schema-v1.json index ca12c0e..95c07f6 100644 --- a/web/public/replay-schema-v1.json +++ b/web/public/replay-schema-v1.json @@ -485,6 +485,30 @@ } } }, + "DebugHeatmap": { + "type": "object", + "description": "2D grid overlay for bot telemetry visualization (e.g., threat, influence).", + "required": ["name", "data"], + "properties": { + "name": { + "type": "string", + "description": "Heatmap name (e.g., 'threat', 'influence').", + "maxLength": 64 + }, + "data": { + "type": "array", + "description": "2D array of values in row-major order (data[row][col]). Each value is normalized to [0.0, 1.0].", + "items": { + "type": "array", + "items": { + "type": "number", + "minimum": 0, + "maximum": 1 + } + } + } + } + }, "DebugInfo": { "type": "object", "description": "Optional bot debug telemetry (stored if bot provides debug field in move response).", @@ -500,6 +524,21 @@ "items": { "$ref": "#/$defs/DebugTarget" } + }, + "values": { + "type": "object", + "description": "Key-value pairs for debug display (e.g., metrics, state flags). Values are strings, numbers, or booleans.", + "additionalProperties": { + "oneOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "boolean" } + ] + } + }, + "heatmap": { + "description": "2D grid overlay for visualization (e.g., threat map, influence map).", + "$ref": "#/$defs/DebugHeatmap" } } }, diff --git a/web/src/pages/replay.ts b/web/src/pages/replay.ts index 04ce96f..a5e5059 100644 --- a/web/src/pages/replay.ts +++ b/web/src/pages/replay.ts @@ -409,9 +409,17 @@ function initReplayViewerWithClass(ReplayViewerClass: any, initialUrl?: string): .debug-player-info { background-color: var(--bg-tertiary); border-radius: 6px; padding: 10px; } .debug-player-name { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; margin-bottom: 6px; font-weight: 600; } .debug-reasoning { color: var(--text-secondary); font-size: 0.8rem; line-height: 1.5; margin-bottom: 8px; } + .debug-values { display: flex; flex-direction: column; gap: 4px; margin-bottom: 8px; } + .debug-value-item { font-size: 0.75rem; font-family: monospace; color: var(--text-muted); display: flex; align-items: center; gap: 6px; } + .debug-value-key { font-weight: 600; } + .debug-value-value { color: var(--text-secondary); } .debug-targets { display: flex; flex-direction: column; gap: 4px; } .debug-target-item { font-size: 0.75rem; font-family: monospace; color: var(--text-muted); display: flex; align-items: center; gap: 6px; } .debug-target-priority { opacity: 0.7; } + .debug-heatmap { font-size: 0.75rem; color: var(--text-muted); display: flex; align-items: center; gap: 4px; flex-wrap: wrap; } + .debug-heatmap-label { font-weight: 600; } + .debug-heatmap-name { color: var(--accent); } + .debug-heatmap-size { opacity: 0.7; } .no-debug-data { color: var(--text-muted); font-size: 0.8rem; font-style: italic; } /* Transcript panel (§15.3) */ .transcript-panel { padding: 0; overflow: hidden; } @@ -1096,7 +1104,7 @@ function initReplayViewer(ReplayViewerClass: any, initialUrl?: string): void { if (viewer.getDebugPlayerEnabled(idx) === false) continue; const color = playerColors[idx] || '#888'; const playerName = replay?.players[idx]?.name ?? `Player ${idx}`; - const hasData = !!(info.reasoning || (info.targets && info.targets.length > 0)); + const hasData = !!(info.reasoning || (info.targets && info.targets.length > 0) || (info.values && Object.keys(info.values).length > 0) || info.heatmap); if (!hasData) continue; html += `
@@ -1106,6 +1114,19 @@ function initReplayViewer(ReplayViewerClass: any, initialUrl?: string): void { html += `
${escapeHtml(info.reasoning)}
`; } + // Display values key-value table + if (info.values && Object.keys(info.values).length > 0) { + html += '
'; + for (const [key, value] of Object.entries(info.values)) { + const displayValue = typeof value === 'boolean' ? (value ? 'true' : 'false') : String(value); + html += `
+ ${escapeHtml(key)}: + ${escapeHtml(displayValue)} +
`; + } + html += '
'; + } + if (info.targets && info.targets.length > 0) { html += '
'; for (const t of info.targets) { @@ -1120,6 +1141,15 @@ function initReplayViewer(ReplayViewerClass: any, initialUrl?: string): void { html += '
'; } + // Display heatmap info + if (info.heatmap) { + html += `
+ Heatmap: + ${escapeHtml(info.heatmap.name)} + (${info.heatmap.data.length}x${info.heatmap.data[0]?.length || 0}) +
`; + } + html += '
'; } diff --git a/web/src/replay-viewer.ts b/web/src/replay-viewer.ts index 00ada4c..ddcc0a4 100644 --- a/web/src/replay-viewer.ts +++ b/web/src/replay-viewer.ts @@ -2180,9 +2180,17 @@ export class ReplayViewer { return `rgba(${r}, ${g}, ${b}, ${alpha})`; } + // Get heatmap color from normalized value (0=blue/cold, 1=red/hot) + private getHeatmapColor(t: number): string { + // Blue (0,0,255) to Red (255,0,0) gradient + const r = Math.floor(Math.min(255, Math.max(0, t * 255 * 2))); + const b = Math.floor(Math.min(255, Math.max(0, (1 - t) * 255 * 2))); + return `rgb(${r}, 0, ${b})`; + } + // Render debug telemetry overlay private renderDebugOverlay(debug: Record, colors: string[]): void { - const { ctx, cellSize } = this; + const { ctx, cellSize, replay } = this; let reasoningRow = 0; for (const [playerId, info] of Object.entries(debug)) { @@ -2193,6 +2201,49 @@ export class ReplayViewer { const color = colors[playerIdx] || '#ffffff'; + // Draw debug heatmap (2D grid overlay with semi-transparent colors) + if (info.heatmap && replay) { + const { rows, cols } = replay.map; + const heatmapData = info.heatmap.data; + + // Validate heatmap dimensions match map dimensions + if (heatmapData.length === rows && heatmapData[0]?.length === cols) { + // Find min/max values for normalization + let minVal = Infinity; + let maxVal = -Infinity; + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + const v = heatmapData[r][c]; + if (typeof v === 'number') { + minVal = Math.min(minVal, v); + maxVal = Math.max(maxVal, v); + } + } + } + + // Only render if there's meaningful data + if (maxVal > minVal) { + const range = maxVal - minVal || 1; + + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + const value = heatmapData[r][c]; + if (typeof value !== 'number') continue; + + // Normalize to 0-1 and apply color gradient + const t = (value - minVal) / range; + // Blue (low) → Red (high) gradient + const heatmapColor = this.getHeatmapColor(t); + ctx.globalAlpha = 0.4; // semi-transparent + ctx.fillStyle = heatmapColor; + ctx.fillRect(c * cellSize, r * cellSize, cellSize, cellSize); + } + } + ctx.globalAlpha = 1.0; + } + } + } + // Draw debug targets with priority-based opacity if (info.targets) { for (const target of info.targets) { diff --git a/web/src/types.ts b/web/src/types.ts index dc2c0ba..c3ae600 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -141,9 +141,16 @@ export interface DebugTarget { priority?: number; // 0.0–1.0; controls marker opacity (1 = fully opaque) } +export interface DebugHeatmap { + name: string; // e.g., "threat", "influence" + data: number[][]; // 2D array of values (row-major) +} + export interface DebugInfo { reasoning?: string; targets?: DebugTarget[]; + values?: Record; // Key-value display data + heatmap?: DebugHeatmap; } // Extended ReplayTurn with debug support