ai/heuristics/evaluators/connect4-heuristic.js

/**
 * @fileoverview Connect4-Heuristik-Evaluator für regular und 3d.
 * Unterstützt drei Bewertungsprofile pro Variante:
 *   v1_baseline  — Standard-Fensterbewertung + Center-Bias
 *   v2_positional — Starke Zentrumskontrolle, Höhenstrafe, Verbindungsbonus
 *   v3_aggressive — Drohungsmaximierung, Setup-Scoring, offensiver Bias
 * @author Alexander Wolf
 */

/**
 * Heuristik-Implementierung für Connect4-Varianten.
 *
 * ## Mathematische Intuition der evaluate()-Methode
 *
 * Die Bewertungsfunktion berechnet einen Gesamtscore S:
 *
 *   S = S_terminal + S_center + S_windows + S_extras
 *
 * **S_terminal**: Sofort-Bewertung bei Spielende → ±win/loss/draw.
 *
 * **S_center**: Für jede besetzte Zelle:
 *   dist = |col − centerCol|
 *   bias = (centerCol − dist + 1) × centerWeight
 *   → Zentrale Spalten erhalten höheren Positionswert.
 *
 * **S_windows**: Gleitfenster der Länge 4 in allen Richtungen:
 *   - 4 eigene → +fourInRow (faktisch Sieg, aber als Safety-Net)
 *   - 3 eigene + 1 leer → +threeInLine
 *   - 2 eigene + 2 leer → +twoInLine
 *   - 3 Gegner + 1 leer → +opponentThreeInLine (negativ!)
 *   - 2 Gegner + 2 leer → +opponentTwoInLine (negativ!)
 *
 * **S_extras** (v2/v3):
 *   - connectivityBonus: Belohnung für angrenzende eigene Steine
 *   - heightPenalty: Bestrafung für zu hohe Steine (instabile Positionen)
 *   - pressureWeight: Drohungsdifferenz × Gewicht
 *   - setupBonus: Bonus für 2er-Fenster mit 2 Leerstellen (Vorbereitung)
 */
