ai/heuristics/evaluators/ttt-heuristic.js

/**
 * @fileoverview TTT-Heuristik-Evaluator für regular, 3d und ultimate.
 * Unterstützt drei Bewertungsprofile pro Variante:
 *   v1_baseline  — Lineare Zählung (Sieg/Verlust + Linien)
 *   v2_positional — Positionswerte (Zentrum, Ecken, Raumkontrolle)
 *   v3_aggressive — Druckmaximierung (Forks, Drohungen, Mobilitätseinschränkung)
 * @author Alexander Wolf
 */

/**
 * Heuristik-Implementierung für alle Tic-Tac-Toe-Varianten.
 *
 * ## Mathematische Intuition der evaluate()-Methode
 *
 * Die Bewertungsfunktion berechnet einen Gesamtscore S für einen Spielzustand:
 *
 *   S = S_terminal + S_lines + S_position + S_pressure
 *
 * **S_terminal**: Sofort-Bewertung bei Spielende → ±win/loss/draw.
 *
 * **S_lines**: Für jede Gewinnlinie L:
 *   - Eigene 2-in-Reihe (+ 1 leer) → +twoInLine
 *   - Gegner 2-in-Reihe (+ 1 leer) → −twoInLine
 *   - Eigene 1-in-Reihe (+ 2 leer) → +oneInLine
 *   - Gegner 1-in-Reihe (+ 2 leer) → −oneInLine
 *
 * **S_position** (v2/v3): Summe über alle besetzten Felder:
 *   - Zentrum(4) → ±centerBonus
 *   - Ecken(0,2,6,8) → ±cornerBonus
 *   - Kanten(1,3,5,7) → ±edgeBonus
 *
 * **S_pressure** (v3): Zusätzlich:
 *   - Fork-Erkennung (≥2 Drohungen nach Zug) → ±forkBonus
 *   - Gegner-Drohungen zählen → −pressureWeight × Anzahl
 */
class TTTHeuristic extends BaseHeuristic {
    /**
     * @param {Object} config - Konfiguration inkl. variant und optionalem profile.
     */
    constructor(config) {
        const variant = (config && config.variant) ? config.variant : 'regular';
        const profile = (config && config.profile) ? config.profile : 'v1_baseline';
        const configFromFactory = (typeof getTTTHeuristicsConfig === 'function')
            ? getTTTHeuristicsConfig(variant, profile)
            : null;
        const mergedConfig = {
            ...(configFromFactory || {}),
            ...(config || {}),
            game: 'ttt',
            variant
        };
        const validated = (typeof validateHeuristicConfig === 'function')
            ? validateHeuristicConfig(mergedConfig)
            : mergedConfig;
        super(validated);
        /** @type {string} Profilname (v1_baseline, v2_positional, v3_aggressive) */
        this.profile = profile;
    }

    /** @returns {Object} Default-Gewichte (Baseline-Profil) */
    getDefaultWeights() {
        const defaultsByVariant = {
            regular: {
                win: 1000, loss: -1000, draw: 0,
                twoInLine: 10, oneInLine: 1
            },
            '3d': {
                win: 10000, loss: -10000, draw: 0,
                twoInLine: 10, oneInLine: 1, centerBonus: 20
            },
            ultimate: {
                win: 100000, loss: -100000, draw: 0,
                macroWeight: 50, boardWonBonus: 20,
                twoInLine: 10, oneInLine: 1
            }
        };
        return defaultsByVariant[this.variant] || defaultsByVariant.regular;
    }

    /**
     * Hauptbewertung — delegiert an varianten-spezifische Methode.
     * @param {GameState} gameState - Aktueller Spielzustand.
     * @param {number} player - Perspektive (PLAYER1 oder PLAYER2).
     * @returns {number} Numerischer Score. Positiv = gut für player.
     */
    evaluate(gameState, player) {
        const terminal = this.checkTerminal(gameState, player);
        if (terminal !== null) return terminal;

        if (this.variant === '3d') return this._evaluate3D(gameState, player);
        if (this.variant === 'ultimate') return this._evaluateUltimate(gameState, player);
        return this._evaluateRegular(gameState, player);
    }

