/**
* @fileoverview StaticTreeRenderer — Schnittstelle zum Rendern statischer Minimax-Bäume
* in tree-viz-v2.html Iframes via IframeBridge.
*
* Wandelt verschachtelte Baumdefinitionen in BATCH ADD_NODE Kommandos um
* und sendet sie über IframeBridgeHost an die tree-viz-v2 Engine.
* Knoten-IDs werden in BFS-Reihenfolge (Ebene für Ebene) vergeben oder können
* per `id`-Feld in der Baumdefinition explizit benannt werden.
*
* @example
* // Baum mit expliziten IDs und automatisch berechneten Minimax-Werten:
* const tree = StaticTreeRenderer.computeMinimaxValues({
* id: 'root',
* children: [
* { id: 'minLeft', children: [{ id: 'leaf3', value: 3 }, { id: 'leaf5', value: 5 }] },
* { id: 'minRight', children: [{ id: 'leaf2', value: 2 }, { id: 'leaf9', value: 9 }] }
* ]
* });
* const renderer = new StaticTreeRenderer(document.getElementById('treeFrame'));
* renderer.renderTree(tree);
* renderer.updateNode('minLeft', { label: '3', status: 'EVALUATED' });
*
* @requires IframeBridgeHost from js/core/iframe-bridge.js
*/
class StaticTreeRenderer {
/**
* @param {HTMLIFrameElement} iframe - Iframe-Element mit src auf tree-viz-v2.html
* @param {Object} [options={}]
* @param {string} [options.sourceId='static-tree'] - Bridge-Kennung
* @param {string} [options.maxColor='#1565c0'] - Farbe für MAX-Knoten
* @param {string} [options.minColor='#c62828'] - Farbe für MIN-Knoten
* @param {boolean} [options.showLevelIndicators=true] - MAX/MIN-Ebenen anzeigen
* @param {number} [options.nodeRadius=30] - Knotenradius
* @param {number} [options.levelHeight=100] - Vertikaler Abstand zwischen Ebenen
* @param {number} [options.horizontalSpacing=80] - Horizontaler Abstand zwischen Knoten
*/
constructor(iframe, options = {}) {
this.iframe = iframe;
this.maxColor = options.maxColor || '#1565c0';
this.minColor = options.minColor || '#c62828';
this._nodeCounter = 0;
this.bridge = new IframeBridgeHost(iframe, {
sourceId: options.sourceId || 'static-tree',
initConfig: {
runtime: {
showLevelIndicators: options.showLevelIndicators !== false,
showOverlay: false,
autoFitZoom: true,
enableActiveNodeTracking: false,
enableZoom: false,
enablePan: false,
rootPlayerColor: this.maxColor,
opponentColor: this.minColor,
nodeRadius: options.nodeRadius || 30,
levelHeight: options.levelHeight || 100,
horizontalSpacing: options.horizontalSpacing || 80
}
}
});
}
/**
* Rendert einen Baum aus einer verschachtelten Definition.
* Knoten-IDs werden in BFS-Reihenfolge (Ebene für Ebene) vergeben, sofern kein
* explizites `id`-Feld gesetzt ist. Dadurch liegen Geschwisterknoten direkt
* nebeneinander in der Nummerierung (n1/n2 statt n1/n4).
*
* @param {Object} treeDef - Baumdefinition
* @param {string} [treeDef.id] - Explizite Knoten-ID (überschreibt Auto-ID)
* @param {number|string|null} [treeDef.value] - Wert/Label (null/undefined = leer)
* @param {string} [treeDef.status] - Status-Override ('EVALUATED'|'PRUNED'|'WAIT'|'READY')
* @param {string} [treeDef.edgeLabel] - Kantenbeschriftung zum Elternknoten
* @param {Object} [treeDef.extraMetadata] - Zusätzliche Metadata-Felder
* @param {Array<Object>} [treeDef.children] - Kindknoten
* @returns {StaticTreeRenderer} this (für Chaining)
*/
renderTree(treeDef) {
this._nodeCounter = 0;
const commands = [];
// BFS — IDs level-by-level, parent always before child (required by ADD_NODE)
const queue = [{ node: treeDef, parentId: null, depth: 0 }];
while (queue.length > 0) {
const { node, parentId, depth } = queue.shift();
const id = node.id != null ? String(node.id) : `n${this._nodeCounter++}`;
const isMax = depth % 2 === 0;
const color = isMax ? this.maxColor : this.minColor;
const isLeaf = !node.children || node.children.length === 0;
const cmd = {
action: 'ADD_NODE',
id,
parentId,
label: node.value !== null && node.value !== undefined ? String(node.value) : '',
color,
status: node.status || 'EVALUATED',
metadata: { depth, isMax, isLeaf, ...(node.extraMetadata || {}) }
};
if (node.edgeLabel != null) cmd.edgeLabel = String(node.edgeLabel);
commands.push(cmd);
if (node.children) {
node.children.forEach(child =>
queue.push({ node: child, parentId: id, depth: depth + 1 })
);
}
}
this.bridge.send('TREE:COMMAND', { action: 'BATCH', commands });
return this;
}
/**
* Aktualisiert einen einzelnen Knoten (z.B. für schrittweise Wertpropagation).
*
* @param {string} nodeId - ID des Knotens (z.B. 'n0', 'root', 'minLeft')
* @param {Object} data - Update-Daten (label, status, color, etc.)
* @returns {StaticTreeRenderer} this
*/
updateNode(nodeId, data) {
this.bridge.send('TREE:COMMAND', {
action: 'UPDATE_NODE',
id: nodeId,
...data
});
return this;
}
/**
* Räumt Bridge-Ressourcen auf.
*/
destroy() {
if (this.bridge) this.bridge.destroy();
}
// ==================== STATIC ====================
/**
* Berechnet Minimax-Werte für alle internen Knoten eines Baums.
* Blattknoten müssen bereits `value` gesetzt haben. Interne Knoten ohne
* `value` (null/undefined) werden durch den Minimax-Algorithmus befüllt.
*
* Entspricht direkt der Logik aus MinimaxEngine._minimax() und stellt sicher,
* dass alle dargestellten Werte algorithmisch korrekt sind.
*
* @param {Object} treeDef - Baumdefinition mit Blattwerten
* @param {boolean} [isMaximizing=true] - MAX an der Wurzel?
* @returns {Object} Tiefenkopie mit berechneten Werten für alle Knoten
*/
static computeMinimaxValues(treeDef, isMaximizing = true) {
const node = { ...treeDef };
const isLeaf = !node.children || node.children.length === 0;
if (isLeaf) return node;
node.children = node.children.map(c =>
StaticTreeRenderer.computeMinimaxValues(c, !isMaximizing)
);
// Only fill in value if not explicitly set
if (node.value === null || node.value === undefined) {
const childValues = node.children
.map(c => c.value)
.filter(v => v !== null && v !== undefined);
if (childValues.length > 0) {
node.value = isMaximizing
? Math.max(...childValues)
: Math.min(...childValues);
}
}
return node;
}
}