viz/adapters/tree-adapters/minimax-tree-adapter.js

/**
 * @fileoverview Minimax Tree Adapter - Visualisierung des Minimax Algorithmus
 * 
 * Extends BaseTreeAdapter mit Minimax-spezifischen Features:
 * - Value-basierte Node-Färbung (Min/Max-Nodes)
 * - Best-Move Tracking und Visualisierung
 * - Performance-Statistiken (visited nodes, iterations)
 * 
 * @class MinimaxTreeAdapter
 * @author Alexander Wolf  
 * @version 2.3
 */
class MinimaxTreeAdapter extends BaseTreeAdapter {
    /**
     * @param {HTMLIFrameElement} iframeElement
     */
    constructor(iframeElement) {
        super(iframeElement);

        // Debug-Konfiguration für diesen Adapter
        this._debugDomain = 'VIZ_TREE_ADAPTER_MINIMAX';
        this._debugPrefix = 'MinimaxTreeAdapter';

        this.constants = this._resolveConstants();
    }

    /**
     * Lädt zentrale Konstanten mit robusten Fallbacks.
     * @returns {Object}
     */
    _resolveConstants() {
        const game = typeof GAME_CONSTANTS !== 'undefined' ? GAME_CONSTANTS : {};
        const viz = typeof MINIMAX_VIZ_CONSTANTS !== 'undefined' ? MINIMAX_VIZ_CONSTANTS : {};
        const flags = viz.FLAGS || {};
        const values = viz.VALUES || {};
        const tree = viz.TREE || {};
        const colors = viz.COLORS || {};

        return {
            winnerNone: game.NONE !== undefined ? game.NONE : 0,
            winnerDraw: game.DRAW !== undefined ? game.DRAW : 3,
            player1: game.PLAYER1 !== undefined ? game.PLAYER1 : 1,
            player2: game.PLAYER2 !== undefined ? game.PLAYER2 : 2,

            valueWin: values.WIN !== undefined ? values.WIN : 1,
            valueLoss: values.LOSS !== undefined ? values.LOSS : -1,
            valueDraw: values.DRAW !== undefined ? values.DRAW : 0,

            rootDepth: tree.ROOT_DEPTH !== undefined ? tree.ROOT_DEPTH : 0,
            edgeWidth: tree.HIGHLIGHT_EDGE_WIDTH !== undefined ? tree.HIGHLIGHT_EDGE_WIDTH : 4,

            enableTreeExpansionMinimax: flags.ENABLE_TREE_EXPANSION_MINIMAX !== false,
            debugMinimaxAdapter: flags.DEBUG_MINIMAX_ADAPTER === true,

            rootPlayer1Color: colors.ROOT_PLAYER_1 || '#3498db',
            rootPlayer2Color: colors.ROOT_PLAYER_2 || '#e74c3c',
            edgePositiveColor: colors.EDGE_POSITIVE || '#2730ae',
            edgeNegativeColor: colors.EDGE_NEGATIVE || '#c0392b',
            edgeNeutralColor: colors.EDGE_NEUTRAL || '#a4ae1c',
            edgePrunedColor: colors.EDGE_PRUNED || '#7f8c8d',
            edgeAlphaPropColor: colors.EDGE_ALPHA_PROP || '#e74c3c',
            edgeBetaPropColor: colors.EDGE_BETA_PROP || '#3498db',
            edgePropWidth: tree.PROP_EDGE_WIDTH !== undefined ? tree.PROP_EDGE_WIDTH : 2.5,
        };
    }

    /**
     * Prüft ob ein Blattknoten terminal ist (spielbasiert).
     * Überschreibt BaseTreeAdapter._isLeafTerminal für Game-State-Prüfung.
     *
     * @param {number} nodeId
     * @param {Object} data
     * @returns {boolean}
     * @protected
     */
    _isLeafTerminal(nodeId, data) {
        const state = this.nodeStates.get(nodeId);
        return this._isTerminalState(state);
    }