    /**
     * Regular 3×3 Bewertung.
     * Score = Σ(linienScoring) + positionScoring + pressureScoring
     * @private
     */
    _evaluateRegular(game, player) {
        const opponent = player === PLAYER1 ? PLAYER2 : PLAYER1;
        const w = this.weights;
        let score = 0;

        const lines = (typeof GAME_CONSTANTS !== 'undefined' && GAME_CONSTANTS.TTT_WIN_CONDITIONS)
            ? GAME_CONSTANTS.TTT_WIN_CONDITIONS
            : [[0,1,2],[3,4,5],[6,7,8],[0,3,6],[1,4,7],[2,5,8],[0,4,8],[2,4,6]];

        // S_lines: Linienbewertung
        for (const line of lines) {
            let myCount = 0, oppCount = 0, emptyCount = 0;
            for (const idx of line) {
                const value = game.grid[idx];
                if (value === player) myCount++;
                else if (value === opponent) oppCount++;
                else emptyCount++;
            }
            if (myCount === 2 && emptyCount === 1) score += w.twoInLine;
            if (oppCount === 2 && emptyCount === 1) score -= w.twoInLine;
            if (myCount === 1 && emptyCount === 2) score += w.oneInLine;
            if (oppCount === 1 && emptyCount === 2) score -= w.oneInLine;
        }

        // S_position: Positionsbewertung (v2_positional, v3_aggressive)
        if (w.centerBonus || w.cornerBonus || w.edgeBonus) {
            score += this._positionScore(game.grid, player, opponent);
        }

        // S_pressure: Fork- und Druckbewertung (v3_aggressive)
        if (w.forkBonus || w.pressureWeight) {
            score += this._pressureScore(game, player, opponent, lines);
        }

        return score;
    }

    /**
     * Bewertet Feldpositionen.
     * Zentrum hat höchsten strategischen Wert, gefolgt von Ecken.
     * @private
     * @param {number[]} grid - Board-Array.
     * @param {number} player
     * @param {number} opponent
     * @returns {number} Positionsscore.
     */
    _positionScore(grid, player, opponent) {
        const w = this.weights;
        let score = 0;
        const CENTER = 4;
        const CORNERS = [0, 2, 6, 8];
        const EDGES = [1, 3, 5, 7];

        if (w.centerBonus) {
            if (grid[CENTER] === player) score += w.centerBonus;
            else if (grid[CENTER] === opponent) score -= w.centerBonus;
        }

        if (w.cornerBonus) {
            for (const c of CORNERS) {
                if (grid[c] === player) score += w.cornerBonus;
                else if (grid[c] === opponent) score -= w.cornerBonus;
            }
        }

        if (w.edgeBonus) {
            for (const e of EDGES) {
                if (grid[e] === player) score += w.edgeBonus;
                else if (grid[e] === opponent) score -= w.edgeBonus;
            }
        }

        return score;
    }

    /**
     * Bewertet Fork-Potenzial und gegnerischen Druck.
     * Fork = Züge die ≥2 Gewinndrohungen gleichzeitig erzeugen.
     * @private
     * @param {GameState} game
     * @param {number} player
     * @param {number} opponent
     * @param {number[][]} lines - Gewinnlinien.
     * @returns {number} Druck-Score.
     */
    _pressureScore(game, player, opponent, lines) {
        const w = this.weights;
        let score = 0;

        // Zähle eigene und gegnerische Drohungen (2-in-Linie mit 1 leer)
        let myThreats = 0;
        let oppThreats = 0;

        for (const line of lines) {
            let myCount = 0, oppCount = 0, emptyCount = 0;
            for (const idx of line) {
                const value = game.grid[idx];
                if (value === player) myCount++;
                else if (value === opponent) oppCount++;
                else emptyCount++;
            }
            if (myCount === 2 && emptyCount === 1) myThreats++;
            if (oppCount === 2 && emptyCount === 1) oppThreats++;
        }

        // Fork-Bonus: ≥2 gleichzeitige Drohungen
        if (w.forkBonus && myThreats >= 2) score += w.forkBonus;
        if (w.opponentForkPenalty && oppThreats >= 2) score += w.opponentForkPenalty;

        // Drohungsdifferenz als Druckmaß
        if (w.pressureWeight) {
            score += (myThreats - oppThreats) * w.pressureWeight;
        }

        return score;
    }

