viz/static-tree-renderer.js

/**
 * @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;
    }
}