ai/agents/minimax-agent.js

/* --- FILE: js/ai/agents/minimax-agent.js --- */

/**
 * @fileoverview
 * Agent für Minimax-Algorithmus mit Alpha-Beta Pruning.
 * Berechnet optimale Züge durch baumartige Spielzustandssimulation.
 */

/**
 * Agent, der Minimax nutzt.
 * @class MinimaxAgent
 * @extends Agent
 */
class MinimaxAgent extends Agent {
    /**
     * Erstellt einen neuen Minimax-Agenten.
     * @param {Object} config - Konfigurationsobjekt.
     * @param {string} [config.name="Minimax"] - Name des Agenten.
     * @param {number} [config.maxDepth=3] - Suchtiefe.
     * @param {boolean} [config.useAlphaBeta=true] - Ob Alpha-Beta genutzt werden soll.
     * @param {function} [config.heuristicFn] - Bewertungsfunktion. Falls null, wird winLoss genutzt.
     */
    constructor(config = {}) {
        super(config.name || "Minimax KI");
        
        /**
         * Maximale Suchtiefe.
         * @type {number}
         */
        this.maxDepth = config.maxDepth || DEFAULT_MAX_DEPTH;

        /**
         * Flag für Alpha-Beta Pruning.
         * @type {boolean}
         */
        this.useAlphaBeta = config.useAlphaBeta !== false;
        
        /**
         * Die Bewertungsfunktion.
         * @type {function(GameState, number): number}
         */
        if (config.heuristicFn) {
            this.heuristicFn = config.heuristicFn;
        } else if (typeof HeuristicRegistry !== 'undefined' && HeuristicRegistry.has('generic', 'winLoss')) {
            this.heuristicFn = HeuristicRegistry.get('generic', 'winLoss').evaluate.bind(
                HeuristicRegistry.get('generic', 'winLoss')
            );
        } else {
            this.heuristicFn = (gameState, player) => {
                if (gameState.winner === player) return 1000;
                if (gameState.winner !== NONE && gameState.winner !== DRAW) return -1000;
                return 0;
            };
        }
        
        /**
         * Die verwendete Minimax-Engine.
         * @type {MinimaxEngine}
         */
        this.engine = new MinimaxEngine({
            heuristicFn: this.heuristicFn,
            maxDepth: this.maxDepth,
            useAlphaBeta: this.useAlphaBeta,
            captureTrace: false // Im Spielbetrieb brauchen wir kein Trace
        });
    }

    /**
     * Berechnet den besten Zug.
     * @param {GameState} gameState - Der aktuelle Spielzustand.
     * @returns {Object|null} Der berechnete Zug mit Begründung.
     */
    getAction(gameState) {
        // 🔴 NEURALGISCH: Agent-Aufruf (Phase 3)
        DebugConfig.log(DEBUG_DOMAINS.AI_AGENTS, 'debug',
            'MinimaxAgent.getAction() called', {
                validMoves: gameState.getAllValidMoves ? gameState.getAllValidMoves().length : 0,
                maxDepth: this.maxDepth,
                useAlphaBeta: this.useAlphaBeta,
                heuristicFn: this.heuristicFn.name || 'unknown'
            });
        
        // Safety: Wenn Spiel schon vorbei, null
        if (gameState.winner !== NONE) {
            DebugConfig.log(DEBUG_DOMAINS.AI_AGENTS, 'warn',
                'MinimaxAgent: Game already over', { winner: gameState.winner });
            return null;
        }
        if (gameState.getAllValidMoves().length === 0) {
            DebugConfig.log(DEBUG_DOMAINS.AI_AGENTS, 'error',
                'MinimaxAgent: No valid moves available', { boardState: 'blocked' });
            return null;
        }

        // Engine starten
        const result = this.engine.findBestMove(gameState);
        
        if (result.move === null) {
            // Fallback (sollte bei korrekter Engine nicht passieren, außer Spiel ist voll)
            DebugConfig.log(DEBUG_DOMAINS.AI_AGENTS, 'error',
                'MinimaxAgent: Engine returned no move', { 
                    nodesVisited: result.nodesVisited,
                    boardState: 'blocked_or_error'
                });
            return null;
        }

        // 🔴 NEURALGISCH: Best Move Result (Phase 3)
        DebugConfig.log(DEBUG_DOMAINS.AI_AGENTS, 'debug',
            'MinimaxAgent: Best move found', {
                move: result.move,
                score: result.score,
                nodesVisited: result.nodesVisited,
                depthSearched: this.maxDepth
            });

        return {
            move: result.move,
            reason: `Score: ${result.score} (Tiefe ${this.maxDepth})`
        };
    }
}