added missing features, time counter, step counter, game instructions and table view for savepoints.
This commit is contained in:
@@ -7,6 +7,7 @@ from flask_sqlalchemy import SQLAlchemy
|
|||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
login_manager = LoginManager()
|
login_manager = LoginManager()
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from .. import db
|
from .. import db
|
||||||
|
|
||||||
|
|
||||||
class GameState(db.Model):
|
class GameState(db.Model):
|
||||||
__tablename__ = "game_state"
|
__tablename__ = "game_state"
|
||||||
|
|
||||||
@@ -12,6 +13,9 @@ class GameState(db.Model):
|
|||||||
save_name = db.Column(db.String(80), nullable=False, default="Unnamed Save")
|
save_name = db.Column(db.String(80), nullable=False, default="Unnamed Save")
|
||||||
save_note = db.Column(db.Text, nullable=False, default="")
|
save_note = db.Column(db.Text, nullable=False, default="")
|
||||||
is_favorite = db.Column(db.Boolean, nullable=False, default=False)
|
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())
|
created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now())
|
||||||
updated_at = db.Column(
|
updated_at = db.Column(
|
||||||
db.DateTime,
|
db.DateTime,
|
||||||
|
|||||||
@@ -28,6 +28,15 @@ def parse_favorite_flag(raw_value):
|
|||||||
return str(raw_value).lower() in {"1", "true", "yes", "on"}
|
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):
|
def get_position_label(row, col, board_size):
|
||||||
return f"{chr(65 + col)}{board_size - row}"
|
return f"{chr(65 + col)}{board_size - row}"
|
||||||
|
|
||||||
@@ -180,6 +189,9 @@ def serialize_game_state(game_state):
|
|||||||
"save_note": game_state.save_note,
|
"save_note": game_state.save_note,
|
||||||
"is_favorite": game_state.is_favorite,
|
"is_favorite": game_state.is_favorite,
|
||||||
"pieces_placed": sum(1 for column in board if column != -1),
|
"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"),
|
"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")
|
"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_name = (data.get("saveName") or "").strip() or f"{piece_mode.title()} Save"
|
||||||
save_note = (data.get("saveNote") or "").strip()
|
save_note = (data.get("saveNote") or "").strip()
|
||||||
is_favorite = bool(data.get("isFavorite"))
|
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()
|
timestamp = datetime.utcnow()
|
||||||
|
|
||||||
game_state = GameState(
|
game_state = GameState(
|
||||||
@@ -292,6 +307,9 @@ def save_game():
|
|||||||
save_name=save_name,
|
save_name=save_name,
|
||||||
save_note=save_note,
|
save_note=save_note,
|
||||||
is_favorite=is_favorite,
|
is_favorite=is_favorite,
|
||||||
|
step_count=step_count,
|
||||||
|
elapsed_seconds=elapsed_seconds,
|
||||||
|
is_solved=is_solved,
|
||||||
created_at=timestamp,
|
created_at=timestamp,
|
||||||
updated_at=timestamp
|
updated_at=timestamp
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -94,6 +94,68 @@ body {
|
|||||||
text-align: center;
|
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 {
|
#statusMessage {
|
||||||
min-height: 24px;
|
min-height: 24px;
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
@@ -478,6 +540,55 @@ body {
|
|||||||
margin: 0;
|
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 {
|
.btn.ghost {
|
||||||
background: #e2e8f0;
|
background: #e2e8f0;
|
||||||
color: #1e293b;
|
color: #1e293b;
|
||||||
@@ -502,6 +613,73 @@ body {
|
|||||||
pointer-events: auto;
|
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 {
|
@keyframes solverPulse {
|
||||||
0% {
|
0% {
|
||||||
transform: scale(0.97);
|
transform: scale(0.97);
|
||||||
@@ -536,6 +714,16 @@ body {
|
|||||||
display: grid;
|
display: grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.game-stats {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-header,
|
||||||
|
.modal-header {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
.solver-step-info,
|
.solver-step-info,
|
||||||
.solver-speed-value {
|
.solver-speed-value {
|
||||||
justify-self: start;
|
justify-self: start;
|
||||||
|
|||||||
+300
-18
@@ -3,11 +3,17 @@ const MIN_BOARD_SIZE = 4;
|
|||||||
const MAX_BOARD_SIZE = 13;
|
const MAX_BOARD_SIZE = 13;
|
||||||
const EMPTY_CELL = -1;
|
const EMPTY_CELL = -1;
|
||||||
const DEFAULT_SOLVER_MESSAGE = "Bereite einen Ablauf vor und spiele ihn Schritt fuer Schritt ab.";
|
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 mode = "queens";
|
||||||
let boardSize = DEFAULT_BOARD_SIZE;
|
let boardSize = DEFAULT_BOARD_SIZE;
|
||||||
let pendingBoardSize = DEFAULT_BOARD_SIZE;
|
let pendingBoardSize = DEFAULT_BOARD_SIZE;
|
||||||
let board = createEmptyBoard(boardSize);
|
let board = createEmptyBoard(boardSize);
|
||||||
|
let stepCount = 0;
|
||||||
|
let elapsedBeforeTimerStartMs = 0;
|
||||||
|
let timerStartedAtMs = Date.now();
|
||||||
|
let gameTimerId = null;
|
||||||
|
let isSolved = false;
|
||||||
|
|
||||||
let solverSteps = [];
|
let solverSteps = [];
|
||||||
let solverStepIndex = -1;
|
let solverStepIndex = -1;
|
||||||
@@ -20,6 +26,9 @@ let currentSolverHighlight = null;
|
|||||||
const boardDiv = document.getElementById("board");
|
const boardDiv = document.getElementById("board");
|
||||||
const statusMessage = document.getElementById("statusMessage");
|
const statusMessage = document.getElementById("statusMessage");
|
||||||
const queenList = document.getElementById("queenList");
|
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 btnQueens = document.getElementById("btnQueens");
|
||||||
const btnRooks = document.getElementById("btnRooks");
|
const btnRooks = document.getElementById("btnRooks");
|
||||||
const btnPrepareSolve = document.getElementById("btnPrepareSolve");
|
const btnPrepareSolve = document.getElementById("btnPrepareSolve");
|
||||||
@@ -49,6 +58,9 @@ const savePointModeFilter = document.getElementById("savePointModeFilter");
|
|||||||
const savePointSizeFilter = document.getElementById("savePointSizeFilter");
|
const savePointSizeFilter = document.getElementById("savePointSizeFilter");
|
||||||
const savePointSortFilter = document.getElementById("savePointSortFilter");
|
const savePointSortFilter = document.getElementById("savePointSortFilter");
|
||||||
const savePointFavoritesOnly = document.getElementById("savePointFavoritesOnly");
|
const savePointFavoritesOnly = document.getElementById("savePointFavoritesOnly");
|
||||||
|
const timeHistoryTableBody = document.getElementById("timeHistoryTableBody");
|
||||||
|
const infoModal = document.getElementById("infoModal");
|
||||||
|
const infoModalBackdrop = document.getElementById("infoModalBackdrop");
|
||||||
|
|
||||||
function createEmptyBoard(size) {
|
function createEmptyBoard(size) {
|
||||||
return Array(size).fill(EMPTY_CELL);
|
return Array(size).fill(EMPTY_CELL);
|
||||||
@@ -62,6 +74,116 @@ function getColumnLabel(index) {
|
|||||||
return String.fromCharCode(65 + 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() {
|
function updateModeButtons() {
|
||||||
btnQueens.classList.toggle("active", mode === "queens");
|
btnQueens.classList.toggle("active", mode === "queens");
|
||||||
btnRooks.classList.toggle("active", mode === "rooks");
|
btnRooks.classList.toggle("active", mode === "rooks");
|
||||||
@@ -187,26 +309,27 @@ function closeAllPanels() {
|
|||||||
toggleSidebar(false);
|
toggleSidebar(false);
|
||||||
toggleSavePanel(false);
|
toggleSavePanel(false);
|
||||||
toggleSavePointsPanel(false);
|
toggleSavePointsPanel(false);
|
||||||
|
toggleInfoModal(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConflicts() {
|
function getConflicts(selectedBoard = board, selectedMode = mode) {
|
||||||
const conflicts = [];
|
const conflicts = [];
|
||||||
|
|
||||||
for (let rowA = 0; rowA < boardSize; rowA++) {
|
for (let rowA = 0; rowA < boardSize; rowA++) {
|
||||||
if (board[rowA] === EMPTY_CELL) {
|
if (selectedBoard[rowA] === EMPTY_CELL) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let rowB = rowA + 1; rowB < boardSize; rowB++) {
|
for (let rowB = rowA + 1; rowB < boardSize; rowB++) {
|
||||||
if (board[rowB] === EMPTY_CELL) {
|
if (selectedBoard[rowB] === EMPTY_CELL) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const colA = board[rowA];
|
const colA = selectedBoard[rowA];
|
||||||
const colB = board[rowB];
|
const colB = selectedBoard[rowB];
|
||||||
const sameColumn = colA === colB;
|
const sameColumn = colA === colB;
|
||||||
const sameDiagonal = Math.abs(colA - colB) === Math.abs(rowA - rowB);
|
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) {
|
if (hasConflict) {
|
||||||
conflicts.push([rowA, colA], [rowB, colB]);
|
conflicts.push([rowA, colA], [rowB, colB]);
|
||||||
@@ -217,6 +340,23 @@ function getConflicts() {
|
|||||||
return conflicts;
|
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() {
|
function checkSolution() {
|
||||||
const allPlaced = board.every((col) => col !== EMPTY_CELL);
|
const allPlaced = board.every((col) => col !== EMPTY_CELL);
|
||||||
|
|
||||||
@@ -230,7 +370,8 @@ function checkSolution() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatus("Perfekt geloest.", "green");
|
finishSolvedRun();
|
||||||
|
setStatus(`Perfekt geloest in ${formatDuration(Math.floor(getElapsedMilliseconds() / 1000))}.`, "green");
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawBoard() {
|
function drawBoard() {
|
||||||
@@ -289,6 +430,8 @@ function placeQueen(row, col) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensureFreshRunAfterSolvedBoard();
|
||||||
|
|
||||||
if (hasPreparedSolverTrace() || isSolverPlaying()) {
|
if (hasPreparedSolverTrace() || isSolverPlaying()) {
|
||||||
discardSolverTrace("Solver-Ablauf verworfen, weil das Brett manuell geaendert wurde.");
|
discardSolverTrace("Solver-Ablauf verworfen, weil das Brett manuell geaendert wurde.");
|
||||||
setStatus("Solver-Ablauf verworfen. Bereite ihn bei Bedarf neu vor.", "#d97706");
|
setStatus("Solver-Ablauf verworfen. Bereite ihn bei Bedarf neu vor.", "#d97706");
|
||||||
@@ -298,7 +441,16 @@ function placeQueen(row, col) {
|
|||||||
|
|
||||||
currentSolverHighlight = null;
|
currentSolverHighlight = null;
|
||||||
board[row] = board[row] === col ? EMPTY_CELL : col;
|
board[row] = board[row] === col ? EMPTY_CELL : col;
|
||||||
|
stepCount += 1;
|
||||||
drawBoard();
|
drawBoard();
|
||||||
|
updateProgressDisplay();
|
||||||
|
|
||||||
|
if (isBoardSolved()) {
|
||||||
|
finishSolvedRun();
|
||||||
|
setStatus(`Perfekt geloest in ${formatDuration(Math.floor(getElapsedMilliseconds() / 1000))}.`, "green");
|
||||||
|
} else {
|
||||||
|
persistCurrentSession();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function previewBoardSize(size) {
|
function previewBoardSize(size) {
|
||||||
@@ -312,6 +464,7 @@ function applyBoardSize() {
|
|||||||
discardSolverTrace(`${getPieceLabel(mode)}-Solver bereit. ${DEFAULT_SOLVER_MESSAGE}`);
|
discardSolverTrace(`${getPieceLabel(mode)}-Solver bereit. ${DEFAULT_SOLVER_MESSAGE}`);
|
||||||
board = createEmptyBoard(boardSize);
|
board = createEmptyBoard(boardSize);
|
||||||
currentSolverHighlight = null;
|
currentSolverHighlight = null;
|
||||||
|
resetProgress();
|
||||||
updateBoardSizeDisplays(boardSize);
|
updateBoardSizeDisplays(boardSize);
|
||||||
setStatus(`Brettgroesse auf ${boardSize} x ${boardSize} gesetzt.`, "#2563eb");
|
setStatus(`Brettgroesse auf ${boardSize} x ${boardSize} gesetzt.`, "#2563eb");
|
||||||
drawBoard();
|
drawBoard();
|
||||||
@@ -357,6 +510,14 @@ function toggleSavePointsPanel(forceOpen) {
|
|||||||
savePointsSidebar.setAttribute("aria-hidden", String(!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) {
|
function getPieceLabel(selectedMode) {
|
||||||
return selectedMode === "queens" ? "Dame" : "Turm";
|
return selectedMode === "queens" ? "Dame" : "Turm";
|
||||||
}
|
}
|
||||||
@@ -423,6 +584,10 @@ function createSavePointActionButton(label, className, onClick) {
|
|||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getModeLabel(selectedMode) {
|
||||||
|
return selectedMode === "rooks" ? "Tuerme" : "Damen";
|
||||||
|
}
|
||||||
|
|
||||||
function renderSavePoints(savePoints) {
|
function renderSavePoints(savePoints) {
|
||||||
savePointsList.innerHTML = "";
|
savePointsList.innerHTML = "";
|
||||||
|
|
||||||
@@ -455,6 +620,10 @@ function renderSavePoints(savePoints) {
|
|||||||
note.textContent = savePoint.save_note ? `Notiz: ${savePoint.save_note}` : "Notiz: Keine Notiz";
|
note.textContent = savePoint.save_note ? `Notiz: ${savePoint.save_note}` : "Notiz: Keine Notiz";
|
||||||
actions.className = "save-point-actions";
|
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("Laden", "btn", () => loadSavePoint(savePoint.id)));
|
||||||
actions.appendChild(createSavePointActionButton(
|
actions.appendChild(createSavePointActionButton(
|
||||||
savePoint.is_favorite ? "Favorit entfernen" : "Als Favorit",
|
savePoint.is_favorite ? "Favorit entfernen" : "Als Favorit",
|
||||||
@@ -467,6 +636,7 @@ function renderSavePoints(savePoints) {
|
|||||||
item.appendChild(createdAt);
|
item.appendChild(createdAt);
|
||||||
item.appendChild(updatedAt);
|
item.appendChild(updatedAt);
|
||||||
item.appendChild(boardMeta);
|
item.appendChild(boardMeta);
|
||||||
|
item.appendChild(progressMeta);
|
||||||
item.appendChild(preview);
|
item.appendChild(preview);
|
||||||
item.appendChild(note);
|
item.appendChild(note);
|
||||||
item.appendChild(actions);
|
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() {
|
async function refreshSavePoints() {
|
||||||
const queryString = getSavePointFilters();
|
const queryString = getSavePointFilters();
|
||||||
const response = await fetch(`/save_points${queryString ? `?${queryString}` : ""}`);
|
const response = await fetch(`/save_points${queryString ? `?${queryString}` : ""}`);
|
||||||
@@ -481,6 +681,17 @@ async function refreshSavePoints() {
|
|||||||
renderSavePoints(savePoints);
|
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() {
|
async function openSavePointsPanel() {
|
||||||
await refreshSavePoints();
|
await refreshSavePoints();
|
||||||
toggleSavePointsPanel(true);
|
toggleSavePointsPanel(true);
|
||||||
@@ -489,19 +700,28 @@ async function openSavePointsPanel() {
|
|||||||
function applyLoadedGame(data) {
|
function applyLoadedGame(data) {
|
||||||
discardSolverTrace(`${getPieceLabel(data.mode || "queens")}-Solver bereit. ${DEFAULT_SOLVER_MESSAGE}`);
|
discardSolverTrace(`${getPieceLabel(data.mode || "queens")}-Solver bereit. ${DEFAULT_SOLVER_MESSAGE}`);
|
||||||
|
|
||||||
board = Array.isArray(data.board) ? data.board.slice() : createEmptyBoard(DEFAULT_BOARD_SIZE);
|
const restoredBoardSize = Array.isArray(data.board) ? clampBoardSize(data.board.length) : DEFAULT_BOARD_SIZE;
|
||||||
boardSize = Array.isArray(data.board) ? clampBoardSize(data.board.length) : DEFAULT_BOARD_SIZE;
|
|
||||||
|
boardSize = restoredBoardSize;
|
||||||
pendingBoardSize = boardSize;
|
pendingBoardSize = boardSize;
|
||||||
mode = data.mode === "rooks" ? "rooks" : "queens";
|
mode = data.mode === "rooks" ? "rooks" : "queens";
|
||||||
|
board = sanitizeBoard(data.board, boardSize);
|
||||||
currentSolverHighlight = null;
|
currentSolverHighlight = null;
|
||||||
|
resetProgress({
|
||||||
|
stepCountValue: data.step_count,
|
||||||
|
elapsedMs: Number(data.elapsed_seconds || 0) * 1000,
|
||||||
|
solved: data.is_solved || isBoardSolved(board, mode)
|
||||||
|
});
|
||||||
|
|
||||||
updateModeButtons();
|
updateModeButtons();
|
||||||
updateBoardSizeDisplays(boardSize);
|
updateBoardSizeDisplays(boardSize);
|
||||||
drawBoard();
|
drawBoard();
|
||||||
|
updateProgressDisplay();
|
||||||
|
persistCurrentSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
function applySolverStep(step) {
|
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)) {
|
if (Number.isInteger(step.row) && Number.isInteger(step.col)) {
|
||||||
currentSolverHighlight = {
|
currentSolverHighlight = {
|
||||||
@@ -524,6 +744,7 @@ function applySolverStep(step) {
|
|||||||
|
|
||||||
setSolverMessage(step.message || DEFAULT_SOLVER_MESSAGE);
|
setSolverMessage(step.message || DEFAULT_SOLVER_MESSAGE);
|
||||||
drawBoard();
|
drawBoard();
|
||||||
|
persistCurrentSession();
|
||||||
updateSolverControls();
|
updateSolverControls();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -538,6 +759,7 @@ function advanceSolverStep() {
|
|||||||
applySolverStep(solverSteps[solverStepIndex]);
|
applySolverStep(solverSteps[solverStepIndex]);
|
||||||
|
|
||||||
if (hasCompletedSolverTrace()) {
|
if (hasCompletedSolverTrace()) {
|
||||||
|
finishSolvedRun();
|
||||||
stopSolverPlayback(false);
|
stopSolverPlayback(false);
|
||||||
setStatus(`Loesung fuer ${boardSize} x ${boardSize} Schritt fuer Schritt aufgebaut.`, "green");
|
setStatus(`Loesung fuer ${boardSize} x ${boardSize} Schritt fuer Schritt aufgebaut.`, "green");
|
||||||
updateSolverControls();
|
updateSolverControls();
|
||||||
@@ -618,6 +840,7 @@ async function prepareSolutionTrace() {
|
|||||||
setSolverMessage("Keine Loesung gefunden.");
|
setSolverMessage("Keine Loesung gefunden.");
|
||||||
setStatus("Keine Loesung fuer dieses Brett gefunden.", "red");
|
setStatus("Keine Loesung fuer dieses Brett gefunden.", "red");
|
||||||
drawBoard();
|
drawBoard();
|
||||||
|
persistCurrentSession();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -730,9 +953,11 @@ async function instantSolve() {
|
|||||||
|
|
||||||
board = mode === "queens" && Array.isArray(solution[0]) ? solution[0].slice() : solution.slice();
|
board = mode === "queens" && Array.isArray(solution[0]) ? solution[0].slice() : solution.slice();
|
||||||
currentSolverHighlight = null;
|
currentSolverHighlight = null;
|
||||||
|
finishSolvedRun();
|
||||||
setSolverMessage("Sofortige Loesung geladen. Starte Solve Step-by-Step, wenn du den Ablauf sehen moechtest.");
|
setSolverMessage("Sofortige Loesung geladen. Starte Solve Step-by-Step, wenn du den Ablauf sehen moechtest.");
|
||||||
setStatus(`Loesung fuer ${boardSize} x ${boardSize} sofort geladen.`, "green");
|
setStatus(`Loesung fuer ${boardSize} x ${boardSize} sofort geladen.`, "green");
|
||||||
drawBoard();
|
drawBoard();
|
||||||
|
persistCurrentSession();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (requestId !== solverRequestId) {
|
if (requestId !== solverRequestId) {
|
||||||
return;
|
return;
|
||||||
@@ -758,6 +983,7 @@ function resetBoard(options = {}) {
|
|||||||
discardSolverTrace(solverMessageText);
|
discardSolverTrace(solverMessageText);
|
||||||
board = createEmptyBoard(boardSize);
|
board = createEmptyBoard(boardSize);
|
||||||
currentSolverHighlight = null;
|
currentSolverHighlight = null;
|
||||||
|
resetProgress();
|
||||||
|
|
||||||
if (statusMessageText) {
|
if (statusMessageText) {
|
||||||
setStatus(statusMessageText, statusColor);
|
setStatus(statusMessageText, statusColor);
|
||||||
@@ -800,7 +1026,10 @@ async function saveGame() {
|
|||||||
boardSize,
|
boardSize,
|
||||||
saveName: saveNameInput.value,
|
saveName: saveNameInput.value,
|
||||||
saveNote: saveNoteInput.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);
|
toggleSavePanel(false);
|
||||||
await refreshSavePoints();
|
await refreshSavePoints();
|
||||||
|
await refreshTimeHistory();
|
||||||
setStatus(`Speicherpunkt "${data.save_point.save_name}" gespeichert.`, "green");
|
setStatus(`Speicherpunkt "${data.save_point.save_name}" gespeichert.`, "green");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -848,6 +1078,7 @@ async function toggleSavePointFavorite(savePointId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await refreshSavePoints();
|
await refreshSavePoints();
|
||||||
|
await refreshTimeHistory();
|
||||||
setStatus("Favoritenstatus aktualisiert.", "#2563eb");
|
setStatus("Favoritenstatus aktualisiert.", "#2563eb");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -868,13 +1099,64 @@ async function deleteSavePoint(savePointId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await refreshSavePoints();
|
await refreshSavePoints();
|
||||||
|
await refreshTimeHistory();
|
||||||
setStatus("Speicherpunkt geloescht.", "#2563eb");
|
setStatus("Speicherpunkt geloescht.", "#2563eb");
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingBoardSize = boardSize;
|
function restoreSessionFromStorage() {
|
||||||
updateModeButtons();
|
try {
|
||||||
updateBoardSizeDisplays(boardSize);
|
const rawSession = window.localStorage.getItem(GAME_STORAGE_KEY);
|
||||||
updateSolverSpeed(solverSpeedMs);
|
|
||||||
setSolverMessage(`${getPieceLabel(mode)}-Solver bereit. ${DEFAULT_SOLVER_MESSAGE}`);
|
if (!rawSession) {
|
||||||
updateSolverControls();
|
return false;
|
||||||
drawBoard();
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|||||||
@@ -59,10 +59,29 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card game-card">
|
<div class="card game-card">
|
||||||
<h2>♟️ Damen- und Tuerme-Spiel</h2>
|
<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
|
<p class="board-size-info">Aktuelle Brettgroesse: <span id="boardSizeValue">8</span> x <span
|
||||||
id="boardSizeValueMirror">8</span></p>
|
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>
|
<p id="statusMessage"></p>
|
||||||
<div class="board" id="board"></div>
|
<div class="board" id="board"></div>
|
||||||
|
|
||||||
@@ -103,6 +122,37 @@
|
|||||||
<h3>📋 Figuren Positionen</h3>
|
<h3>📋 Figuren Positionen</h3>
|
||||||
<ul id="queenList"></ul>
|
<ul id="queenList"></ul>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<aside class="sidebar" id="settingsSidebar" aria-hidden="true">
|
<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="saveBackdrop" onclick="toggleSavePanel(false)"></div>
|
||||||
<div class="sidebar-backdrop" id="savePointsBackdrop" onclick="toggleSavePointsPanel(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>
|
<script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user