    /**
     * 3D 3×3×3 Bewertung.
     * Score = Zentrumskontrolle + Raumlinienbewertung + Positionsboni
     * @private
     */
    _evaluate3D(game, player) {
        const opponent = player === PLAYER1 ? PLAYER2 : PLAYER1;
        const w = this.weights;
        let score = 0;
        const CENTER_3D = 13;

        // Zentrumsbonus (Index 13 = Mitte des 3×3×3 Würfels)
        if (w.centerBonus) {
            if (game.grid[CENTER_3D] === player) score += w.centerBonus;
            else if (game.grid[CENTER_3D] === opponent) score -= w.centerBonus;
        }

        // Raumlinien-Analyse
        const lines = this._getLines3D();
        let myThreats = 0, oppThreats = 0;

        for (const line of lines) {
            let myCount = 0, oppCount = 0, emptyCount = 0;
            for (const idx of line) {
                const value = game.grid[idx];
                if (value === player) myCount++;
                else if (value === opponent) oppCount++;
                else emptyCount++;
            }
            if (myCount === 2 && emptyCount === 1) {
                score += w.twoInLine;
                myThreats++;
            }
            if (oppCount === 2 && emptyCount === 1) {
                score -= w.twoInLine;
                oppThreats++;
            }
            if (myCount === 1 && emptyCount === 2) score += w.oneInLine;
            if (oppCount === 1 && emptyCount === 2) score -= w.oneInLine;
        }

        // v2: Raumkontrolle — Abstand zur Mitte bewerten
        if (w.spatialControl) {
            for (let idx = 0; idx < 27; idx++) {
                if (game.grid[idx] === NONE) continue;
                const x = idx % 3, y = Math.floor(idx / 3) % 3, z = Math.floor(idx / 9);
                const dist = Math.abs(x - 1) + Math.abs(y - 1) + Math.abs(z - 1);
                const bonus = (3 - dist) * w.spatialControl;
                if (game.grid[idx] === player) score += bonus;
                else score -= bonus;
            }
        }

        // v2: Eckenbonus für 3D-Ecken (8 Stück)
        if (w.cornerBonus) {
            const corners3D = [0, 2, 6, 8, 18, 20, 24, 26];
            for (const c of corners3D) {
                if (game.grid[c] === player) score += w.cornerBonus;
                else if (game.grid[c] === opponent) score -= w.cornerBonus;
            }
        }

        // v3: Fork/Druck
        if (w.forkBonus && myThreats >= 2) score += w.forkBonus;
        if (w.opponentForkPenalty && oppThreats >= 2) score += w.opponentForkPenalty;
        if (w.pressureWeight) score += (myThreats - oppThreats) * w.pressureWeight;

        return score;
    }

    /**
     * Ultimate TTT Bewertung.
     * Score = Makro-Linien × macroWeight + Board-Gewinn-Boni + lokale Bewertung
     * @private
     */
    _evaluateUltimate(game, player) {
        const opponent = player === PLAYER1 ? PLAYER2 : PLAYER1;
        const w = this.weights;
        let score = 0;
        const macro = Array.isArray(game.macroBoard) ? game.macroBoard : Array(9).fill(NONE);
        const lines = [[0,1,2],[3,4,5],[6,7,8],[0,3,6],[1,4,7],[2,5,8],[0,4,8],[2,4,6]];

        // Makro-Board-Linienbewertung
        let myMacroThreats = 0, oppMacroThreats = 0;
        for (const line of lines) {
            let myCount = 0, oppCount = 0, neutralCount = 0;
            for (const idx of line) {
                if (macro[idx] === player) myCount++;
                else if (macro[idx] === opponent) oppCount++;
                else if (macro[idx] === NONE || macro[idx] === 0) neutralCount++;
            }
            if (myCount === 2 && oppCount === 0) {
                score += w.twoInLine * w.macroWeight;
                myMacroThreats++;
            }
            if (oppCount === 2 && myCount === 0) {
                score -= w.twoInLine * w.macroWeight;
                oppMacroThreats++;
            }
            if (myCount === 1 && oppCount === 0 && neutralCount === 2) {
                score += w.oneInLine * w.macroWeight;
            }
            if (oppCount === 1 && myCount === 0 && neutralCount === 2) {
                score -= w.oneInLine * w.macroWeight;
            }
        }

        // Board-Gewinn-Boni
        for (let index = 0; index < 9; index++) {
            if (macro[index] === player) score += w.boardWonBonus;
            else if (macro[index] === opponent) score -= w.boardWonBonus;
        }

        // v2: Makro-Zentrumskontrolle (Board 4 = Mitte im Makro)
        if (w.centerBonus) {
            if (macro[4] === player) score += w.centerBonus;
            else if (macro[4] === opponent) score -= w.centerBonus;
        }

        // v3: Makro-Druck
        if (w.pressureWeight) {
            score += (myMacroThreats - oppMacroThreats) * w.pressureWeight;
        }
        if (w.blockUrgency && oppMacroThreats >= 2) {
            score -= w.blockUrgency;
        }

        return score;
    }

