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:
parent
1f43a6a321
commit
0c223aa10d
5 changed files with 139 additions and 4 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<string, string | number | boolean>; // Key-value display data
|
||||
heatmap?: DebugHeatmap;
|
||||
}
|
||||
|
||||
// Extended ReplayTurn with debug support
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue