viz/adapters/tree-adapters/alpha-beta-tree-adapter.js

/**
 * @fileoverview Alpha-Beta Tree Adapter - Visualisierung mit Pruning
 * 
 * Extends MinimaxTreeAdapter mit Alpha-Beta Pruning Features:
 * - Pruned-Node Visualisierung und Tracking
 * - Alpha/Beta Threshold Anzeige
 * - Performance-Verbesserungen durch Cut-Off-Highlighting
 * 
 * @class AlphaBetaTreeAdapter
 * @extends MinimaxTreeAdapter
 * @author Alexander Wolf
 * @version 2.3
 */
class AlphaBetaTreeAdapter extends MinimaxTreeAdapter {
    /**
     * Konstruktor mit lokalem Cache für Strict-Marker-Updates.
     * @param {HTMLIFrameElement} iframeElement
     */
    constructor(iframeElement) {
        super(iframeElement);
        this._strictNextByParent = new Map();
    }

    /**
     * @returns {boolean}
     */
    _isStrictOrderEnabled() {
        const flags = typeof MINIMAX_VIZ_CONSTANTS !== 'undefined' ? MINIMAX_VIZ_CONSTANTS.FLAGS || {} : {};
        return flags.ENFORCE_ALPHABETA_EVAL_ORDER === true;
    }

    /**
     * Liefert den nächsten offenen Kindknoten eines Elternknotens.
     * @param {Object} parent
     * @returns {number|null}
     */
    _getNextOpenChild(parent) {
        if (!parent || !Array.isArray(parent.children)) return null;
        const nextChild = parent.children.find((childId) => {
            const child = this.treeStructure.get(childId);
            return child && child.status !== 'EVALUATED' && child.status !== 'PRUNED';
        });
        return nextChild !== undefined ? nextChild : null;
    }

    /**
     * Synchronisiert strikte Evaluationsreihenfolge pro Elternknoten.
     * - Nur der nächste offene Kindknoten bleibt READY.
     * - Andere offene READY-Kinder werden auf WAIT gesetzt.
     * - Der nächste offene Kindknoten erhält Marker `NEXT_TO_EVALUATE`.
     * @param {number} parentId
     */
    _syncStrictOrderingForParent(parentId) {
        if (!this._isStrictOrderEnabled()) return;

        const parent = this.treeStructure.get(parentId);
        if (!parent || !Array.isArray(parent.children) || parent.children.length === 0) return;

        const nextChildId = this._getNextOpenChild(parent);
        const previousNextChildId = this._strictNextByParent.has(parentId)
            ? this._strictNextByParent.get(parentId)
            : null;
        this._strictNextByParent.set(parentId, nextChildId);

        parent.children.forEach((childId) => {
            this.commands.push({
                action: 'UPDATE_NODE',
                id: childId,
                removeStatus: 'NEXT_TO_EVALUATE',
            });
        });

        if (nextChildId === null) return;

        // Recompute natural status for selected child first.
        // This prevents strict deadlocks where a formerly READY child
        // was demoted to WAIT and never promoted back.
        super.checkNodeStatus(nextChildId);

        const selectedChild = this.treeStructure.get(nextChildId);

        parent.children.forEach((childId) => {
            const child = this.treeStructure.get(childId);
            if (!child) return;
            if (child.status === 'EVALUATED' || child.status === 'PRUNED') return;

            if (childId === nextChildId) {
                if (selectedChild && selectedChild.status === 'READY') {
                    this.commands.push({
                        action: 'UPDATE_NODE',
                        id: childId,
                        addStatus: 'NEXT_TO_EVALUATE',
                    });
                }
                return;
            }

            if (child.status === 'READY') {
                NodeStatusManager.setNodeStatus(childId, 'WAIT', [], this.treeStructure, this.commands);
            }
        });

        if (previousNextChildId !== nextChildId) {
            this._debugAB('strict-next-child', { parentId, nextChildId });
        }
    }

    /**
     * Expandiert Kinder eines geprunten Knotens rein zur Visualisierung.
     * Die erzeugten Kinder bleiben PRUNED und sind nicht bewertbar.
     * @param {number} nodeId
     * @param {GameState} state
     */
    _expandPrunedNodeChildren(nodeId, state) {
        if (this._isTerminalState(state)) return;

        const currentData = this.treeStructure.get(nodeId);
        if (!currentData) return;

        const validMoves = state.getAllValidMoves();
        const orderedMoves = this._orderMoves(validMoves, state, currentData);

        for (const move of orderedMoves) {
            const childState = state.clone();
            childState.makeMove(move);

            const metadata = this._createChildMetadata(nodeId, childState, move, currentData);
            const childId = this.createNode(childState, nodeId, metadata, 'PRUNED');

            this.nodeStates.set(childId, childState);

            const childStruct = this._createChildStructure(childId, nodeId, childState, currentData);
            childStruct.status = 'PRUNED';
            childStruct.move = move;
            this.treeStructure.set(childId, childStruct);

            currentData.children.push(childId);

            NodeStatusManager.setNodeStatus(childId, 'PRUNED', [], this.treeStructure, this.commands);

            if (!childStruct.isTerminal) {
                this.commands.push({ action: 'MARK_EXPANDABLE', id: childId });
            }
        }
    }

    /**
     * Schätzt die Anzahl potenziell prunbarer Knoten eines Teilbaums.
     * Nutzt vorhandene Utility-Funktion, inkl. Option für Blattknoten-Zählung.
     * @param {number} nodeId
     * @param {boolean} [leafOnly=false]
     * @returns {number}
     */
    _estimatePotentialSubtreeSize(nodeId, leafOnly = false) {
        const state = this.nodeStates.get(nodeId);
        if (!state) return 0;

        if (typeof window === 'undefined' || !window.TreeAnalysisUtils || typeof window.TreeAnalysisUtils.countPossibleNodesFromState !== 'function') {
            return 1;
        }

        return window.TreeAnalysisUtils.countPossibleNodesFromState(
            state,
            (candidateState) => this._isTerminalState(candidateState),
            {
                leafOnly,
                includeStart: true,
            }
        );
    }

    /**
     * Zählt bereits evaluierte Knoten im aktuell materialisierten Teilbaum.
     * @param {number} nodeId
     * @param {boolean} [leafOnly=false]
     * @returns {number}
     */
    _countEvaluatedStoredNodes(nodeId, leafOnly = false) {
        if (typeof window === 'undefined' || !window.TreeAnalysisUtils || typeof window.TreeAnalysisUtils.countStoredSubtreeNodes !== 'function') {
            return 0;
        }

        return window.TreeAnalysisUtils.countStoredSubtreeNodes(this.treeStructure, nodeId, {
            leafOnly,
            includeStart: true,
            filter: (node) => node.status === 'EVALUATED',
        });
    }

    /**
     * Ermittelt neue Pruning-Knotenanzahl auf Basis des vollständigen Potenzial-Teilbaums.
     * @param {number} nodeId
     * @returns {number}
     */
    _estimateNewlyPrunedCount(nodeId) {
        const potentialAll = this._estimatePotentialSubtreeSize(nodeId, false);
        const evaluatedAll = this._countEvaluatedStoredNodes(nodeId, false);

        if (potentialAll <= 0) return 0;

        return Math.max(1, potentialAll - evaluatedAll);
    }

    /**
     * Erweitert Knoten; geprunte Knoten werden visualisierend erweitert.
     * @param {number} nodeId
     * @param {GameState} state
     */
    expandNodeChildren(nodeId, state) {
        const data = this.treeStructure.get(nodeId);
        if (!data) return;

        if (data.status === 'PRUNED') {
            this._expandPrunedNodeChildren(nodeId, state);
            this.checkNodeStatus(nodeId);
            return;
        }

        super.expandNodeChildren(nodeId, state);
    }