    /**
     * Liefert Farbe und Breite für Best-Edge-Highlighting (wertabhängig).
     * @param {number} bestValue
     * @returns {{color: string, width: number}}
     * @protected
     */
    _getHighlightEdgeStyle(bestValue) {
        return { color: this._getEdgeColorForValue(bestValue), width: this.constants.edgeWidth };
    }

    /**
     * Prüft, ob ein Zustand terminal ist.
     * @param {GameState} state
     * @returns {boolean}
     */
    _isTerminalState(state) {
        if (!state) return false;
        return state.winner !== this.constants.winnerNone || state.getAllValidMoves().length === 0;
    }

    /**
     * Erzeugt ein Standard-Resultat für die UI.
     * @param {number} rootId
     * @returns {Object}
     */
    _buildResult(rootId) {
        const rootData = this.treeStructure.get(rootId);
        return {
            bestMove: null,
            bestValue: rootData ? rootData.value : null,
            nodesVisited: this.stats.nodesVisited,
            nodesPruned: this.stats.nodesPruned,
            evaluatedNodes: this.stats.evaluatedNodes,
        };
    }

    onTreeReady() {
        super.onTreeReady();
        // Enable tree expansion for Minimax
        this.sendCommand({
            action: 'UPDATE_CONFIG',
            config: { enableTreeExpansion: this.constants.enableTreeExpansionMinimax }
        });
    }

    /**
     * Startet die Visualisierung (Aufbauphase).
     */
    async visualizeSearch(gameState, config) {
        DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_MINIMAX, "debug",
            '[MinimaxAdapter] visualizeSearch START',
            { algorithm: config?.algorithm, maxDepth: config?.maxDepth });
        
        this.currentGameState = gameState;
        this.currentConfig = config;
        this.stats.nodesVisited = 0;
        this.stats.nodesPruned = 0;
        this.stats.evaluatedNodes = 0;
        this._notifyStatsChanged();
        
        // Speichere den Root-Spieler für relative Bewertung
        this.rootPlayer = gameState.currentPlayer;
        this._debugLog('visualizeSearch:start', {
            algorithm: config && config.algorithm,
            rootPlayer: this.rootPlayer,
            maxDepth: config && config.maxDepth,
        });
        
        this.reset();
        
        // Root Node erstellen
        const rootId = this.createNode(gameState, null, {
            depth: this.constants.rootDepth,
            isMaximizing: true,
            value: null
        });
        
        this.nodeStates.set(rootId, gameState);
        this.treeStructure.set(rootId, { 
            parentId: null, 
            children: [], 
            status: 'WAIT',
            value: null, 
            depth: this.constants.rootDepth,
            isMaximizing: true 
        });
        
        // Erste Ebene expandieren
        this.expandNodeChildren(rootId, gameState);
        
        // Update Root Status
        this.checkNodeStatus(rootId);
        
        DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_MINIMAX, "debug",
            '[MinimaxAdapter] visualizeSearch: About to flushCommands',
            { nodeCount: this.nodeIdCounter, commandQueueSize: this.commands.length });
        
        this.flushCommands();
        
        DebugConfig.log(DEBUG_DOMAINS.VIZ_TREE_ADAPTER_MINIMAX, "debug",
            '[MinimaxAdapter] visualizeSearch COMPLETE',
            { rootId, totalNodes: this.nodeIdCounter, initialStats: this.stats });
        
        this._debugLog('visualizeSearch:ready', { rootId });
        return this._buildResult(rootId);
    }

    getInitialConfig() {
        const isRootPlayer2 = this.rootPlayer === this.constants.player2;
        return { 
            showLevelIndicators: true,
            levelIndicatorType: 'minimax',
            rootPlayerColor: isRootPlayer2 ? this.constants.rootPlayer2Color : this.constants.rootPlayer1Color,
            opponentColor: isRootPlayer2 ? this.constants.rootPlayer1Color : this.constants.rootPlayer2Color,
            enableTreeExpansion: this.constants.enableTreeExpansionMinimax
        };
    }

    /**
     * Helper to create child metadata. Can be overridden.
     */
    _createChildMetadata(nodeId, childState, move, currentData) {
        return {
            depth: currentData.depth + 1,
            isMaximizing: !currentData.isMaximizing,
            move: move,
            moveOrder: currentData.children.length,
            value: null
        };
    }
    
    /**
     * Helper to create additional tree structure data. Can be overridden.
     */
    _createChildStructure(childId, nodeId, childState, currentData) {
        return {
            parentId: nodeId,
            children: [],
            status: 'WAIT',
            value: null,
            depth: currentData.depth + 1,
            isMaximizing: !currentData.isMaximizing,
            isTerminal: this._isTerminalState(childState),
            moveOrder: currentData.children.length,
        };
    }

    /**
     * Optionales Move-Ordering für Visualisierung.
     * Kann in Subklassen überschrieben werden (z. B. Alpha-Beta).
     * @param {Array} validMoves
     * @param {GameState} state
     * @param {Object} currentData
     * @returns {Array}
     */
    _orderMoves(validMoves, state, currentData) {
        return validMoves;
    }

    expandNodeChildren(nodeId, state) {
        const currentData = this.treeStructure.get(nodeId);
        if (!currentData) return;

        if (currentData.status === 'EVALUATED') {
            this._debugLog('expandNodeChildren:skip-non-expandable', {
                nodeId,
                status: currentData.status,
            });
            return;
        }

        if (this._isTerminalState(state)) return;
        
        const validMoves = state.getAllValidMoves();
        const orderedMoves = this._orderMoves(validMoves, state, currentData);
        this._debugLog('expandNodeChildren', { nodeId, validMovesCount: validMoves.length });
        
        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);
            
            this.nodeStates.set(childId, childState);
            
            // Update Structure
            const childStruct = this._createChildStructure(childId, nodeId, childState, currentData);
            childStruct.move = move;
            this.treeStructure.set(childId, childStruct);
            
            // Add to parent
            if (currentData) currentData.children.push(childId);
            
            // Mark expandable if not terminal
            if (!childStruct.isTerminal) {
                this.commands.push({ action: 'MARK_EXPANDABLE', id: childId });
            }
            
            // Check status immediately
            this.checkNodeStatus(childId);
        }
        
        this.checkNodeStatus(nodeId);
    }

    // checkNodeStatus geerbt von BaseTreeAdapter (mit _isLeafTerminal Override)
    // handleNodeClick geerbt von BaseTreeAdapter

    /**
     * Bewertet einen terminalen Knoten relativ zum Root-Spieler.
     * @param {GameState} state
     * @returns {{value:number,labelText:string,statusesToAdd:string[]}}
     */
    _evaluateTerminalState(state) {
        const statusesToAdd = ['EVALUATED'];
        const opponent = this.rootPlayer === this.constants.player1 ? this.constants.player2 : this.constants.player1;

        if (state.winner === this.rootPlayer) {
            statusesToAdd.push(this.rootPlayer === this.constants.player1 ? 'WIN_BLUE' : 'WIN_RED');
            return {
                value: this.constants.valueWin,
                labelText: `Win = +${this.constants.valueWin}`,
                statusesToAdd,
            };
        }

        if (state.winner === opponent) {
            statusesToAdd.push(opponent === this.constants.player1 ? 'WIN_BLUE' : 'WIN_RED');
            return {
                value: this.constants.valueLoss,
                labelText: `Loss = ${this.constants.valueLoss}`,
                statusesToAdd,
            };
        }

        statusesToAdd.push('DRAW');
        return {
            value: this.constants.valueDraw,
            labelText: `Draw = ${this.constants.valueDraw}`,
            statusesToAdd,
        };
    }

    /**
     * Liefert zusätzliche Visualisierungs-Status anhand des Werts.
     * @param {number} value
     * @returns {string[]}
     */
    _getValueStatuses(value) {
        if (value === this.constants.valueWin) {
            return [this.rootPlayer === this.constants.player1 ? 'WIN_BLUE' : 'WIN_RED'];
        }
        if (value === this.constants.valueLoss) {
            return [this.rootPlayer === this.constants.player1 ? 'WIN_RED' : 'WIN_BLUE'];
        }
        return ['DRAW'];
    }

    /**
     * Liefert Kantenfarbe entsprechend Gewinnerfarbe (statt Vorzeichenfarbe).
     * @param {number} value
     * @returns {string}
     */
    _getEdgeColorForValue(value) {
        if (value === this.constants.valueWin) {
            return this.rootPlayer === this.constants.player1
                ? this.constants.rootPlayer1Color
                : this.constants.rootPlayer2Color;
        }

        if (value === this.constants.valueLoss) {
            return this.rootPlayer === this.constants.player1
                ? this.constants.rootPlayer2Color
                : this.constants.rootPlayer1Color;
        }

        return this.constants.edgeNeutralColor;
    }

    // _highlightBestEdges geerbt von BaseTreeAdapter (mit _getHighlightEdgeStyle Override)

    /**
     * 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._debugLog('evaluateNode:missing-data', { nodeId });
            return;
        }
        
        this.commands = [];
        this.stats.nodesVisited += 1;
        this.stats.evaluatedNodes += 1;
        this._notifyStatsChanged();
        
        let value = 0;
        let labelText = "";
        let statusesToAdd = ['EVALUATED'];
        
        // Evaluate node and propagate values upward
        
        // CASE 1: Leaf Node (Terminal)
        if (data.children.length === 0) {
            const leafResult = this._evaluateTerminalState(state);
            value = leafResult.value;
            labelText = leafResult.labelText;
            statusesToAdd = leafResult.statusesToAdd;
        } 
        // CASE 2: Inner Node
        else {
            const childValues = data.children.map(cid => {
                const child = this.treeStructure.get(cid);
                return child && child.value !== undefined && child.value !== null
                    ? child.value
                    : this.constants.valueDraw;
            });
            
            if (data.isMaximizing) {
                value = Math.max(...childValues);
                const sign = value > 0 ? '+' : value < 0 ? '' : '';
                labelText = `Max = ${sign}${value}`;
            } else {
                value = Math.min(...childValues);
                const sign = value > 0 ? '+' : value < 0 ? '' : '';
                labelText = `Min = ${sign}${value}`;
            }
            
            this._highlightBestEdges(nodeId, value);
            statusesToAdd = ['EVALUATED', ...this._getValueStatuses(value)];
        }
        
        // Save value
        data.value = value;
        
        // 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,
                }
            }
        });
        
        // "Wenn ein neuer Knoten bewertet wurde, prüfe den Status des Elternknotens"
        if (data.parentId !== null) {
            this.checkNodeStatus(data.parentId);
        }

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

        this._debugLog('evaluateNode:done', {
            nodeId,
            value,
            status: data.status,
            parentId: data.parentId,
        });

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

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

    /**
     * Ermittelt den besten Zug des Wurzelknotens und feuert onBestMoveFound.
     * Wird aufgerufen, wenn der Root-Knoten (parentId === null) evaluiert wird.
     *
     * @param {Object} rootData - Daten des Wurzelknotens aus treeStructure
     * @param {number} bestValue - Bester Wert (MAX oder MIN)
     * @private
     */
    _notifyBestMove(rootData, bestValue) {
        if (typeof this.onBestMoveFound !== 'function') return;

        const bestMoves = [];
        rootData.children.forEach(childId => {
            const child = this.treeStructure.get(childId);
            if (!child || child.value !== bestValue) return;
            // move aus der Node-Metadata lesen (wurde in createNode gespeichert)
            if (child.move !== undefined) {
                bestMoves.push(child.move);
            }
        });

        if (bestMoves.length > 0) {
            // Bei mehreren gleichwertigen Zügen zufällig einen auswählen
            const selectedMove = bestMoves[Math.floor(Math.random() * bestMoves.length)];
            this.onBestMoveFound([selectedMove], bestValue);
        }
    }
}