core/base-game-controller.js

/**
 * Basis-Controller für gitterbasierte Strategiespiele (TTT, Connect4, etc.).
 * 
 * Reduziert Code-Duplikation durch ein gemeinsames Template,
 * das von Regular TTT, 3D, Ultimate, Connect 4 und weiteren genutzt wird.
 * @fileoverview
 */

class BaseGameController {
    /**
     * Initialisiert den Controller.
     * Muss von der Subklasse aufgerufen werden.
     * @param {string} gameType - z.B. 'regular', 'connect4-3d'
     * @param {string} canvasId - HTML Canvas Element ID
     */
    constructor(gameType, canvasId) {
        DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "debug", '🎮 BaseGameController.constructor() aufgerufen');
        DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "debug", '   - gameType:', gameType);
        DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "debug", '   - canvasId:', canvasId);
        
        this.gameType = gameType;
        this.canvas = document.getElementById(canvasId);
        
        DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "debug", '   - Canvas gefunden?', this.canvas !== null);
        if (this.canvas) {
            DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "debug", '   - Canvas Dimensionen:', this.canvas.width, 'x', this.canvas.height);
        } else {
            DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "error", '❌ FEHLER: Canvas nicht gefunden! ID:', canvasId);
        }
        
        this.game = null;
        this.adapter = null;
        this.isProcessing = false;

        /** @type {IframeBridgeClient|null} Bridge-Client für iframe-Kontext */
        this.bridge = null;
        /** @type {boolean} Wurde CONFIG:INIT über Bridge empfangen? */
        this._bridgeActive = false;
        /** @type {number|null} Fallback-Timer ID */
        this._bridgeFallbackTimer = null;
    }

    /**
     * Erstellt das Spiel (wird von Subklasse überschrieben).
     * @abstract
     * @returns {TTTRegularBoard|TTT3DBoard|UltimateBoard}
     */
    createGame() {
        throw new Error('createGame() muss von Subklasse implementiert werden');
    }

    /**
     * Zeichnet das Spiel (wird von Subklasse überschrieben).
     * @abstract
     */
    drawGame() {
        throw new Error('drawGame() muss von Subklasse implementiert werden');
    }

    /**
     * Konvertiert Canvas-Koordinaten zu einem Zug (wird von Subklasse überschrieben).
     * @abstract
     * @param {number} mx - Mouse X in Canvas-Koordinaten
     * @param {number} my - Mouse Y in Canvas-Koordinaten
     * @returns {number|object|null} Der Zug oder null
     */
    coordsToMove(mx, my) {
        throw new Error('coordsToMove() muss von Subklasse implementiert werden');
    }

    /**
     * Initialisiert den Controller (aufgerufen von onload).
     */
    init() {
        DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "debug", '🎮 BaseGameController.init() - Initialisierung startet');
        DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "debug", '🔍 Überprüfung der Konstanten:', { NONE, PLAYER1, PLAYER2, DRAW });
        
        this.canvas.addEventListener('mousedown', (e) => this.handleCanvasClick(e));
        
        const p1Sel = document.getElementById('p1Type');
        const p2Sel = document.getElementById('p2Type');
        if (p1Sel) p1Sel.onchange = () => this.checkTurn();
        if (p2Sel) p2Sel.onchange = () => this.checkTurn();

        // Bridge-Integration: Im iframe-Kontext IframeBridgeClient initialisieren
        if (window.self !== window.top && window.IframeBridgeClient) {
            this._initBridge();
        }

        this.reset();
    }

    /**
     * Setzt das Spiel zurück.
     */
    reset() {
        DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "debug", '🔄 BaseGameController.reset() wird aufgerufen');
        
        try {
            this.game = this.createGame();
            DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "debug", '✅ Game-Objekt erstellt:', this.game);
            DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "debug", '   - game.grid:', this.game.grid?.slice(0, 3), '...');
            DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "debug", '   - game.winner:', this.game.winner);
            DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "debug", '   - game.currentPlayer:', this.game.currentPlayer);
            DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "debug", '   - game.getAllValidMoves:', typeof this.game.getAllValidMoves);
        } catch (error) {
            DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "error", '❌ FEHLER beim Erstellen des Game-Objekts:', error);
            DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "error", '   Stack:', error.stack);
            return;
        }
        
        try {
            this.adapter = new GameAdapter(this.game, this.gameType);
            DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "debug", '✅ GameAdapter erstellt');
        } catch (error) {
            DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "error", '❌ FEHLER beim Erstellen des GameAdapter:', error);
            DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "error", '   Stack:', error.stack);
            return;
        }
        
        this.isProcessing = false;
        this.updateUI();
        
        try {
            this.drawGame();
        } catch (error) {
            DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "error", '❌ FEHLER beim drawGame():', error);
            DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "error", '   Stack:', error.stack);
        }
        
        try {
            this.checkTurn();
        } catch (error) {
            DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "error", '❌ FEHLER beim checkTurn():', error);
            DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "error", '   Stack:', error.stack);
        }
    }

    /**
     * Verarbeitet Klicks auf das Canvas.
     * @param {MouseEvent} e
     */
    handleCanvasClick(e) {
        if (this.isProcessing || this.adapter.isGameOver()) return;

        const rect = this.canvas.getBoundingClientRect();
        const scale = this.canvas.width / rect.width;
        const mx = (e.clientX - rect.left) * scale;
        const my = (e.clientY - rect.top) * scale;

        const move = this.coordsToMove(mx, my);
        if (move !== null && move !== undefined) {
            if (this.adapter.makeMove(move)) {
                this.drawGame();
                this.checkTurn();
            }
        }
    }

    /**
     * Hauptlogik: Prüft, ob Spiel vorbei ist oder KI am Zug ist.
     */
    checkTurn() {
        const validMoves = this.adapter.getValidMoves();
        const hasWinner = this.adapter.isGameOver();
        const hasValidMoves = validMoves.length > 0;

        DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "debug", 
            `📊 ${this.gameType} checkTurn: ` +
            `Player=${this.adapter.getCurrentPlayer()}, ` +
            `Winner=${this.adapter.getWinner()}, ` +
            `ValidMoves=${validMoves.length}`
        );

        if (hasWinner || !hasValidMoves) {
            DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "debug", `🏁 Game Over: Winner=${this.adapter.getWinner()}`);
            this.updateUI();
            this._sendGameResult();
            return;
        }

        this.updateUI();

        // KI-Check
        const p1Type = document.getElementById('p1Type')?.value || 'human';
        const p2Type = document.getElementById('p2Type')?.value || 'human';
        const currentType = this.adapter.getCurrentPlayer() === 1 ? p1Type : p2Type;

        if (currentType !== 'human') {
            this.isProcessing = true;
            const speed = this.getAISpeed();

            setTimeout(() => {
                try {
                    DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "debug", `🤖 Versuche AI ${currentType} zu erstellen...`);
                    const agent = this.createAIAgent(currentType);
                    if (agent) {
                        DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "debug", '🤖 Agent erstellt, rufe getAction() auf...');
                        const action = agent.getAction(this.game);
                        DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "debug", `🤖 KI ${this.adapter.getCurrentPlayer()} Aktion:`, action);

                        if (action && action.move !== undefined && action.move !== null) {
                            DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "debug", `✅ Zug: ${JSON.stringify(action.move)}`);
                            this.adapter.makeMove(action.move);
                        } else {
                            DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "warn", '❌ KI findet keinen gültigen Zug!');
                        }
                    } else {
                        DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "warn", '⚠️ Agent war null!');
                    }
                } catch (error) {
                    DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "error", '❌ FEHLER im AI-Timeout:', error);
                    DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "error", '   Stack:', error.stack);
                } finally {
                    this.isProcessing = false;
                    this.drawGame();
                    this.checkTurn();
                }
            }, speed);
        }
    }

    /**
     * Erstellt den KI-Agenten (wird von Subklasse überschrieben).
     * @param {string} type - 'random', 'rulebased', oder 'minimax'
     * @returns {Agent|null}
     */
    createAIAgent(type) {
        throw new Error('createAIAgent() muss von Subklasse implementiert werden');
    }

    /**
     * Holt die KI-Geschwindigkeit aus dem Slider.
     * @returns {number} Verzögerung in ms
     */
    getAISpeed() {
        const slider = document.getElementById('aiSpeed');
        if (!slider) return 1000;
        const sliderValue = parseInt(slider.value);
        return 2000 - sliderValue; // Invertierte Logik: 0=schnell, 2000=langsam
    }

    /**
     * Aktualisiert die UI (Status-Text).
     */
    updateUI() {
        const statusEl = document.getElementById('statusText');
        if (!statusEl) return;

        if (this.adapter.isGameOver()) {
            const winner = this.adapter.getWinner();
            if (winner === 3) {
                statusEl.textContent = 'REMIS';
            } else {
                statusEl.textContent = `SIEG: ${winner === 1 ? 'BLAU' : 'ROT'}`;
            }
        } else if (this.adapter.getRemainingMoves() === 0) {
            statusEl.textContent = 'REMIS';
        } else {
            const player = this.adapter.getCurrentPlayer();
            statusEl.textContent = `${player === 1 ? 'BLAU' : 'ROT'} ist dran`;
        }
    }

    // ==================== BRIDGE INTEGRATION ====================

    /**
     * Initialisiert den IframeBridgeClient für Kommunikation mit dem Host.
     * Wird nur aufgerufen wenn die Seite in einem iframe läuft.
     * @private
     */
    _initBridge() {
        DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "debug", '🌉 Bridge-Client wird initialisiert...');

        this.bridge = new IframeBridgeClient({
            sourceId: this.gameType,
            clientType: 'game',
            acceptLegacy: true
        });

        // CONFIG:INIT — Initiale Konfiguration vom Host
        this.bridge.on('CONFIG:INIT', (config) => {
            DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "debug", '🌉 CONFIG:INIT empfangen', config);
            this._bridgeActive = true;
            if (this._bridgeFallbackTimer) {
                clearTimeout(this._bridgeFallbackTimer);
                this._bridgeFallbackTimer = null;
            }
            this._applyBridgeConfig(config);
        });

        // CONFIG:UPDATE — Dynamische Updates vom Host
        this.bridge.on('CONFIG:UPDATE', (config) => {
            DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "debug", '🌉 CONFIG:UPDATE empfangen', config);
            this._applyBridgeConfig(config);
        });

        // Fallback: Wenn kein CONFIG:INIT in 1s kommt, URL-Parameter verwenden
        this._bridgeFallbackTimer = setTimeout(() => {
            if (!this._bridgeActive) {
                DebugConfig.log(DEBUG_DOMAINS.CORE_GAME_CONTROLLER, "debug", '🌉 Bridge-Timeout — Fallback auf URL-Parameter');
                this._applyUrlParams();
            }
        }, 1000);
    }

    /**
     * Wendet die Bridge-Konfiguration auf die UI an.
     * @private
     * @param {Object} config - Konfiguration mit ui/runtime-Schlüsseln
     */
    _applyBridgeConfig(config) {
        const ui = config.ui || {};
        const runtime = config.runtime || {};

        // UI: Back-Button verstecken
        if (ui.hideBackBtn) {
            const backBtn = document.querySelector('.btn-back');
            if (backBtn) backBtn.style.display = 'none';
        }

        // UI: Gesamte Sidebar verstecken
        if (ui.hideControls) {
            const sidebar = document.querySelector('.sidebar');
            if (sidebar) sidebar.style.display = 'none';
        }

        // Runtime: Spielertypen setzen
        if (runtime.p1Type) {
            const p1Sel = document.getElementById('p1Type');
            if (p1Sel) { p1Sel.value = runtime.p1Type; }
        }
        if (runtime.p2Type) {
            const p2Sel = document.getElementById('p2Type');
            if (p2Sel) { p2Sel.value = runtime.p2Type; }
        }

        // Nach Config-Anwendung ggf. KI-Zug prüfen
        if (this.game) {
            this.checkTurn();
        }
    }

    /**
     * Fallback: Liest UI-Konfiguration aus URL-Parametern.
     * Wird nur aufgerufen wenn kein Bridge-Host antwortet.
     * @private
     */
    _applyUrlParams() {
        const params = new URLSearchParams(window.location.search);

        if (params.get('hideBackBtn') === 'true') {
            const backBtn = document.querySelector('.btn-back');
            if (backBtn) backBtn.style.display = 'none';
        }
        if (params.get('hideControls') === 'true') {
            const sidebar = document.querySelector('.sidebar');
            if (sidebar) sidebar.style.display = 'none';
        }
        if (params.has('p1Type')) {
            const p1Sel = document.getElementById('p1Type');
            if (p1Sel) p1Sel.value = params.get('p1Type');
        }
        if (params.has('p2Type')) {
            const p2Sel = document.getElementById('p2Type');
            if (p2Sel) p2Sel.value = params.get('p2Type');
        }
    }

    /**
     * Sendet den Spielausgang via Bridge an den Host.
     * @private
     */
    _sendGameResult() {
        if (!this.bridge || !this._bridgeActive) return;

        const winner = this.adapter.getWinner();
        const remainingMoves = this.adapter.getRemainingMoves();

        if (winner === 3 || remainingMoves === 0) {
            this.bridge.send('GAME:DRAW', {
                gameType: this.gameType
            });
        } else {
            this.bridge.send('GAME:WON', {
                winner: winner,
                gameType: this.gameType
            });
        }
    }
}