    /**
     * Sortiert Züge optional in günstiger AB-Reihenfolge.
     * Aktuell mit generischer Heuristik + TTT-3x3-Spezialfall (Center -> Ecken -> Kanten).
     * @param {Array} validMoves
     * @param {GameState} state
     * @returns {Array}
     */
    _orderMoves(validMoves, state) {
        const flags = typeof MINIMAX_VIZ_CONSTANTS !== 'undefined' ? MINIMAX_VIZ_CONSTANTS.FLAGS || {} : {};
        if (flags.USE_ALPHABETA_MOVE_ORDERING !== true) return validMoves;

        const sortedMoves = [...validMoves];

        const isRegularTTT =
            state &&
            Array.isArray(state.grid) &&
            state.grid.length === GAME_CONSTANTS.TTT_BOARD_SIZE;

        if (isRegularTTT) {
            const corners = new Set(GAME_CONSTANTS.TTT_CORNERS);
            const centerIndex = GAME_CONSTANTS.TTT_CENTER_INDEX;

            sortedMoves.sort((a, b) => {
                const rank = (move) => {
                    if (move === centerIndex) return 0;
                    if (corners.has(move)) return 1;
                    return 2;
                };
                const rankA = rank(a);
                const rankB = rank(b);
                if (rankA !== rankB) return rankA - rankB;
                return a - b;
            });
        }

        this._debugAB('move-ordering', {
            before: validMoves,
            after: sortedMoves,
        });

        return sortedMoves;
    }

    /**
     * Debug Logging für Alpha-Beta.
     * Nutzt die zentrale _debugLog-Infrastruktur aus BaseTreeAdapter
     * mit zusätzlichem Flag-Gate für AB-spezifisches Debug-Logging.
     * @param {string} message - Log-Nachricht
     * @param {Object} [payload] - Optionaler Payload
     * @private
     */
    _debugAB(message, payload) {
        const flags = typeof MINIMAX_VIZ_CONSTANTS !== 'undefined' ? MINIMAX_VIZ_CONSTANTS.FLAGS || {} : {};
        if (flags.DEBUG_ALPHABETA_ADAPTER !== true) {
            const hasDebugConfig = typeof DebugConfig !== 'undefined' && typeof DEBUG_DOMAINS !== 'undefined';
            if (!hasDebugConfig || !DebugConfig.shouldLog(DEBUG_DOMAINS.ABPRUNING, 'debug')) return;
        }
        this._debugLog(message, payload);
    }

    /**
     * Kritischer Logger (warn/error) für AB-Kontext.
     * @param {'warn'|'error'} level - Log-Level
     * @param {string} message - Log-Nachricht
     * @param {Object} [payload] - Optionaler Payload
     * @private
     */
    _logCritical(level, message, payload) {
        this._debugLog(message, payload, level);
    }

    /**
     * Liefert Initialwerte für Alpha/Beta.
     * @returns {{alpha:number,beta:number}}
     */
    _getInitialBounds() {
        const alpha = typeof AI_CONSTANTS !== 'undefined' && AI_CONSTANTS.ALPHA_INIT !== undefined
            ? AI_CONSTANTS.ALPHA_INIT
            : Number.NEGATIVE_INFINITY;
        const beta = typeof AI_CONSTANTS !== 'undefined' && AI_CONSTANTS.BETA_INIT !== undefined
            ? AI_CONSTANTS.BETA_INIT
            : Number.POSITIVE_INFINITY;
        return { alpha, beta };
    }

    onTreeReady() {
        super.onTreeReady();
        const flags = typeof MINIMAX_VIZ_CONSTANTS !== 'undefined' ? MINIMAX_VIZ_CONSTANTS.FLAGS || {} : {};
        this.sendCommand({
            action: 'UPDATE_CONFIG',
            config: {
                enableTreeExpansion: flags.ENABLE_TREE_EXPANSION_ALPHABETA === true,
            }
        });
    }

    async visualizeSearch(gameState, config) {
        DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_ALPHABETA, "debug",
            '[AlphaBetaAdapter] visualizeSearch START',
            { algorithm: config?.algorithm, maxDepth: config?.maxDepth });
        
        this.currentGameState = gameState;
        this.currentConfig = config;
        this.rootPlayer = gameState.currentPlayer;
        this.stats.nodesVisited = 0;
        this.stats.nodesPruned = 0;
        this.stats.evaluatedNodes = 0;
        this._notifyStatsChanged();
        
        this.reset();
        const initial = this._getInitialBounds();
        
