const DEFAULT_BOARD_SIZE = 8; const MIN_BOARD_SIZE = 4; const MAX_BOARD_SIZE = 13; const EMPTY_CELL = -1; const DEFAULT_SOLVER_MESSAGE = "Bereite einen Ablauf vor und spiele ihn Schritt fuer Schritt ab."; const GAME_STORAGE_KEY = "eight-queens-dashboard-session-v1"; let mode = "queens"; let boardSize = DEFAULT_BOARD_SIZE; let pendingBoardSize = DEFAULT_BOARD_SIZE; let board = createEmptyBoard(boardSize); let stepCount = 0; let elapsedBeforeTimerStartMs = 0; let timerStartedAtMs = Date.now(); let gameTimerId = null; let isSolved = false; let solverSteps = []; let solverStepIndex = -1; let solverTimerId = null; let solverSpeedMs = 320; let solverLoading = false; let solverRequestId = 0; let currentSolverHighlight = null; const boardDiv = document.getElementById("board"); const statusMessage = document.getElementById("statusMessage"); const queenList = document.getElementById("queenList"); const stepCounterValue = document.getElementById("stepCounterValue"); const timerValue = document.getElementById("timerValue"); const gameProgressState = document.getElementById("gameProgressState"); const btnQueens = document.getElementById("btnQueens"); const btnRooks = document.getElementById("btnRooks"); const btnPrepareSolve = document.getElementById("btnPrepareSolve"); const btnInstantSolve = document.getElementById("btnInstantSolve"); const btnNextStep = document.getElementById("btnNextStep"); const btnPlayPause = document.getElementById("btnPlayPause"); const solverMessage = document.getElementById("solverMessage"); const solverStepInfo = document.getElementById("solverStepInfo"); const solverSpeedInput = document.getElementById("solverSpeedInput"); const solverSpeedValue = document.getElementById("solverSpeedValue"); const settingsSidebar = document.getElementById("settingsSidebar"); const sidebarBackdrop = document.getElementById("sidebarBackdrop"); const saveSidebar = document.getElementById("saveSidebar"); const saveBackdrop = document.getElementById("saveBackdrop"); const savePointsSidebar = document.getElementById("savePointsSidebar"); const savePointsBackdrop = document.getElementById("savePointsBackdrop"); const boardSizeInput = document.getElementById("boardSizeInput"); const boardSizeValue = document.getElementById("boardSizeValue"); const boardSizeValueMirror = document.getElementById("boardSizeValueMirror"); const sidebarBoardSizeValue = document.getElementById("sidebarBoardSizeValue"); const sidebarBoardSizeValueMirror = document.getElementById("sidebarBoardSizeValueMirror"); const savePointsList = document.getElementById("savePointsList"); const saveNameInput = document.getElementById("saveNameInput"); const saveNoteInput = document.getElementById("saveNoteInput"); const saveFavoriteInput = document.getElementById("saveFavoriteInput"); const savePointModeFilter = document.getElementById("savePointModeFilter"); const savePointSizeFilter = document.getElementById("savePointSizeFilter"); const savePointSortFilter = document.getElementById("savePointSortFilter"); const savePointFavoritesOnly = document.getElementById("savePointFavoritesOnly"); const timeHistoryTableBody = document.getElementById("timeHistoryTableBody"); const infoModal = document.getElementById("infoModal"); const infoModalBackdrop = document.getElementById("infoModalBackdrop"); function createEmptyBoard(size) { return Array(size).fill(EMPTY_CELL); } function clampBoardSize(size) { return Math.min(MAX_BOARD_SIZE, Math.max(MIN_BOARD_SIZE, Number(size))); } function getColumnLabel(index) { return String.fromCharCode(65 + index); } function sanitizeBoard(candidateBoard, size) { if (!Array.isArray(candidateBoard) || candidateBoard.length !== size) { return createEmptyBoard(size); } return candidateBoard.map((value) => { const numericValue = Number(value); if (!Number.isInteger(numericValue)) { return EMPTY_CELL; } return numericValue >= 0 && numericValue < size ? numericValue : EMPTY_CELL; }); } function formatDuration(totalSeconds) { const safeSeconds = Math.max(0, Number(totalSeconds) || 0); const hours = Math.floor(safeSeconds / 3600); const minutes = Math.floor((safeSeconds % 3600) / 60); const seconds = safeSeconds % 60; return [hours, minutes, seconds] .map((value) => String(value).padStart(2, "0")) .join(":"); } function getElapsedMilliseconds() { if (isSolved) { return elapsedBeforeTimerStartMs; } return elapsedBeforeTimerStartMs + Math.max(0, Date.now() - timerStartedAtMs); } function persistCurrentSession() { try { window.localStorage.setItem( GAME_STORAGE_KEY, JSON.stringify({ mode, boardSize, board, stepCount, elapsedMs: getElapsedMilliseconds(), isSolved }) ); } catch (error) { // Ignore unavailable storage so the game keeps working normally. } } function updateProgressDisplay() { stepCounterValue.textContent = String(stepCount); timerValue.textContent = formatDuration(Math.floor(getElapsedMilliseconds() / 1000)); gameProgressState.textContent = isSolved ? "Geloest" : "Laeuft"; gameProgressState.style.color = isSolved ? "#15803d" : "#1d4ed8"; } function stopGameTimer() { if (gameTimerId !== null) { window.clearInterval(gameTimerId); gameTimerId = null; } } function startGameTimer() { stopGameTimer(); if (isSolved) { updateProgressDisplay(); persistCurrentSession(); return; } timerStartedAtMs = Date.now(); gameTimerId = window.setInterval(() => { updateProgressDisplay(); persistCurrentSession(); }, 1000); updateProgressDisplay(); persistCurrentSession(); } function resetProgress({ stepCountValue = 0, elapsedMs = 0, solved = false } = {}) { stepCount = Math.max(0, Number(stepCountValue) || 0); elapsedBeforeTimerStartMs = Math.max(0, Number(elapsedMs) || 0); isSolved = Boolean(solved); timerStartedAtMs = Date.now(); if (isSolved) { stopGameTimer(); updateProgressDisplay(); persistCurrentSession(); return; } startGameTimer(); } function ensureFreshRunAfterSolvedBoard() { if (!isSolved) { return; } resetProgress(); } function updateModeButtons() { btnQueens.classList.toggle("active", mode === "queens"); btnRooks.classList.toggle("active", mode === "rooks"); } function updateBoardSizeDisplays(size) { boardSizeValue.textContent = size; boardSizeValueMirror.textContent = size; sidebarBoardSizeValue.textContent = size; sidebarBoardSizeValueMirror.textContent = size; boardSizeInput.value = size; } function setStatus(message = "", color = "") { statusMessage.textContent = message; statusMessage.style.color = color; } function setSolverMessage(message = DEFAULT_SOLVER_MESSAGE) { solverMessage.textContent = message; } function updateSolverSpeed(rawValue) { solverSpeedMs = Number(rawValue); solverSpeedInput.value = String(solverSpeedMs); solverSpeedValue.textContent = `${solverSpeedMs} ms`; if (isSolverPlaying()) { stopSolverPlayback(false); scheduleNextSolverPlaybackStep(); } } function updateBoardLayout() { const cellSize = Math.max(32, Math.min(60, Math.floor(480 / boardSize))); boardDiv.style.setProperty("--board-size", boardSize); boardDiv.style.setProperty("--cell-size", `${cellSize}px`); } function isSolverPlaying() { return solverTimerId !== null; } function hasPreparedSolverTrace() { return solverSteps.length > 0; } function hasCompletedSolverTrace() { return hasPreparedSolverTrace() && solverStepIndex >= solverSteps.length - 1; } function stopSolverPlayback(refreshControls = true) { if (solverTimerId !== null) { window.clearTimeout(solverTimerId); solverTimerId = null; } if (refreshControls) { updateSolverControls(); } } function updateSolverControls() { const currentStep = Math.max(0, solverStepIndex + 1); btnPrepareSolve.disabled = solverLoading; btnInstantSolve.disabled = solverLoading; btnNextStep.disabled = solverLoading || hasCompletedSolverTrace(); btnPlayPause.disabled = solverLoading; btnPrepareSolve.textContent = solverLoading ? "Preparing..." : "Solve Step-by-Step"; if (isSolverPlaying()) { btnPlayPause.textContent = "Pause"; } else if (hasCompletedSolverTrace()) { btnPlayPause.textContent = "Replay Animation"; } else { btnPlayPause.textContent = "Play Animation"; } solverStepInfo.textContent = `Schritt ${currentStep} / ${solverSteps.length}`; } function discardSolverTrace(message = DEFAULT_SOLVER_MESSAGE) { solverRequestId += 1; solverLoading = false; stopSolverPlayback(false); solverSteps = []; solverStepIndex = -1; currentSolverHighlight = null; setSolverMessage(message); updateSolverControls(); } function rewindSolverTrace() { stopSolverPlayback(false); solverStepIndex = -1; currentSolverHighlight = null; board = createEmptyBoard(boardSize); setSolverMessage("Ablauf zurueckgesetzt. Starte die Animation erneut oder gehe weiter mit Next Step."); drawBoard(); updateSolverControls(); } function setMode(newMode) { if (mode === newMode) { return; } mode = newMode; updateModeButtons(); resetBoard({ statusMessageText: `${getPieceLabel(mode)}-Modus aktiviert.`, statusColor: "#2563eb", solverMessageText: `${getPieceLabel(mode)}-Solver bereit. ${DEFAULT_SOLVER_MESSAGE}` }); } function closeAllPanels() { toggleSidebar(false); toggleSavePanel(false); toggleSavePointsPanel(false); toggleInfoModal(false); } function getConflicts(selectedBoard = board, selectedMode = mode) { const conflicts = []; for (let rowA = 0; rowA < boardSize; rowA++) { if (selectedBoard[rowA] === EMPTY_CELL) { continue; } for (let rowB = rowA + 1; rowB < boardSize; rowB++) { if (selectedBoard[rowB] === EMPTY_CELL) { continue; } const colA = selectedBoard[rowA]; const colB = selectedBoard[rowB]; const sameColumn = colA === colB; const sameDiagonal = Math.abs(colA - colB) === Math.abs(rowA - rowB); const hasConflict = selectedMode === "rooks" ? sameColumn : sameColumn || sameDiagonal; if (hasConflict) { conflicts.push([rowA, colA], [rowB, colB]); } } } return conflicts; } function isBoardSolved(selectedBoard = board, selectedMode = mode) { const allPlaced = selectedBoard.every((col) => col !== EMPTY_CELL); return allPlaced && getConflicts(selectedBoard, selectedMode).length === 0; } function finishSolvedRun() { if (isSolved) { return; } elapsedBeforeTimerStartMs = getElapsedMilliseconds(); isSolved = true; stopGameTimer(); updateProgressDisplay(); persistCurrentSession(); } function checkSolution() { const allPlaced = board.every((col) => col !== EMPTY_CELL); if (!allPlaced) { setStatus("Noch nicht alle Figuren platziert.", "#d97706"); return; } if (getConflicts().length > 0) { setStatus("Es gibt Konflikte auf dem Brett.", "red"); return; } finishSolvedRun(); setStatus(`Perfekt geloest in ${formatDuration(Math.floor(getElapsedMilliseconds() / 1000))}.`, "green"); } function drawBoard() { boardDiv.innerHTML = ""; boardDiv.className = "board-grid"; updateBoardLayout(); const conflicts = getConflicts(); const highlight = currentSolverHighlight; for (let row = -1; row < boardSize; row++) { for (let col = -1; col < boardSize; col++) { const cell = document.createElement("div"); if (row === -1 && col === -1) { cell.classList.add("label"); } else if (row === -1) { cell.textContent = getColumnLabel(col); cell.classList.add("label"); } else if (col === -1) { cell.textContent = boardSize - row; cell.classList.add("label"); } else { cell.classList.add("cell", (row + col) % 2 === 0 ? "white" : "black"); if (highlight && highlight.row === row && highlight.col === col) { cell.classList.add( highlight.type === "remove" ? "solver-remove" : highlight.type === "solution" ? "solver-solution" : "solver-place" ); } if (board[row] === col) { cell.textContent = mode === "queens" ? "♛" : "♜"; if (conflicts.some(([conflictRow, conflictCol]) => conflictRow === row && conflictCol === col)) { cell.classList.add("invalid"); } } cell.onclick = () => placeQueen(row, col); } boardDiv.appendChild(cell); } } updateQueenList(); } function placeQueen(row, col) { if (solverLoading) { return; } ensureFreshRunAfterSolvedBoard(); if (hasPreparedSolverTrace() || isSolverPlaying()) { discardSolverTrace("Solver-Ablauf verworfen, weil das Brett manuell geaendert wurde."); setStatus("Solver-Ablauf verworfen. Bereite ihn bei Bedarf neu vor.", "#d97706"); } else { setStatus(); } currentSolverHighlight = null; board[row] = board[row] === col ? EMPTY_CELL : col; stepCount += 1; drawBoard(); updateProgressDisplay(); if (isBoardSolved()) { finishSolvedRun(); setStatus(`Perfekt geloest in ${formatDuration(Math.floor(getElapsedMilliseconds() / 1000))}.`, "green"); } else { persistCurrentSession(); } } function previewBoardSize(size) { pendingBoardSize = clampBoardSize(size); sidebarBoardSizeValue.textContent = pendingBoardSize; sidebarBoardSizeValueMirror.textContent = pendingBoardSize; } function applyBoardSize() { boardSize = pendingBoardSize; discardSolverTrace(`${getPieceLabel(mode)}-Solver bereit. ${DEFAULT_SOLVER_MESSAGE}`); board = createEmptyBoard(boardSize); currentSolverHighlight = null; resetProgress(); updateBoardSizeDisplays(boardSize); setStatus(`Brettgroesse auf ${boardSize} x ${boardSize} gesetzt.`, "#2563eb"); drawBoard(); toggleSidebar(false); } function toggleSidebar(forceOpen) { const shouldOpen = typeof forceOpen === "boolean" ? forceOpen : !settingsSidebar.classList.contains("open"); if (shouldOpen) { toggleSavePanel(false); toggleSavePointsPanel(false); } settingsSidebar.classList.toggle("open", shouldOpen); sidebarBackdrop.classList.toggle("visible", shouldOpen); settingsSidebar.setAttribute("aria-hidden", String(!shouldOpen)); } function toggleSavePanel(forceOpen) { const shouldOpen = typeof forceOpen === "boolean" ? forceOpen : !saveSidebar.classList.contains("open"); if (shouldOpen) { toggleSidebar(false); toggleSavePointsPanel(false); } saveSidebar.classList.toggle("open", shouldOpen); saveBackdrop.classList.toggle("visible", shouldOpen); saveSidebar.setAttribute("aria-hidden", String(!shouldOpen)); } function toggleSavePointsPanel(forceOpen) { const shouldOpen = typeof forceOpen === "boolean" ? forceOpen : !savePointsSidebar.classList.contains("open"); if (shouldOpen) { toggleSidebar(false); toggleSavePanel(false); } savePointsSidebar.classList.toggle("open", shouldOpen); savePointsBackdrop.classList.toggle("visible", shouldOpen); savePointsSidebar.setAttribute("aria-hidden", String(!shouldOpen)); } function toggleInfoModal(forceOpen) { const shouldOpen = typeof forceOpen === "boolean" ? forceOpen : !infoModal.classList.contains("open"); infoModal.classList.toggle("open", shouldOpen); infoModalBackdrop.classList.toggle("visible", shouldOpen); infoModal.setAttribute("aria-hidden", String(!shouldOpen)); } function getPieceLabel(selectedMode) { return selectedMode === "queens" ? "Dame" : "Turm"; } function getPlacedPieceCount() { return board.filter((col) => col !== EMPTY_CELL).length; } function resetSaveForm() { saveNameInput.value = `${getPieceLabel(mode)} ${boardSize}x${boardSize} - ${getPlacedPieceCount()} gesetzt`; saveNoteInput.value = ""; saveFavoriteInput.checked = false; } function openSavePanel() { resetSaveForm(); toggleSavePanel(true); } function getBoardPreview(savedBoard, limit = 4) { const preview = []; for (let row = 0; row < savedBoard.length; row++) { if (savedBoard[row] === EMPTY_CELL) { continue; } preview.push(`${getColumnLabel(savedBoard[row])}${savedBoard.length - row}`); if (preview.length === limit) { break; } } return preview.length > 0 ? preview.join(", ") : "Noch keine Figuren gesetzt"; } function getSavePointFilters() { const params = new URLSearchParams(); if (savePointModeFilter.value !== "all") { params.set("mode", savePointModeFilter.value); } if (savePointSizeFilter.value !== "all") { params.set("board_size", savePointSizeFilter.value); } params.set("sort", savePointSortFilter.value); if (savePointFavoritesOnly.checked) { params.set("favorites_only", "true"); } return params.toString(); } function createSavePointActionButton(label, className, onClick) { const button = document.createElement("button"); button.className = className; button.type = "button"; button.textContent = label; button.onclick = onClick; return button; } function getModeLabel(selectedMode) { return selectedMode === "rooks" ? "Tuerme" : "Damen"; } function renderSavePoints(savePoints) { savePointsList.innerHTML = ""; if (savePoints.length === 0) { savePointsList.innerHTML = '
'; return; } for (const savePoint of savePoints) { const item = document.createElement("article"); const title = document.createElement("h4"); const createdAt = document.createElement("p"); const updatedAt = document.createElement("p"); const boardMeta = document.createElement("p"); const preview = document.createElement("p"); const note = document.createElement("p"); const actions = document.createElement("div"); item.className = "save-point-card"; title.textContent = `${savePoint.is_favorite ? "★ " : ""}${savePoint.save_name}`; createdAt.className = "save-point-meta"; createdAt.textContent = `Erstellt: ${savePoint.created_at}`; updatedAt.className = "save-point-meta"; updatedAt.textContent = `Aktualisiert: ${savePoint.updated_at}`; boardMeta.className = "save-point-meta"; boardMeta.textContent = `${getPieceLabel(savePoint.mode)} | ${savePoint.board_size} x ${savePoint.board_size} | ${savePoint.pieces_placed}/${savePoint.board_size} gesetzt`; preview.className = "save-point-preview"; preview.textContent = `Vorschau: ${getBoardPreview(savePoint.board)}`; note.className = "save-point-note"; note.textContent = savePoint.save_note ? `Notiz: ${savePoint.save_note}` : "Notiz: Keine Notiz"; actions.className = "save-point-actions"; const progressMeta = document.createElement("p"); progressMeta.className = "save-point-meta"; progressMeta.textContent = `Schritte: ${savePoint.step_count || 0} | Zeit: ${formatDuration(savePoint.elapsed_seconds)} | Status: ${savePoint.is_solved ? "Geloest" : "Offen"}`; actions.appendChild(createSavePointActionButton("Laden", "btn", () => loadSavePoint(savePoint.id))); actions.appendChild(createSavePointActionButton( savePoint.is_favorite ? "Favorit entfernen" : "Als Favorit", "btn secondary", () => toggleSavePointFavorite(savePoint.id) )); actions.appendChild(createSavePointActionButton("Loeschen", "btn ghost", () => deleteSavePoint(savePoint.id))); item.appendChild(title); item.appendChild(createdAt); item.appendChild(updatedAt); item.appendChild(boardMeta); item.appendChild(progressMeta); item.appendChild(preview); item.appendChild(note); item.appendChild(actions); savePointsList.appendChild(item); } } function renderTimeHistory(savePoints) { timeHistoryTableBody.innerHTML = ""; if (savePoints.length === 0) { timeHistoryTableBody.innerHTML = '