    /**
     * Generiert und cacht alle Gewinnlinien im 3×3×3 Raum.
     * 49 Linien insgesamt: 27 Achsen + 18 Flächendiagonalen + 4 Raumdiagonalen.
     * @private
     * @returns {number[][]}
     */
    _getLines3D() {
        if (TTTHeuristic._lines3d) return TTTHeuristic._lines3d;

        const SIZE = 3;
        const directions = [
            [1,0,0], [0,1,0], [0,0,1],
            [1,1,0], [1,-1,0], [1,0,1], [1,0,-1], [0,1,1], [0,-1,1],
            [1,1,1], [1,1,-1], [1,-1,1], [1,-1,-1]
        ];

        const lines = [];
        const seen = new Set();
        const isValid = (x, y, z) => x >= 0 && x < SIZE && y >= 0 && y < SIZE && z >= 0 && z < SIZE;
        const getIdx = (x, y, z) => x + y * SIZE + z * SIZE * SIZE;

        for (let z = 0; z < SIZE; z++) {
            for (let y = 0; y < SIZE; y++) {
                for (let x = 0; x < SIZE; x++) {
                    for (const [dx, dy, dz] of directions) {
                        const line = [];
                        for (let step = 0; step < SIZE; step++) {
                            const px = x + step * dx, py = y + step * dy, pz = z + step * dz;
                            if (!isValid(px, py, pz)) { line.length = 0; break; }
                            line.push(getIdx(px, py, pz));
                        }
                        if (line.length === SIZE) {
                            const key = [...line].sort((a, b) => a - b).join(',');
                            if (!seen.has(key)) { seen.add(key); lines.push(line); }
                        }
                    }
                }
            }
        }

        TTTHeuristic._lines3d = lines;
        return lines;
    }
}

// ═══════════════════════════════════════════════════════════════
//  Registrierung: 3 Profile × 3 Varianten = 9 Instanzen
// ═══════════════════════════════════════════════════════════════
HeuristicRegistry.registerConstructor('ttt', TTTHeuristic);

const TTT_VARIANTS = ['regular', '3d', 'ultimate'];
const TTT_PROFILES = ['v1_baseline', 'v2_positional', 'v3_aggressive'];

for (const variant of TTT_VARIANTS) {
    for (const profile of TTT_PROFILES) {
        const cfg = (typeof getTTTHeuristicsConfig === 'function')
            ? getTTTHeuristicsConfig(variant, profile)
            : { game: 'ttt', variant, profile };
        const instance = new TTTHeuristic({
            ...cfg,
            variant,
            profile,
            name: cfg.name || `TTT ${variant} (${profile})`
        });
        // Für Profile mit anderem Suffix: registriere unter game:variant:profile
        if (profile === 'v1_baseline') {
            // Default-Instanz unter dem Hauptschlüssel ttt:variant
            HeuristicRegistry.register(instance);
        }
        // Alle Profile unter erweitertem Schlüssel (überschreibe id)
        instance.id = `ttt:${variant}:${profile}`;
        HeuristicRegistry.register(instance);
    }
}