phase-9: implement bot debug telemetry with values and heatmap support

Add optional debug field in move response schema with extended telemetry:
- values: key-value pairs for debug display (metrics, state flags)
- heatmap: 2D grid overlay for visualization (threat maps, influence maps)

Engine changes:
- Add Values and Heatmap fields to DebugInfo struct in bot_http.go
- Add DebugHeatmap struct with name and 2D data array

Web viewer changes:
- Extend DebugInfo interface in types.ts with values and heatmap
- Implement heatmap rendering with blue→red gradient overlay
- Add getHeatmapColor helper for normalized value visualization
- Update debug panel to display values as key-value table
- Show heatmap info with name and dimensions

Schema updates:
- Add DebugHeatmap definition to replay-schema-v1.json
- Extend DebugInfo with values and heatmap properties

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-05-08 11:02:23 -04:00
parent 1f43a6a321
commit 0c223aa10d
5 changed files with 139 additions and 4 deletions

View file

@ -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) {

View file

@ -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"
}
}
},

View file

@ -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 += `<div class="debug-player-info">
@ -1106,6 +1114,19 @@ function initReplayViewer(ReplayViewerClass: any, initialUrl?: string): void {
html += `<div class="debug-reasoning">${escapeHtml(info.reasoning)}</div>`;
}
// Display values key-value table
if (info.values && Object.keys(info.values).length > 0) {
html += '<div class="debug-values">';
for (const [key, value] of Object.entries(info.values)) {
const displayValue = typeof value === 'boolean' ? (value ? 'true' : 'false') : String(value);
html += `<div class="debug-value-item">
<span class="debug-value-key">${escapeHtml(key)}:</span>
<span class="debug-value-value">${escapeHtml(displayValue)}</span>
</div>`;
}
html += '</div>';
}
if (info.targets && info.targets.length > 0) {
html += '<div class="debug-targets">';
for (const t of info.targets) {
@ -1120,6 +1141,15 @@ function initReplayViewer(ReplayViewerClass: any, initialUrl?: string): void {
html += '</div>';
}
// Display heatmap info
if (info.heatmap) {
html += `<div class="debug-heatmap">
<span class="debug-heatmap-label">Heatmap:</span>
<span class="debug-heatmap-name">${escapeHtml(info.heatmap.name)}</span>
<span class="debug-heatmap-size">(${info.heatmap.data.length}x${info.heatmap.data[0]?.length || 0})</span>
</div>`;
}
html += '</div>';
}

View file

@ -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<number, DebugInfo>, 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) {

View file

@ -141,9 +141,16 @@ export interface DebugTarget {
priority?: number; // 0.01.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<string, string | number | boolean>; // Key-value display data
heatmap?: DebugHeatmap;
}
// Extended ReplayTurn with debug support