class Connect4Heuristic 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 getConnect4HeuristicsConfig === 'function')
            ? getConnect4HeuristicsConfig(variant, profile)
            : null;
        const mergedConfig = {
            ...(configFromFactory || {}),
            ...(config || {}),
            game: 'connect4',
            variant
        };
        const validated = (typeof validateHeuristicConfig === 'function')
            ? validateHeuristicConfig(mergedConfig)
            : mergedConfig;
        super(validated);
        /** @type {string} Profilname */
        this.profile = profile;
    }

    /** @returns {Object} Default-Gewichte (Baseline-Profil) */
    getDefaultWeights() {
        const defaultsByVariant = {
            regular: {
                win: 100000, loss: -100000, draw: 0,
                fourInRow: 10000, threeInLine: 100, twoInLine: 10,
                opponentThreeInLine: -90, opponentTwoInLine: -5,
                centerWeight: 3
            },
            '3d': {
                win: 100000, loss: -100000, draw: 0,
                fourInRow: 100000, threeInLine: 1000, twoInLine: 50,
                opponentThreeInLine: -1000, opponentTwoInLine: -50,
                centerWeight: 2
            }
        };
        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);
        return this._evaluateRegular(gameState, player);
    }

    /**
     * Öffentliche Methode für Linienbewertung (kompatibel mit Adapter).
     * @param {GameState} gameState
     * @param {number} player
     * @returns {number}
     */
    evaluateLines(gameState, player) {
        return this._evaluateLines(gameState, player);
    }

    /**
     * Regular Connect4 Bewertung.
     * Score = Center-Bias + Gleitfenster-Analyse + Extras (v2/v3).
     * @private
     */
    _evaluateRegular(game, player) {
        const w = this.weights;
        const opponent = player === PLAYER1 ? PLAYER2 : PLAYER1;
        const centerCol = Math.floor(game.cols / 2);
        let score = 0;

        // S_center: Zentrumsdistanz-Bewertung
        for (let row = 0; row < game.rows; row++) {
            for (let col = 0; col < game.cols; col++) {
                const idx = row * game.cols + col;
                const value = game.grid[idx];
                if (value === NONE) continue;
                const distance = Math.abs(col - centerCol);
                const centerScore = (centerCol - distance + 1) * w.centerWeight;
                if (value === player) score += centerScore;
                else if (value === opponent) score -= centerScore;
            }
        }

        // S_windows: Gleitfenster-Analyse
        score += this._evaluateLines(game, player);

        // S_extras: Verbindungsbonus (v2)
        if (w.connectivityBonus) {
            score += this._connectivityScore(game, player, opponent);
        }

        // S_extras: Höhenstrafe (v2) — hohe Steine in instabilen Positionen
        if (w.heightPenalty) {
            score += this._heightScore(game, player, opponent);
        }

        // S_extras: Setup-Bonus (v3) — Vorbereitung auf 3er/4er
        if (w.setupBonus) {
            score += this._setupScore(game, player, opponent);
        }

        return score;
    }

    /**
     * Gleitfenster-Analyse in allen 4 Richtungen.
     * Bewertet jedes 4er-Fenster nach Inhalt.
     * @private
     */
    _evaluateLines(game, player) {
        let score = 0;
        const w = this.weights;
        const opponent = player === PLAYER1 ? PLAYER2 : PLAYER1;

        const evaluateWindow = (cells) => {
            let myCount = 0, oppCount = 0, emptyCount = 0;
            for (const cell of cells) {
                if (cell === player) myCount++;
                else if (cell === opponent) oppCount++;
                else emptyCount++;
            }
            if (myCount === 4) return w.fourInRow;
            if (myCount === 3 && emptyCount === 1) return w.threeInLine;
            if (myCount === 2 && emptyCount === 2) return w.twoInLine;
            if (oppCount === 3 && emptyCount === 1) return w.opponentThreeInLine;
            if (oppCount === 2 && emptyCount === 2) return w.opponentTwoInLine;
            return 0;
        };

        // Horizontal
        for (let row = 0; row < game.rows; row++) {
            for (let col = 0; col < game.cols - 3; col++) {
                const window = [];
                for (let s = 0; s < 4; s++) window.push(game.grid[row * game.cols + col + s]);
                score += evaluateWindow(window);
            }
        }
        // Vertikal
        for (let col = 0; col < game.cols; col++) {
            for (let row = 0; row < game.rows - 3; row++) {
                const window = [];
                for (let s = 0; s < 4; s++) window.push(game.grid[(row + s) * game.cols + col]);
                score += evaluateWindow(window);
            }
        }
        // Diagonal ↘
        for (let row = 0; row < game.rows - 3; row++) {
            for (let col = 0; col < game.cols - 3; col++) {
                const window = [];
                for (let s = 0; s < 4; s++) window.push(game.grid[(row + s) * game.cols + col + s]);
                score += evaluateWindow(window);
            }
        }
        // Diagonal ↙
        for (let row = 0; row < game.rows - 3; row++) {
            for (let col = 3; col < game.cols; col++) {
                const window = [];
                for (let s = 0; s < 4; s++) window.push(game.grid[(row + s) * game.cols + (col - s)]);
                score += evaluateWindow(window);
            }
        }

        return score;
    }

    /**
     * Verbindungsbewertung: Belohnt horizontal angrenzende eigene Steine.
     * @private
     */
    _connectivityScore(game, player, opponent) {
        const w = this.weights;
        let score = 0;
        for (let row = 0; row < game.rows; row++) {
            for (let col = 0; col < game.cols - 1; col++) {
                const idx = row * game.cols + col;
                const nextIdx = idx + 1;
                if (game.grid[idx] === player && game.grid[nextIdx] === player) score += w.connectivityBonus;
                if (game.grid[idx] === opponent && game.grid[nextIdx] === opponent) score -= w.connectivityBonus;
            }
        }
        return score;
    }

    /**
     * Höhenstrafbewertung: Steine in hohen Reihen (oben) sind instabiler.
     * @private
     */
    _heightScore(game, player, opponent) {
        const w = this.weights;
        let score = 0;
        for (let row = 0; row < game.rows; row++) {
            for (let col = 0; col < game.cols; col++) {
                const idx = row * game.cols + col;
                if (game.grid[idx] === NONE) continue;
                const heightFromBottom = game.rows - 1 - row;
                const penalty = heightFromBottom * Math.abs(w.heightPenalty);
                if (game.grid[idx] === player) score -= penalty;
                else if (game.grid[idx] === opponent) score += penalty;
            }
        }
        return score;
    }

    /**
     * Setup-Bewertung: Bonus für 2er-Gruppen mit 2 Leerstellen (Vorbereitung).
     * Zählt wie viele offene Aufbaupositionen der Spieler hat.
     * @private
     */
    _setupScore(game, player, opponent) {
        const w = this.weights;
        let score = 0;
        // Zähle offene 2er-Fenster (meine und gegnerische)
        const countSetups = (p) => {
            let count = 0;
            // Horizontal
            for (let row = 0; row < game.rows; row++) {
                for (let col = 0; col < game.cols - 3; col++) {
                    let pCount = 0, empty = 0;
                    for (let s = 0; s < 4; s++) {
                        const v = game.grid[row * game.cols + col + s];
                        if (v === p) pCount++;
                        else if (v === NONE) empty++;
                    }
                    if (pCount === 2 && empty === 2) count++;
                }
            }
            return count;
        };
        score += countSetups(player) * w.setupBonus;
        score -= countSetups(opponent) * w.setupBonus;
        return score;
    }

    /**
     * 3D Connect4 (4×4×4) Bewertung.
     * Score = Zentrums-Distanz + 3D-Linienbewertung + Extras.
     * @private
     */
    _evaluate3D(game, player) {
        const w = this.weights;
        const size = game.size;
        const opponent = player === PLAYER1 ? PLAYER2 : PLAYER1;
        let score = 0;

        // Zentrumsdistanz-Bewertung
        const center = (size - 1) / 2;
        for (let idx = 0; idx < game.grid.length; idx++) {
            const cell = game.grid[idx];
            if (cell === NONE) continue;
            const x = idx % size;
            const z = Math.floor(idx / size) % size;
            const y = Math.floor(idx / (size * size));
            const dist = Math.abs(x - center) + Math.abs(y - center) + Math.abs(z - center);
            const centerScore = (size * 2 - dist) * w.centerWeight;
            if (cell === player) score += centerScore;
            else if (cell === opponent) score -= centerScore;
        }

        // 3D-Linienbewertung (Fenstergröße = size)
        score += this._evaluateLines3D(game, player, opponent);

        return score;
    }

    /**
     * 3D-Gleitfenster-Analyse (4er-Fenster in 13 Richtungen).
     * Generiert und cacht die 3D-Linien.
     * @private
     */
    _evaluateLines3D(game, player, opponent) {
        const w = this.weights;
        const size = game.size;
        const lines = this._getLines3D(size);
        let score = 0;

        for (const line of lines) {
            let myCount = 0, oppCount = 0, emptyCount = 0;
            for (const idx of line) {
                const cell = game.grid[idx];
                if (cell === player) myCount++;
                else if (cell === opponent) oppCount++;
                else emptyCount++;
            }
            if (myCount === size && oppCount === 0) score += w.fourInRow;
            if (myCount === size - 1 && emptyCount === 1) score += w.threeInLine;
            if (myCount === size - 2 && emptyCount === 2) score += w.twoInLine;
            if (oppCount === size - 1 && emptyCount === 1) score += w.opponentThreeInLine;
            if (oppCount === size - 2 && emptyCount === 2) score += w.opponentTwoInLine;
        }

        return score;
    }

    /**
     * Generiert und cacht alle Gewinnlinien in einem NxNxN 3D-Gitter.
     * @private
     * @param {number} size - Gittergröße (typisch 4).
     * @returns {number[][]} Array von Linien (je size Indizes).
     */
    _getLines3D(size) {
        const cacheKey = `_lines3d_${size}`;
        if (Connect4Heuristic[cacheKey]) return Connect4Heuristic[cacheKey];

        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) => z * size * size + y * size + x;

        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); }
                        }
                    }
                }
            }
        }

        Connect4Heuristic[cacheKey] = lines;
        return lines;
    }
}

// ═══════════════════════════════════════════════════════════════
//  Registrierung: 3 Profile × 2 Varianten = 6 Instanzen
// ═══════════════════════════════════════════════════════════════
HeuristicRegistry.registerConstructor('connect4', Connect4Heuristic);

const C4_VARIANTS = ['regular', '3d'];
const C4_PROFILES = ['v1_baseline', 'v2_positional', 'v3_aggressive'];

for (const variant of C4_VARIANTS) {
    for (const profile of C4_PROFILES) {
        const cfg = (typeof getConnect4HeuristicsConfig === 'function')
            ? getConnect4HeuristicsConfig(variant, profile)
            : { game: 'connect4', variant, profile };
        const instance = new Connect4Heuristic({
            ...cfg,
            variant,
            profile,
            name: cfg.name || `Connect4 ${variant} (${profile})`
        });
        if (profile === 'v1_baseline') {
            HeuristicRegistry.register(instance);
        }
        instance.id = `connect4:${variant}:${profile}`;
        HeuristicRegistry.register(instance);
    }
}