added missing features, time counter, step counter, game instructions and table view for savepoints.

This commit is contained in:
2026-04-04 23:15:36 +02:00
parent 21fe673ab4
commit 59b48683a8
6 changed files with 589 additions and 20 deletions
+1
View File
@@ -7,6 +7,7 @@ from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
login_manager = LoginManager()
def create_app():
app = Flask(__name__)
+4
View File
@@ -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,
+18
View File
@@ -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
)
+188
View File
@@ -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,6 +714,16 @@ 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;
+300 -18
View File
@@ -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 = '<tr><td colspan="7" class="history-empty">Noch keine gespeicherten Zeiten vorhanden.</td></tr>';
return;
}
for (const savePoint of savePoints) {
const row = document.createElement("tr");
const values = [
`${savePoint.is_favorite ? "★ " : ""}${savePoint.save_name}`,
getModeLabel(savePoint.mode),
`${savePoint.board_size} x ${savePoint.board_size}`,
String(savePoint.step_count || 0),
formatDuration(savePoint.elapsed_seconds),
savePoint.is_solved ? "Geloest" : "Offen",
savePoint.updated_at
];
for (const value of values) {
const cell = document.createElement("td");
cell.textContent = value;
row.appendChild(cell);
}
timeHistoryTableBody.appendChild(row);
}
}
async function refreshSavePoints() {
const queryString = getSavePointFilters();
const response = await fetch(`/save_points${queryString ? `?${queryString}` : ""}`);
@@ -481,6 +681,17 @@ async function refreshSavePoints() {
renderSavePoints(savePoints);
}
async function refreshTimeHistory() {
const response = await fetch("/save_points?sort=updated");
if (!response.ok) {
return;
}
const savePoints = await response.json();
renderTimeHistory(savePoints);
}
async function openSavePointsPanel() {
await refreshSavePoints();
toggleSavePointsPanel(true);
@@ -489,19 +700,28 @@ async function openSavePointsPanel() {
function applyLoadedGame(data) {
discardSolverTrace(`${getPieceLabel(data.mode || "queens")}-Solver bereit. ${DEFAULT_SOLVER_MESSAGE}`);
board = Array.isArray(data.board) ? data.board.slice() : createEmptyBoard(DEFAULT_BOARD_SIZE);
boardSize = Array.isArray(data.board) ? clampBoardSize(data.board.length) : DEFAULT_BOARD_SIZE;
const restoredBoardSize = Array.isArray(data.board) ? clampBoardSize(data.board.length) : DEFAULT_BOARD_SIZE;
boardSize = restoredBoardSize;
pendingBoardSize = boardSize;
mode = data.mode === "rooks" ? "rooks" : "queens";
board = sanitizeBoard(data.board, boardSize);
currentSolverHighlight = null;
resetProgress({
stepCountValue: data.step_count,
elapsedMs: Number(data.elapsed_seconds || 0) * 1000,
solved: data.is_solved || isBoardSolved(board, mode)
});
updateModeButtons();
updateBoardSizeDisplays(boardSize);
drawBoard();
updateProgressDisplay();
persistCurrentSession();
}
function applySolverStep(step) {
board = Array.isArray(step.board) ? step.board.slice() : createEmptyBoard(boardSize);
board = sanitizeBoard(step.board, boardSize);
if (Number.isInteger(step.row) && Number.isInteger(step.col)) {
currentSolverHighlight = {
@@ -524,6 +744,7 @@ function applySolverStep(step) {
setSolverMessage(step.message || DEFAULT_SOLVER_MESSAGE);
drawBoard();
persistCurrentSession();
updateSolverControls();
}
@@ -538,6 +759,7 @@ function advanceSolverStep() {
applySolverStep(solverSteps[solverStepIndex]);
if (hasCompletedSolverTrace()) {
finishSolvedRun();
stopSolverPlayback(false);
setStatus(`Loesung fuer ${boardSize} x ${boardSize} Schritt fuer Schritt aufgebaut.`, "green");
updateSolverControls();
@@ -618,6 +840,7 @@ async function prepareSolutionTrace() {
setSolverMessage("Keine Loesung gefunden.");
setStatus("Keine Loesung fuer dieses Brett gefunden.", "red");
drawBoard();
persistCurrentSession();
return false;
}
@@ -730,9 +953,11 @@ async function instantSolve() {
board = mode === "queens" && Array.isArray(solution[0]) ? solution[0].slice() : solution.slice();
currentSolverHighlight = null;
finishSolvedRun();
setSolverMessage("Sofortige Loesung geladen. Starte Solve Step-by-Step, wenn du den Ablauf sehen moechtest.");
setStatus(`Loesung fuer ${boardSize} x ${boardSize} sofort geladen.`, "green");
drawBoard();
persistCurrentSession();
} catch (error) {
if (requestId !== solverRequestId) {
return;
@@ -758,6 +983,7 @@ function resetBoard(options = {}) {
discardSolverTrace(solverMessageText);
board = createEmptyBoard(boardSize);
currentSolverHighlight = null;
resetProgress();
if (statusMessageText) {
setStatus(statusMessageText, statusColor);
@@ -800,7 +1026,10 @@ async function saveGame() {
boardSize,
saveName: saveNameInput.value,
saveNote: saveNoteInput.value,
isFavorite: saveFavoriteInput.checked
isFavorite: saveFavoriteInput.checked,
stepCount,
elapsedSeconds: Math.floor(getElapsedMilliseconds() / 1000),
isSolved
})
});
@@ -813,6 +1042,7 @@ async function saveGame() {
toggleSavePanel(false);
await refreshSavePoints();
await refreshTimeHistory();
setStatus(`Speicherpunkt "${data.save_point.save_name}" gespeichert.`, "green");
}
@@ -848,6 +1078,7 @@ async function toggleSavePointFavorite(savePointId) {
}
await refreshSavePoints();
await refreshTimeHistory();
setStatus("Favoritenstatus aktualisiert.", "#2563eb");
}
@@ -868,13 +1099,64 @@ async function deleteSavePoint(savePointId) {
}
await refreshSavePoints();
await refreshTimeHistory();
setStatus("Speicherpunkt geloescht.", "#2563eb");
}
pendingBoardSize = boardSize;
updateModeButtons();
updateBoardSizeDisplays(boardSize);
updateSolverSpeed(solverSpeedMs);
setSolverMessage(`${getPieceLabel(mode)}-Solver bereit. ${DEFAULT_SOLVER_MESSAGE}`);
updateSolverControls();
drawBoard();
function restoreSessionFromStorage() {
try {
const rawSession = window.localStorage.getItem(GAME_STORAGE_KEY);
if (!rawSession) {
return false;
}
const savedSession = JSON.parse(rawSession);
const restoredBoardSize = clampBoardSize(savedSession.boardSize || DEFAULT_BOARD_SIZE);
const restoredMode = savedSession.mode === "rooks" ? "rooks" : "queens";
boardSize = restoredBoardSize;
pendingBoardSize = restoredBoardSize;
mode = restoredMode;
board = sanitizeBoard(savedSession.board, restoredBoardSize);
currentSolverHighlight = null;
updateModeButtons();
updateBoardSizeDisplays(boardSize);
drawBoard();
resetProgress({
stepCountValue: savedSession.stepCount,
elapsedMs: savedSession.elapsedMs,
solved: savedSession.isSolved || isBoardSolved(board, mode)
});
return true;
} catch (error) {
return false;
}
}
async function initializeDashboard() {
pendingBoardSize = boardSize;
updateModeButtons();
updateBoardSizeDisplays(boardSize);
updateSolverSpeed(solverSpeedMs);
setSolverMessage(`${getPieceLabel(mode)}-Solver bereit. ${DEFAULT_SOLVER_MESSAGE}`);
updateSolverControls();
if (!restoreSessionFromStorage()) {
resetProgress();
drawBoard();
}
updateProgressDisplay();
await refreshTimeHistory();
}
window.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
closeAllPanels();
}
});
initializeDashboard();
+76
View File
@@ -59,10 +59,29 @@
</div>
<div class="card game-card">
<div class="game-card-header">
<h2>♟️ Damen- und Tuerme-Spiel</h2>
<button class="info-button" id="infoButton" onclick="toggleInfoModal(true)" type="button"
aria-label="Spielanleitung und Erklaerung anzeigen">i</button>
</div>
<p class="board-size-info">Aktuelle Brettgroesse: <span id="boardSizeValue">8</span> x <span
id="boardSizeValueMirror">8</span></p>
<div class="game-stats" aria-label="Spielstatus">
<article class="game-stat-card">
<p class="game-stat-label">Schritte</p>
<p class="game-stat-value" id="stepCounterValue">0</p>
</article>
<article class="game-stat-card">
<p class="game-stat-label">Zeit</p>
<p class="game-stat-value" id="timerValue">00:00:00</p>
</article>
<article class="game-stat-card">
<p class="game-stat-label">Status</p>
<p class="game-stat-value game-stat-status" id="gameProgressState">Laeuft</p>
</article>
</div>
<p id="statusMessage"></p>
<div class="board" id="board"></div>
@@ -103,6 +122,37 @@
<h3>📋 Figuren Positionen</h3>
<ul id="queenList"></ul>
</div>
<div class="card">
<div class="history-header">
<div>
<h3>⏱️ Bisherige Zeiten</h3>
<p class="history-subtitle">Die Tabelle zeigt gespeicherte Save-Points mit Zeit und Schritten.</p>
</div>
<button class="btn secondary" onclick="refreshTimeHistory()" type="button">Aktualisieren</button>
</div>
<div class="history-table-wrapper">
<table class="history-table">
<thead>
<tr>
<th>Save-Point</th>
<th>Modus</th>
<th>Brett</th>
<th>Schritte</th>
<th>Zeit</th>
<th>Status</th>
<th>Gespeichert</th>
</tr>
</thead>
<tbody id="timeHistoryTableBody">
<tr>
<td colspan="7" class="history-empty">Noch keine gespeicherten Zeiten vorhanden.</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<aside class="sidebar" id="settingsSidebar" aria-hidden="true">
@@ -193,6 +243,32 @@
<div class="sidebar-backdrop" id="saveBackdrop" onclick="toggleSavePanel(false)"></div>
<div class="sidebar-backdrop" id="savePointsBackdrop" onclick="toggleSavePointsPanel(false)"></div>
<div class="modal-backdrop" id="infoModalBackdrop" onclick="toggleInfoModal(false)"></div>
<section class="modal" id="infoModal" aria-hidden="true" aria-labelledby="infoModalTitle">
<div class="modal-header">
<h3 id="infoModalTitle">Spielanleitung und Acht-Damen-Problem</h3>
<button class="btn secondary sidebar-close" onclick="toggleInfoModal(false)" type="button">Schliessen</button>
</div>
<div class="modal-content">
<p>Setze pro Reihe genau eine Figur auf das Brett. Eine gueltige Loesung hat keine Konflikte.</p>
<h4>So funktioniert das Spiel</h4>
<p>Im Damen-Modus duerfen sich Damen weder in derselben Spalte noch diagonal angreifen. Im Tuerme-Modus
zaehlen nur Spalten-Konflikte. Mit einem Klick setzt oder entfernst du eine Figur.</p>
<h4>Was ist das Acht-Damen-Problem?</h4>
<p>Das klassische Acht-Damen-Problem fragt nach einer Anordnung von acht Damen auf einem 8x8-Brett, bei
der sich keine zwei Damen schlagen. Es ist ein bekanntes Beispiel fuer kombinatorische Suche und
Backtracking.</p>
<h4>Was zeigen Zeit und Schritte?</h4>
<p>Der Schrittzaehler erfasst deine Zuege auf dem Brett. Die Zeit laeuft fuer das aktuelle Spiel weiter,
auch wenn du die Seite neu laedst. Beim Speichern werden Zeit und Schritte zusammen mit dem Save-Point
abgelegt.</p>
</div>
</section>
<script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
</body>