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