/**
* Controller RotateBox.
* @fileoverview
*/
const RotateController = {
currentBoard: null,
optimalPath: [],
isOffPath: false,
isAnimating: false,
canvas: null,
ctx: null,
/** @type {IframeBridgeClient|null} Bridge-Client für iframe-Kommunikation */
bridge: null,
/** @type {boolean} true wenn Bridge-Handshake (CONFIG:INIT) erfolgreich war */
_bridgeActive: false,
checkUrlParams() {
const params = new URLSearchParams(window.location.search);
// Level parameter
const level = params.get('level');
if (level !== null) {
const selectInfo = document.getElementById('boardSelect');
if (selectInfo && selectInfo.querySelector(`option[value="${level}"]`)) {
selectInfo.value = level;
}
}
// hide_ai: Versteckt KI-Lösen Button
if (params.has('hide_ai') || params.get('hide_ai') === 'true') {
const solveBtn = document.getElementById('solveBtn');
if (solveBtn) solveBtn.style.display = 'none';
const hr = document.querySelector('.stats-panel hr');
if (hr) hr.style.display = 'none';
}
// hideControls: Versteckt gesamtes Menü
if (params.has('hideControls') || params.get('hideControls') === 'true') {
const menu = document.getElementById('menu');
if (menu) menu.style.display = 'none';
}
// hideLevelSelect: Versteckt nur die Level-Auswahl
if (params.has('hideLevelSelect') || params.get('hideLevelSelect') === 'true') {
const boardSelect = document.getElementById('boardSelect');
if (boardSelect) {
const group = boardSelect.closest('.control-group');
if (group) group.style.display = 'none';
}
}
// hideReset: Versteckt Reset Button
if (params.has('hideReset') || params.get('hideReset') === 'true') {
const resetBtn = document.getElementById('resetBtn');
if (resetBtn) resetBtn.style.display = 'none';
}
// hideAnimation: Versteckt Animation Checkbox
if (params.has('hideAnimation') || params.get('hideAnimation') === 'true') {
const animToggle = document.getElementById('animateToggle');
if (animToggle) {
const group = animToggle.closest('.control-group');
if (group) group.style.display = 'none';
}
}
// hideInstructions: Versteckt die Spielbeschreibung
if (params.has('hideInstructions') || params.get('hideInstructions') === 'true') {
const instructions = document.querySelector('.instructions');
if (instructions) instructions.style.display = 'none';
}
// hideBackBtn: Versteckt den Zurück Button
if (params.has('hideBackBtn') || params.get('hideBackBtn') === 'true') {
const backBtn = document.getElementById('backToMenu');
if (backBtn) backBtn.style.display = 'none';
}
},
init() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.checkUrlParams();
document.getElementById('boardSelect').onchange = () => this.reset();
document.getElementById('resetBtn').onclick = () => this.reset();
document.getElementById('solveBtn').onclick = () => this.runAISolver();
const animBtn = document.getElementById('animateBtn');
if (animBtn) animBtn.onclick = () => this.playSolution();
window.addEventListener('keydown', (e) => {
if (["ArrowLeft", "ArrowRight"].includes(e.key)) e.preventDefault();
if (e.key === 'ArrowLeft') this.handleMove(false);
if (e.key === 'ArrowRight') this.handleMove(true);
});
// Bridge-Integration: Im iframe auf CONFIG:INIT warten statt sofort zu rendern
if (window.parent !== window && typeof IframeBridgeClient !== 'undefined') {
this._initBridge();
} else {
this.reset();
}
},
/**
* Initialisiert den IframeBridgeClient für die Kommunikation mit dem Host.
* Wartet auf CONFIG:INIT und fällt bei Timeout auf URL-Parameter zurück.
* @private
*/
_initBridge() {
this.bridge = new IframeBridgeClient({
sourceId: 'rotatebox-game',
clientType: 'game',
acceptLegacy: true
});
// CONFIG:INIT — komplette Konfiguration vom Host
this.bridge.on('CONFIG:INIT', (config) => {
this._bridgeActive = true;
clearTimeout(this._bridgeTimeout);
this._applyConfig(config);
this.reset();
});
// CONFIG:UPDATE — partielle Änderung (z.B. KI-Button einblenden)
this.bridge.on('CONFIG:UPDATE', (config) => {
this._applyConfig(config);
});
// INPUT:KEYBOARD — Tastatureingaben vom Host (Bridge + Legacy-Konvertierung)
this.bridge.on('INPUT:KEYBOARD', (payload) => {
if (payload.key === 'ArrowLeft') this.handleMove(false);
else if (payload.key === 'ArrowRight') this.handleMove(true);
});
// Fallback: Wenn kein CONFIG:INIT kommt (Legacy-Host), URL-Parameter verwenden
this._bridgeTimeout = setTimeout(() => {
if (!this._bridgeActive) {
this.reset();
}
}, 1000);
},
/**
* Wendet eine Konfiguration aus CONFIG:INIT oder CONFIG:UPDATE an.
* Steuert UI-Sichtbarkeit und Runtime-Parameter.
*
* @private
* @param {Object} config - Konfigurationsobjekt
* @param {Object} [config.ui] - UI-Sichtbarkeit (hideAi, hideLevelSelect, etc.)
* @param {Object} [config.runtime] - Runtime-Parameter (level, etc.)
*/
_applyConfig(config) {
if (!config) return;
// Runtime-Parameter
if (config.runtime && config.runtime.level !== undefined) {
const select = document.getElementById('boardSelect');
if (select && select.querySelector(`option[value="${config.runtime.level}"]`)) {
select.value = String(config.runtime.level);
}
}
// UI-Sichtbarkeit
if (config.ui) {
if (config.ui.hideAi !== undefined) {
const solveBtn = document.getElementById('solveBtn');
const hr = document.querySelector('.stats-panel hr');
if (solveBtn) solveBtn.style.display = config.ui.hideAi ? 'none' : '';
if (hr) hr.style.display = config.ui.hideAi ? 'none' : '';
}
if (config.ui.hideLevelSelect !== undefined) {
const boardSelect = document.getElementById('boardSelect');
if (boardSelect) {
const group = boardSelect.closest('.control-group');
if (group) group.style.display = config.ui.hideLevelSelect ? 'none' : '';
}
}
if (config.ui.hideControls !== undefined) {
const menu = document.getElementById('menu');
if (menu) menu.style.display = config.ui.hideControls ? 'none' : '';
}
if (config.ui.hideReset !== undefined) {
const resetBtn = document.getElementById('resetBtn');
if (resetBtn) resetBtn.style.display = config.ui.hideReset ? 'none' : '';
}
if (config.ui.hideAnimation !== undefined) {
const animToggle = document.getElementById('animateToggle');
if (animToggle) {
const group = animToggle.closest('.control-group');
if (group) group.style.display = config.ui.hideAnimation ? 'none' : '';
}
}
if (config.ui.hideInstructions !== undefined) {
const instructions = document.querySelector('.instructions');
if (instructions) instructions.style.display = config.ui.hideInstructions ? 'none' : '';
}
if (config.ui.hideBackBtn !== undefined) {
const backBtn = document.getElementById('backToMenu');
if (backBtn) backBtn.style.display = config.ui.hideBackBtn ? 'none' : '';
}
}
},
reset() {
const selector = document.getElementById('boardSelect');
this.currentBoard = new RotateBoard(selector.value);
this.optimalPath = [];
this.isOffPath = false;
this.isAnimating = false;
document.getElementById('winMessage').classList.add('hidden');
document.getElementById('aiOutput').classList.add('hidden');
document.getElementById('pathWarning').classList.add('hidden');
document.getElementById('solutionPath').innerHTML = '';
document.getElementById('solveBtn').disabled = false;
document.getElementById('solveBtn').innerText = "KI Lösung suchen 🧠";
this.updateStats();
this.render();
},
async handleMove(isRight) {
if (!this.currentBoard || this.currentBoard.won || this.isAnimating) return;
const moveChar = isRight ? 'R' : 'L';
this.currentBoard.rotate(isRight);
// Pfad-Check
if (this.optimalPath.length > 0 && !this.isOffPath) {
// moves ist 1-basiert, Array ist 0-basiert
if (moveChar !== this.optimalPath[this.currentBoard.moves - 1]) {
this.isOffPath = true;
document.getElementById('pathWarning').classList.remove('hidden');
}
}
// --- HIGHLIGHTING LOGIK ---
this.updatePathHighlighting();
const useAnimation = document.getElementById('animateToggle').checked;
if (useAnimation) {
this.isAnimating = true;
await animateRelax(this.currentBoard, this.canvas, this.ctx, 0.15, () => this.render());
this.isAnimating = false;
} else {
this.currentBoard.relaxBoardSync();
this.render();
}
// Spielstatus an Parent senden (Bridge oder Legacy)
if (window.parent && window.parent !== window) {
if (this._bridgeActive && this.bridge) {
this.bridge.send('GAME:STATE', { rotationCount: this.currentBoard.moves });
} else {
window.parent.postMessage({
type: 'gameState',
rotationCount: this.currentBoard.moves
}, '*');
}
}
if (this.currentBoard.won) {
document.getElementById('winMessage').classList.remove('hidden');
if (window.parent && window.parent !== window) {
if (this._bridgeActive && this.bridge) {
this.bridge.send('GAME:WON', { moves: this.currentBoard.moves });
} else {
window.parent.postMessage({ type: 'gameWon', moves: this.currentBoard.moves }, '*');
}
}
}
this.updateStats();
this.render();
},
updatePathHighlighting() {
// 1. Alle entfernen
document.querySelectorAll('.step-badge').forEach(el => el.classList.remove('active'));
// 2. Aktuellen finden
// Wir suchen das Badge mit der ID "step-X", wobei X der aktuelle Move-Count ist.
// Bsp: Nach 1. Zug (moves=1) soll Badge "step-1" leuchten.
const currentMove = this.currentBoard.moves;
const activeBadge = document.getElementById(`step-${currentMove}`);
if (activeBadge) {
activeBadge.classList.add('active');
// Auto-Scroll, damit das Badge sichtbar bleibt
activeBadge.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
}
},
render() {
if (!this.currentBoard) return;
drawRotateBoard(this.currentBoard, this.canvas, this.ctx);
},
updateStats() {
document.getElementById('moveCount').innerText = this.currentBoard.moves;
},
async runAISolver() {
this.reset(); // Board immer zurücksetzen, bevor KI-Lösung startet
const btn = document.getElementById('solveBtn');
btn.disabled = true;
btn.innerText = "Rechne...";
const simBoard = this.currentBoard.clone();
const result = await solveBFS(simBoard);
if (result) {
this.optimalPath = result.path;
document.getElementById('aiOutput').classList.remove('hidden');
document.getElementById('stat-depth').innerText = result.path.length;
document.getElementById('stat-nodes').innerText = result.nodes;
document.getElementById('stat-duplicates').innerText = result.duplicates || 0;
const pathDiv = document.getElementById('solutionPath');
pathDiv.innerHTML = '';
// Badges rendern
// Start-Offset: Falls User schon 5 Züge gemacht hat, beginnt der Pfad bei Schritt 6.
const startMove = this.currentBoard.moves;
result.path.forEach((dir, i) => {
const span = document.createElement('span');
span.className = 'step-badge';
// ID generieren: step-(Start + Index + 1)
span.id = `step-${startMove + i + 1}`;
span.innerText = dir;
pathDiv.appendChild(span);
});
this.isOffPath = false;
document.getElementById('pathWarning').classList.add('hidden');
btn.innerText = "Lösung anzeigen";
} else {
alert("Keine Lösung gefunden!");
btn.innerText = "Nichts gefunden";
}
btn.disabled = false;
},
async playSolution() {
if (this.optimalPath.length === 0 || this.isAnimating) return;
// Speichere die Lösung und UI-State, weil reset() sie löscht
const savedPath = [...this.optimalPath];
const aiOutputEl = document.getElementById('aiOutput');
const solutionPathEl = document.getElementById('solutionPath');
const savedSolutionHTML = solutionPathEl ? solutionPathEl.innerHTML : '';
this.reset();
await new Promise(r => setTimeout(r, 200));
// Stelle die Lösung und UI wieder her
this.optimalPath = savedPath;
if (aiOutputEl) aiOutputEl.classList.remove('hidden');
if (solutionPathEl) solutionPathEl.innerHTML = savedSolutionHTML;
for (const move of this.optimalPath) {
if(this.currentBoard.won) break;
await this.handleMove(move === 'R');
await new Promise(r => setTimeout(r, 250));
}
}
};
window.RotateController = RotateController;
window.onload = () => RotateController.init();