games/knights-tour/controller.js

/**
 * @fileoverview Controller für das Springerproblem.
 * Steuert Interaktionen, KI-Lösung und Unterbrechung.
 */

const Game = {
    /**
     * Das aktuelle Spielbrett.
     * @type {KnightBoard|null}
     */
    board: null,
    /**
     * Canvas-Element für Rendering.
     * @type {HTMLCanvasElement|null}
     */
    canvas: null,
    
    /**
     * Läuft der Solver gerade?
     * @type {boolean}
     */
    isSolving: false,
    /**
     * Wurde Stopp angefordert?
     * @type {boolean}
     */
    stopRequested: false,
    
    /**
     * Callbacks für Integration (z.B. Tree Viz)
     */
    onMoveCallback: null,
    onUndoCallback: null,
    onResetCallback: null,

    /** @type {IframeBridgeClient|null} Bridge-Client für iframe-Kontext */
    bridge: null,
    /** @type {boolean} Wurde CONFIG:INIT über Bridge empfangen? */
    _bridgeActive: false,
    /** @type {number|null} Fallback-Timer ID */
    _bridgeFallbackTimer: null,
    
    init() {
        this.canvas = document.getElementById('boardCanvas');
        if (this.canvas) {
            // Maus und Touch Events (nur wenn nicht von Visualisierung überschrieben)
            this.canvas.addEventListener('mousedown', (e) => this.handleInput(e));
        }
        
        const sizeSelect = document.getElementById('sizeSelect');
        const chkShowMoves = document.getElementById('chkShowMoves');
        const chkShowWarnsdorf = document.getElementById('chkShowWarnsdorf');
        
        if (sizeSelect) sizeSelect.onchange = () => this.reset();
        if (chkShowMoves) chkShowMoves.onchange = () => this.draw();
        if (chkShowWarnsdorf) chkShowWarnsdorf.onchange = () => this.draw();

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

        this.reset();
    },

    /**
     * Resettet das Spiel und stoppt laufende KI-Prozesse.
     */
    reset() {
        // Falls KI läuft: Stoppen!
        if (this.isSolving) {
            this.stopRequested = true;
        }

        const sizeSelect = document.getElementById('sizeSelect');
        const sizeVal = sizeSelect ? sizeSelect.value : '6';
        const size = parseInt(sizeVal);
        
        this.board = new KnightBoard(size);
        
        // Canvas Größe an Container anpassen oder fix lassen
        if (this.canvas) {
            this.canvas.width = 600; 
            this.canvas.height = 600;
        }
        
        // KI-Counter zurücksetzen
        const aiStats = document.getElementById('aiStats');
        if (aiStats) aiStats.innerText = 'KI Knoten: 0';
        
        this.updateUI();
        this.draw();
        
        // Callback aufrufen
        if (this.onResetCallback) {
            this.onResetCallback();
        }
    },

    /**
     * Wechselt zwischen "Lösen" und "Stoppen".
     */
    toggleSolver() {
        if (this.isSolving) {
            this.stopRequested = true;
            // UI wird im Loop aktualisiert
        } else {
            this.solveFast();
        }
    },

    /**
     * Startet die KI-Lösung.
     */
    async solveFast() {
        // Wenn kein Startfeld definiert ist, setze den Springer automatisch unten links
        if (!this.board.currentPos) {
            const startRow = this.board.size - 1;  // Unten
            const startCol = 0;                     // Links
            this.board.move(startRow, startCol);
            this.draw();
        }
        
        if (this.board.won) return;

        // Status setzen
        this.isSolving = true;
        this.stopRequested = false;
        this.updateUI();

        // Warnsdorf-Checkbox prüfen
        const useWarnsdorf = document.getElementById('chkShowWarnsdorf').checked;

        // Engine konfigurieren
        const engine = new SearchEngine({
            strategy: 'DFS', 
            maxDepth: 2000,
            checkDuplicates: true, // Zyklen vermeiden
            
            // Warnsdorf-Heuristik: Wähle Feld mit wenigsten Nachfolgern (nur wenn aktiviert)
            sortSuccessors: useWarnsdorf ? (nodeA, nodeB) => {
                const degA = nodeA.state.getPossibleMoves().length;
                const degB = nodeB.state.getPossibleMoves().length;
                return degA - degB;
            } : null,
            
            // Callback pro Schritt (für Animation & Abbruch)
            onStep: async (state, openListSize, nodesVisited) => {
                // 1. Abbruch prüfen
                if (this.stopRequested) {
                    return 'STOP'; // Signal an SearchEngine
                }

                // 2. Counter aktualisieren
                document.getElementById('aiStats').innerText = `KI Knoten: ${nodesVisited}`;

                // 3. Zeichnen
                const sliderValue = parseInt(document.getElementById('solveSpeed').value);
                // Invertierte Logik: 0 = schnell, 200 = langsam
                const delay = 200 - sliderValue;
                
                // Um Performance zu sparen bei 0 Delay nicht jeden Frame zeichnen
                if (delay > 0 || Math.random() < 0.05) {
                    KnightRenderer.draw(this.canvas, state, { showPossibleMoves: false });
                    if (delay > 0) await new Promise(r => setTimeout(r, delay));
                }
            }
        });

        // Starten
        const result = await engine.solve(this.board);
        
        // Aufräumen
        this.isSolving = false;
        this.stopRequested = false;
        
        if (result.stopped) {
            // Wurde abgebrochen -> UI updaten, Board so lassen wie es ist
            this.updateUI();
            return; 
        }

        if (!result.success) {
            alert("Sackgasse! Keine Lösung von hier.");
            this.draw(); // Originalzustand zeichnen
        } else {
            // Lösung gefunden -> Auf echtes Board anwenden
            // (Die Visualisierung lief auf Kopien)
            // Wir "beamen" das Board in den Endzustand oder spielen schnell ab
            for (const move of result.path) {
                this.board.move(move.r, move.c);
            }
            this.draw();
            setTimeout(() => alert("Gelöst!"), 50);
        }
        this.updateUI();
    },

    handleInput(e) {
        // Eingabe sperren während KI läuft
        if (this.board.won || this.isSolving) return;

        const rect = this.canvas.getBoundingClientRect();
        // Skalierung beachten, falls CSS Canvas Größe ändert
        const scaleX = this.canvas.width / rect.width;
        const scaleY = this.canvas.height / rect.height;

        const x = (e.clientX - rect.left) * scaleX; 
        const y = (e.clientY - rect.top) * scaleY; 
        
        const padding = 30; // Muss mit Renderer übereinstimmen
        const availableW = this.canvas.width - padding;
        const cellSize = availableW / this.board.size;

        // Klick im Padding (links)?
        if (x < padding) return;

        const col = Math.floor((x - padding) / cellSize);
        const row = Math.floor(y / cellSize);

        if (row >= 0 && row < this.board.size && col >= 0 && col < this.board.size) {
            // Check if user clicked the previous square (undo move)
            if (this.board.isUndoMove(row, col)) {
                this.undo();
                return;
            }
            
            if (this.board.move(row, col)) {
                this.draw();
                this.updateUI();
                
                // Callback aufrufen
                if (this.onMoveCallback) {
                    this.onMoveCallback(row, col);
                }
                
                if (this.board.won) setTimeout(() => alert("Gewonnen!"), 100);
                
                // Bridge: Gewonnen-Status melden
                if (this.board.won && this.bridge && this._bridgeActive) {
                    this.bridge.send('GAME:WON', { 
                        gameType: 'knights-tour',
                        moveCount: this.board.moveCount 
                    });
                }
            }
        }
    },

    undo() {
        if (this.isSolving) return;
        this.board.undo();
        this.draw();
        this.updateUI();
        
        // Callback aufrufen
        if (this.onUndoCallback) {
            this.onUndoCallback();
        }
    },

    updateUI() {
        const statsEl = document.getElementById('stats');
        if (statsEl) {
            const max = this.board.size * this.board.size;
            statsEl.innerText = `Züge: ${this.board.moveCount} / ${max}`;
        }
        
        // Button Text und Style ändern
        const btn = document.getElementById('solveBtn');
        if (btn) {
            if (this.isSolving) {
                btn.innerText = "⏹ Stoppen";
                btn.className = "viz-btn btn-danger"; // Rot
            } else {
                btn.innerText = "⚡ Automatisch Lösen";
                btn.className = "viz-btn btn-action"; // Blau/Grün
            }
        }
    },

    draw() {
        if (!this.canvas || !this.board) return;
        
        const chkShowMoves = document.getElementById('chkShowMoves');
        const chkShowWarnsdorf = document.getElementById('chkShowWarnsdorf');
        
        const showMoves = chkShowMoves ? chkShowMoves.checked : false;
        const showWarnsdorf = chkShowWarnsdorf ? chkShowWarnsdorf.checked : false;
        
        KnightRenderer.draw(this.canvas, this.board, { 
            showPossibleMoves: showMoves,
            showWarnsdorf: showWarnsdorf, 
            highContrast: false 
        });
    },

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

    /**
     * Initialisiert den IframeBridgeClient.
     * @private
     */
    _initBridge() {
        this.bridge = new IframeBridgeClient({
            sourceId: 'knights-tour',
            clientType: 'game',
            acceptLegacy: true
        });

        // CONFIG:INIT — Initiale Konfiguration vom Host
        this.bridge.on('CONFIG:INIT', (config) => {
            console.log('[KnightsTour] 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) => {
            console.log('[KnightsTour] CONFIG:UPDATE empfangen', config);
            this._applyBridgeConfig(config);
        });

        // Fallback: Wenn kein CONFIG:INIT in 1s kommt, URL-Parameter verwenden
        this._bridgeFallbackTimer = setTimeout(() => {
            if (!this._bridgeActive) {
                console.log('[KnightsTour] Bridge-Timeout — Fallback auf URL-Parameter');
                this._applyUrlParams();
            }
        }, 1000);
    },

    /**
     * Wendet die Bridge-Konfiguration auf die UI an.
     * @private
     */
    _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: Brettgröße setzen
        if (runtime.boardSize) {
            const sizeSelect = document.getElementById('sizeSelect');
            if (sizeSelect) {
                sizeSelect.value = runtime.boardSize;
                this.reset();
            }
        }
    },

    /**
     * Fallback: Liest UI-Konfiguration aus URL-Parametern.
     * @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';
        }
    }
};

window.onload = () => Game.init();