Schritte
+0
+diff --git a/app/__init__.py b/app/__init__.py index 21dc324..9abff38 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -7,6 +7,7 @@ from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() login_manager = LoginManager() + def create_app(): app = Flask(__name__) diff --git a/app/models/gamestate.py b/app/models/gamestate.py index 7aef035..ddb1ab2 100644 --- a/app/models/gamestate.py +++ b/app/models/gamestate.py @@ -1,5 +1,6 @@ from .. import db + class GameState(db.Model): __tablename__ = "game_state" @@ -12,6 +13,9 @@ class GameState(db.Model): save_name = db.Column(db.String(80), nullable=False, default="Unnamed Save") save_note = db.Column(db.Text, nullable=False, default="") is_favorite = db.Column(db.Boolean, nullable=False, default=False) + step_count = db.Column(db.Integer, nullable=False, default=0, server_default=db.text("0")) + elapsed_seconds = db.Column(db.Integer, nullable=False, default=0, server_default=db.text("0")) + is_solved = db.Column(db.Boolean, nullable=False, default=False, server_default=db.text("0")) created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now()) updated_at = db.Column( db.DateTime, diff --git a/app/routes/main_routes.py b/app/routes/main_routes.py index 15ea363..ad17578 100644 --- a/app/routes/main_routes.py +++ b/app/routes/main_routes.py @@ -28,6 +28,15 @@ def parse_favorite_flag(raw_value): return str(raw_value).lower() in {"1", "true", "yes", "on"} +def parse_non_negative_int(raw_value, default=0): + try: + value = int(raw_value) + except (TypeError, ValueError): + return default + + return max(0, value) + + def get_position_label(row, col, board_size): return f"{chr(65 + col)}{board_size - row}" @@ -180,6 +189,9 @@ def serialize_game_state(game_state): "save_note": game_state.save_note, "is_favorite": game_state.is_favorite, "pieces_placed": sum(1 for column in board if column != -1), + "step_count": game_state.step_count, + "elapsed_seconds": game_state.elapsed_seconds, + "is_solved": game_state.is_solved, "created_at": game_state.created_at.strftime("%Y-%m-%d %H:%M:%S"), "updated_at": game_state.updated_at.strftime("%Y-%m-%d %H:%M:%S") } @@ -282,6 +294,9 @@ def save_game(): save_name = (data.get("saveName") or "").strip() or f"{piece_mode.title()} Save" save_note = (data.get("saveNote") or "").strip() is_favorite = bool(data.get("isFavorite")) + step_count = parse_non_negative_int(data.get("stepCount")) + elapsed_seconds = parse_non_negative_int(data.get("elapsedSeconds")) + is_solved = bool(data.get("isSolved")) timestamp = datetime.utcnow() game_state = GameState( @@ -292,6 +307,9 @@ def save_game(): save_name=save_name, save_note=save_note, is_favorite=is_favorite, + step_count=step_count, + elapsed_seconds=elapsed_seconds, + is_solved=is_solved, created_at=timestamp, updated_at=timestamp ) diff --git a/app/static/css/dashboard.css b/app/static/css/dashboard.css index fe15596..b83c8e2 100644 --- a/app/static/css/dashboard.css +++ b/app/static/css/dashboard.css @@ -94,6 +94,68 @@ body { text-align: center; } +.game-card-header { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; +} + +.game-card-header h2 { + margin: 0; +} + +.info-button { + width: 38px; + height: 38px; + border: none; + border-radius: 999px; + background: #dbeafe; + color: #1d4ed8; + font-size: 20px; + font-weight: bold; + cursor: pointer; +} + +.info-button:hover { + background: #bfdbfe; +} + +.game-stats { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; + margin: 18px 0 12px; +} + +.game-stat-card { + padding: 14px 16px; + border-radius: 12px; + background: #f8fafc; + border: 1px solid #dbe4ee; + text-align: left; +} + +.game-stat-label { + margin: 0 0 8px; + font-size: 14px; + color: #64748b; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.game-stat-value { + margin: 0; + font-size: 26px; + font-weight: bold; + color: #1e293b; + font-variant-numeric: tabular-nums; +} + +.game-stat-status { + font-size: 20px; +} + #statusMessage { min-height: 24px; margin: 0 0 8px; @@ -478,6 +540,55 @@ body { margin: 0; } +.history-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 12px; + margin-bottom: 14px; +} + +.history-header h3 { + margin: 0; +} + +.history-subtitle { + margin: 6px 0 0; + color: #64748b; +} + +.history-table-wrapper { + overflow-x: auto; +} + +.history-table { + width: 100%; + border-collapse: collapse; + min-width: 720px; +} + +.history-table th, +.history-table td { + padding: 12px 10px; + border-bottom: 1px solid #e2e8f0; + text-align: left; +} + +.history-table th { + color: #334155; + font-size: 14px; +} + +.history-table td { + color: #475569; + font-size: 14px; +} + +.history-empty { + color: #64748b; + text-align: center !important; +} + .btn.ghost { background: #e2e8f0; color: #1e293b; @@ -502,6 +613,73 @@ body { pointer-events: auto; } +.modal-backdrop { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.42); + opacity: 0; + pointer-events: none; + transition: opacity 0.25s ease; + z-index: 40; +} + +.modal-backdrop.visible { + opacity: 1; + pointer-events: auto; +} + +.modal { + position: fixed; + top: 50%; + left: 50%; + width: min(620px, calc(100vw - 32px)); + max-height: min(82vh, 720px); + padding: 24px; + box-sizing: border-box; + background: white; + border-radius: 18px; + box-shadow: 0 20px 60px rgba(15, 23, 42, 0.28); + transform: translate(-50%, -48%); + opacity: 0; + pointer-events: none; + transition: opacity 0.25s ease, transform 0.25s ease; + overflow-y: auto; + z-index: 50; +} + +.modal.open { + opacity: 1; + pointer-events: auto; + transform: translate(-50%, -50%); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 12px; +} + +.modal-header h3 { + margin: 0; +} + +.modal-content { + margin-top: 16px; + color: #334155; + text-align: left; +} + +.modal-content h4 { + margin: 16px 0 8px; + color: #1e293b; +} + +.modal-content p { + margin: 0; + line-height: 1.6; +} + @keyframes solverPulse { 0% { transform: scale(0.97); @@ -536,8 +714,18 @@ body { display: grid; } + .game-stats { + grid-template-columns: 1fr; + } + + .history-header, + .modal-header { + grid-template-columns: 1fr; + display: grid; + } + .solver-step-info, .solver-speed-value { justify-self: start; } -} \ No newline at end of file +} diff --git a/app/static/js/dashboard.js b/app/static/js/dashboard.js index ca8b30c..691af6e 100644 --- a/app/static/js/dashboard.js +++ b/app/static/js/dashboard.js @@ -3,11 +3,17 @@ 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; @@ -20,6 +26,9 @@ 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"); @@ -49,6 +58,9 @@ 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); @@ -62,6 +74,116 @@ 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"); @@ -187,26 +309,27 @@ function closeAllPanels() { toggleSidebar(false); toggleSavePanel(false); toggleSavePointsPanel(false); + toggleInfoModal(false); } -function getConflicts() { +function getConflicts(selectedBoard = board, selectedMode = mode) { const conflicts = []; for (let rowA = 0; rowA < boardSize; rowA++) { - if (board[rowA] === EMPTY_CELL) { + if (selectedBoard[rowA] === EMPTY_CELL) { continue; } for (let rowB = rowA + 1; rowB < boardSize; rowB++) { - if (board[rowB] === EMPTY_CELL) { + if (selectedBoard[rowB] === EMPTY_CELL) { continue; } - const colA = board[rowA]; - const colB = board[rowB]; + const colA = selectedBoard[rowA]; + const colB = selectedBoard[rowB]; const sameColumn = colA === colB; const sameDiagonal = Math.abs(colA - colB) === Math.abs(rowA - rowB); - const hasConflict = mode === "rooks" ? sameColumn : sameColumn || sameDiagonal; + const hasConflict = selectedMode === "rooks" ? sameColumn : sameColumn || sameDiagonal; if (hasConflict) { conflicts.push([rowA, colA], [rowB, colB]); @@ -217,6 +340,23 @@ function getConflicts() { 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); @@ -230,7 +370,8 @@ function checkSolution() { return; } - setStatus("Perfekt geloest.", "green"); + finishSolvedRun(); + setStatus(`Perfekt geloest in ${formatDuration(Math.floor(getElapsedMilliseconds() / 1000))}.`, "green"); } function drawBoard() { @@ -289,6 +430,8 @@ function placeQueen(row, col) { 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"); @@ -298,7 +441,16 @@ function placeQueen(row, col) { 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) { @@ -312,6 +464,7 @@ function applyBoardSize() { 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(); @@ -357,6 +510,14 @@ function toggleSavePointsPanel(forceOpen) { 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"; } @@ -423,6 +584,10 @@ function createSavePointActionButton(label, className, onClick) { return button; } +function getModeLabel(selectedMode) { + return selectedMode === "rooks" ? "Tuerme" : "Damen"; +} + function renderSavePoints(savePoints) { savePointsList.innerHTML = ""; @@ -455,6 +620,10 @@ function renderSavePoints(savePoints) { 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", @@ -467,6 +636,7 @@ function renderSavePoints(savePoints) { item.appendChild(createdAt); item.appendChild(updatedAt); item.appendChild(boardMeta); + item.appendChild(progressMeta); item.appendChild(preview); item.appendChild(note); item.appendChild(actions); @@ -474,6 +644,36 @@ function renderSavePoints(savePoints) { } } +function renderTimeHistory(savePoints) { + timeHistoryTableBody.innerHTML = ""; + + if (savePoints.length === 0) { + timeHistoryTableBody.innerHTML = '
Aktuelle Brettgroesse: 8 x 8
+Schritte
+0
+Zeit
+00:00:00
+Status
+Laeuft
+Die Tabelle zeigt gespeicherte Save-Points mit Zeit und Schritten.
+| Save-Point | +Modus | +Brett | +Schritte | +Zeit | +Status | +Gespeichert | +
|---|---|---|---|---|---|---|
| Noch keine gespeicherten Zeiten vorhanden. | +||||||