        // Root Node with Alpha/Beta
        const rootId = this.createNode(gameState, null, {
            depth: this.constants.rootDepth,
            isMaximizing: true,
            inheritedAlpha: initial.alpha,
            inheritedBeta: initial.beta,
            alpha: initial.alpha,
            beta: initial.beta,
        });
        
        this.nodeStates.set(rootId, gameState);
        this.treeStructure.set(rootId, { 
            parentId: null, 
            children: [], 
            status: 'WAIT',
            value: null, 
            depth: this.constants.rootDepth,
            isMaximizing: true,
            alpha: initial.alpha,
            beta: initial.beta,
            inheritedAlpha: initial.alpha,
            inheritedBeta: initial.beta,
        });
        
        this.expandNodeChildren(rootId, gameState);
        this.checkNodeStatus(rootId);
        
        DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_ALPHABETA, "debug",
            '[AlphaBetaAdapter] visualizeSearch: About to flushCommands',
            { nodeCount: this.nodeIdCounter, alpha: initial.alpha, beta: initial.beta });
        
        this.flushCommands();
        
        DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_ALPHABETA, "debug",
            '[AlphaBetaAdapter] visualizeSearch COMPLETE',
            { rootId, initialStats: this.stats });
        
        this._debugAB('visualizeSearch:ready', { rootId, alpha: initial.alpha, beta: initial.beta });
        
        return this._buildResult(rootId);
    }

    getInitialConfig() {
        const config = super.getInitialConfig();
        const flags = typeof MINIMAX_VIZ_CONSTANTS !== 'undefined' ? MINIMAX_VIZ_CONSTANTS.FLAGS || {} : {};
        // Override for AlphaBeta
        config.enableTreeExpansion = flags.ENABLE_TREE_EXPANSION_ALPHABETA === true;
        return config;
    }

    /**
     * Override metadata creation to include Alpha/Beta from parent
     */
    _createChildMetadata(nodeId, childState, move, currentData) {
        // Calculate current effective alpha/beta for NEW children
        // (Inherit from parent)
        const initial = this._getInitialBounds();
        const currentAlpha = currentData ? currentData.alpha : initial.alpha;
        const currentBeta = currentData ? currentData.beta : initial.beta;

        return {
            depth: currentData.depth + 1,
            isMaximizing: !currentData.isMaximizing,
            inheritedAlpha: currentAlpha,
            inheritedBeta: currentBeta,
            alpha: currentAlpha,
            beta: currentBeta,
            move: move,
            moveOrder: currentData.children.length,
        };
    }

    _createChildStructure(childId, nodeId, childState, currentData) {
        const initial = this._getInitialBounds();
        const currentAlpha = currentData ? currentData.alpha : initial.alpha;
        const currentBeta = currentData ? currentData.beta : initial.beta;

        return {
            parentId: nodeId,
            children: [],
            status: 'WAIT',
            value: null,
            depth: currentData.depth + 1,
            isMaximizing: !currentData.isMaximizing,
            isTerminal: this._isTerminalState(childState),
            alpha: currentAlpha,
            beta: currentBeta,
            inheritedAlpha: currentAlpha,
            inheritedBeta: currentBeta,
            moveOrder: currentData.children.length,
        };
    }

    /**
     * Erweiterte Statusprüfung mit Debug für READY-Übergänge.
     * @param {number} nodeId
     */
    checkNodeStatus(nodeId) {
        const data = this.treeStructure.get(nodeId);
        if (!data) return;

        const oldStatus = data.status;
        super.checkNodeStatus(nodeId);

        if (data.parentId !== null) {
            this._syncStrictOrderingForParent(data.parentId);
        }

        if (Array.isArray(data.children) && data.children.length > 0) {
            this._syncStrictOrderingForParent(nodeId);
        }

        const updated = this.treeStructure.get(nodeId);
        if (!updated || oldStatus === updated.status) return;

        if (updated.status === 'READY') {
            const childStatuses = updated.children.map((childId) => {
                const child = this.treeStructure.get(childId);
                return child ? child.status : 'UNKNOWN';
            });

            const evaluatedCount = childStatuses.filter((status) => status === 'EVALUATED').length;
            const prunedCount = childStatuses.filter((status) => status === 'PRUNED').length;

            this._debugAB('node-ready', {
                nodeId,
                oldStatus,
                newStatus: updated.status,
                childCount: updated.children.length,
                evaluatedCount,
                prunedCount,
                childStatuses,
            });
        }
    }

    /**
     * Optional: Prüft erzwungene Evaluationsreihenfolge.
     * @param {number} nodeId
     * @returns {boolean}
     */
    _isAllowedByOrder(nodeId) {
        if (!this._isStrictOrderEnabled()) return true;

        const data = this.treeStructure.get(nodeId);
        if (!data || data.parentId === null) return true;

        const parent = this.treeStructure.get(data.parentId);
        if (!parent) return true;

        const nextChild = this._getNextOpenChild(parent);

        if (nextChild === null) {
            return true;
        }

        const allowed = nextChild === nodeId;
        if (!allowed) {
            this._debugAB('order-blocked', { nodeId, expectedChild: nextChild });
            if (nextChild !== null) {
                this.commands.push({
                    action: 'UPDATE_NODE',
                    id: nextChild,
                    addStatus: 'NEXT_TO_EVALUATE',
                });
                this.flushCommands();
            }
        }
        return allowed;
    }

    /**
     * Rechnet Alpha/Beta eines Nodes aus seinen ausgewerteten Kindern neu.
     * Visualisiert den AB-Informationsfluss über farbkodierte Kanten:
     * - Rot: α-Update (MAX-Knoten aktualisiert Alpha)
     * - Blau: β-Update (MIN-Knoten aktualisiert Beta)
     * - Propagation nach unten an offene Kinder in gleicher Farbe
     *
     * @param {number} nodeId
     * @param {number} [evaluatedChildId=null] - Kind das gerade evaluiert wurde
     */
    _recomputeNodeBounds(nodeId, evaluatedChildId = null) {
        const node = this.treeStructure.get(nodeId);
        if (!node) return;

        // Alte Bounds sichern für Änderungserkennung
        const oldAlpha = node.alpha;
        const oldBeta = node.beta;

        let alpha = node.inheritedAlpha;
        let beta = node.inheritedBeta;

        const evaluatedChildren = node.children
            .map((childId) => this.treeStructure.get(childId))
            .filter((child) => child && child.status === 'EVALUATED');

        if (node.isMaximizing && evaluatedChildren.length > 0) {
            const maxChild = Math.max(...evaluatedChildren.map((child) => child.value));
            alpha = Math.max(alpha, maxChild);
        }

        if (!node.isMaximizing && evaluatedChildren.length > 0) {
            const minChild = Math.min(...evaluatedChildren.map((child) => child.value));
            beta = Math.min(beta, minChild);
        }

        node.alpha = alpha;
        node.beta = beta;

        // ---- AB-Propagation Edge-Highlighting ----
        const alphaChanged = node.alpha > oldAlpha;
        const betaChanged = node.beta < oldBeta;

        if (evaluatedChildId !== null && (alphaChanged || betaChanged)) {
            const propColor = alphaChanged
                ? this.constants.edgeAlphaPropColor
                : this.constants.edgeBetaPropColor;
            const propWidth = this.constants.edgePropWidth;

            // Edge vom evaluierten Kind zum Elternknoten (Wert geht nach oben)
            this.commands.push({
                action: 'HIGHLIGHT_EDGE',
                from: nodeId,
                to: evaluatedChildId,
                color: propColor,
                width: propWidth,
                showArrow: true,
                arrowDirection: 'from',  // Kind → Eltern (Wert fließt nach oben)
            });

            // Propagation nach unten: Bounds rekursiv an alle offenen Nachkommen
            node.children.forEach(childId => {
                if (childId === evaluatedChildId) return;
                const child = this.treeStructure.get(childId);
                if (!child || child.status === 'EVALUATED' || child.status === 'PRUNED') return;
                this.commands.push({
                    action: 'HIGHLIGHT_EDGE',
                    from: nodeId,
                    to: childId,
                    color: propColor,
                    width: propWidth,
                    showArrow: true,
                    arrowDirection: 'to',
                });
            });
        }

        this._debugAB('bounds-recomputed', {
            nodeId,
            inheritedAlpha: node.inheritedAlpha,
            inheritedBeta: node.inheritedBeta,
            alpha,
            beta,
            evaluatedChildren: evaluatedChildren.length,
            isMaximizing: node.isMaximizing,
        });

        this.commands.push({
            action: 'UPDATE_NODE',
            id: nodeId,
            data: {
                metadata: {
                    depth: node.depth,
                    isMaximizing: node.isMaximizing,
                    moveOrder: node.moveOrder,
                    value: node.value,
                    alpha: node.alpha,
                    beta: node.beta,
                },
            },
        });

        // Bounds an noch nicht evaluierte Kinder weiterreichen,
        // damit AB-Anzeige und Cutoff-Logik konsistent bleiben.
        node.children.forEach((childId) => {
            const child = this.treeStructure.get(childId);
            if (!child || child.status === 'EVALUATED' || child.status === 'PRUNED') return;

            child.inheritedAlpha = node.alpha;
            child.inheritedBeta = node.beta;
            child.alpha = node.alpha;
            child.beta = node.beta;

            this.commands.push({
                action: 'UPDATE_NODE',
                id: childId,
                data: {
                    metadata: {
                        depth: child.depth,
                        isMaximizing: child.isMaximizing,
                        moveOrder: child.moveOrder,
                        value: child.value,
                        alpha: child.alpha,
                        beta: child.beta,
                    },
                },
            });
        });
    }

    /**
     * Führt die Bewertung eines Knotens durch.
     */
    evaluateNode(nodeId) {
        const data = this.treeStructure.get(nodeId);
        const state = this.nodeStates.get(nodeId);
        if (!data || !state) {
            this._debugAB('evaluateNode:missing-data', { nodeId });
            return;
        }

        if (!this._isAllowedByOrder(nodeId)) {
            return;
        }

        this.commands = [];
        this.stats.nodesVisited += 1;
        this.stats.evaluatedNodes += 1;
        this._notifyStatsChanged();
        
        let value = 0;
        let labelText = "";
        let statusesToAdd = ['EVALUATED'];
        
        // --- CALCULATION ---
        if (data.children.length === 0) {
            const leafResult = this._evaluateTerminalState(state);
            value = leafResult.value;
            labelText = leafResult.labelText;
            statusesToAdd = leafResult.statusesToAdd;
        } else {
            // Inner Node Evaluation
            // We only look at EVALUATED children (ignore pruned/wait)
            const evaluatedChildren = data.children
                .map(cid => this.treeStructure.get(cid))
                .filter(c => c.status === 'EVALUATED'); // Only real values
            
            if (evaluatedChildren.length === 0) {
                 // Should not happen if logic is correct
                  this._logCritical('error', '[AlphaBetaTreeAdapter] evaluateNode: inner node has no evaluated children', { nodeId });
                 return;
            }

            const values = evaluatedChildren.map(c => c.value);
            
            if (data.isMaximizing) {
                value = Math.max(...values);
                labelText = `Max = ${value}`;
            } else {
                value = Math.min(...values);
                labelText = `Min = ${value}`;
            }
            
            // Highlight Best Edge
            data.children.forEach(childId => {
                const child = this.treeStructure.get(childId);
                if (child && child.status === 'EVALUATED' && child.value === value) {
                    const c = this._getEdgeColorForValue(value);
                    this.commands.push({
                        action: 'HIGHLIGHT_EDGE',
                        from: nodeId,
                        to: childId,
                        color: c,
                        width: this.constants.edgeWidth,
                        showArrow: true,
                        arrowDirection: 'from',  // Kind → Eltern (bester Wert)
                    });
                }
            });

            statusesToAdd = ['EVALUATED', ...this._getValueStatuses(value)];
        }
        
        // Update data
        data.value = value;
        this._recomputeNodeBounds(nodeId);
        
        // Set status and add additional statuses
        NodeStatusManager.setNodeStatus(nodeId, 'EVALUATED', statusesToAdd, this.treeStructure, this.commands);
        
        // Update label and metadata
        this.commands.push({
            action: 'UPDATE_NODE',
            id: nodeId,
            data: {
                label: labelText,
                metadata: {
                    depth: data.depth,
                    isMaximizing: data.isMaximizing,
                    moveOrder: data.moveOrder,
                    value: value,
                    alpha: data.alpha,
                    beta: data.beta,
                }
            }
        });
        
        // --- PRUNING CHECK (Check Parent) ---
        if (data.parentId !== null) {
            this.checkParentPruning(data.parentId, nodeId);
            this.checkNodeStatus(data.parentId); // Might become READY
        }

        // Wurzelknoten evaluiert → besten Zug ermitteln und melden
        if (data.parentId === null && data.children.length > 0) {
            this._notifyBestMove(data, value);
        }

        this._debugAB('evaluateNode:done', {
            nodeId,
            value,
            alpha: data.alpha,
            beta: data.beta,
            parentId: data.parentId,
        });

        if (typeof this.onActiveNodeChanged === 'function') {
            this.onActiveNodeChanged(nodeId, data);
        }

        this._notifyStatsChanged();
        
        this.flushCommands();
    }

    /**
     * Markiert einen gesamten Teilbaum rekursiv als PRUNED.
     * Bereits EVALUATED Knoten behalten ihren Status (Priorität: manuell evaluiert).
     * @param {number} nodeId
     * @param {number|null} parentId
     * @returns {number} Anzahl neu geprunter Knoten
     */
    _pruneSubtree(nodeId, parentId) {
        const node = this.treeStructure.get(nodeId);
        if (!node) return 0;

        if (node.status === 'EVALUATED') {
            return 0;
        }

        let prunedCount = 0;
        if (node.status !== 'PRUNED') {
            // Wert beibehalten für Anzeige im Renderer (nicht auf null setzen)
            NodeStatusManager.setNodeStatus(nodeId, 'PRUNED', [], this.treeStructure, this.commands);
            prunedCount += 1;

            const flags = typeof MINIMAX_VIZ_CONSTANTS !== 'undefined' ? MINIMAX_VIZ_CONSTANTS.FLAGS || {} : {};
            if (flags.ENABLE_PRUNING_HIGHLIGHT !== false && parentId !== null) {
                this.commands.push({
                    action: 'HIGHLIGHT_EDGE',
                    from: parentId,
                    to: nodeId,
                    color: this.constants.edgePrunedColor,
                    width: this.constants.edgeWidth,
                });
            }
        }

        node.children.forEach((childId) => {
            prunedCount += this._pruneSubtree(childId, nodeId);
        });

        return prunedCount;
    }

    /**
     * Checks if the parent can prune its remaining children based on the new value.
     * @param {number} parentId
     * @param {number} [evaluatedChildId=null] - Kind das gerade evaluiert wurde (für Edge-Highlighting)
     */
    checkParentPruning(parentId, evaluatedChildId = null) {
        const parent = this.treeStructure.get(parentId);
        if (!parent) return;
        if (parent.status === 'EVALUATED') return; // Already done

        this._recomputeNodeBounds(parentId, evaluatedChildId);

        const currentAlpha = parent.alpha;
        const currentBeta = parent.beta;
        
        // CHECK CUTOFF
        if (currentAlpha >= currentBeta) {
            this._debugAB('prune-triggered', {
                parentId,
                alpha: currentAlpha,
                beta: currentBeta,
            });

            let newlyPruned = 0;
            // Mark all remaining siblings recursively as PRUNED
            parent.children.forEach(childId => {
                const sib = this.treeStructure.get(childId);
                if (!sib || sib.status === 'EVALUATED' || sib.status === 'PRUNED') return;
                newlyPruned += this._estimateNewlyPrunedCount(childId);
                this._pruneSubtree(childId, parentId);
            });

            if (newlyPruned > 0) {
                this.stats.nodesPruned += newlyPruned;
                this._notifyStatsChanged();
            }

            this._syncStrictOrderingForParent(parentId);
        } else {
            this._debugAB('no-prune', {
                parentId,
                alpha: currentAlpha,
                beta: currentBeta,
                reason: 'alpha < beta',
            });
        }
    }

    // handleNodeClick geerbt von BaseTreeAdapter
}