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
+189 -1
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,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;
}
}
}
+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();