diff --git a/app/routes/__pycache__/main_routes.cpython-311.pyc b/app/routes/__pycache__/main_routes.cpython-311.pyc index c3c7267..c5a0bbf 100644 Binary files a/app/routes/__pycache__/main_routes.cpython-311.pyc and b/app/routes/__pycache__/main_routes.cpython-311.pyc differ diff --git a/app/routes/__pycache__/main_routes.cpython-312.pyc b/app/routes/__pycache__/main_routes.cpython-312.pyc new file mode 100644 index 0000000..7282671 Binary files /dev/null and b/app/routes/__pycache__/main_routes.cpython-312.pyc differ diff --git a/app/routes/main_routes.py b/app/routes/main_routes.py index 5222b6f..cf8405c 100644 --- a/app/routes/main_routes.py +++ b/app/routes/main_routes.py @@ -1,20 +1,88 @@ +import json +import random + from flask import Blueprint, jsonify, render_template, request -from flask_login import login_required, current_user -import random, json -from ..models.gamestate import GameState -from ..models.user import User +from flask_login import current_user, login_required + from .. import db +from ..models.gamestate import GameState + +DEFAULT_BOARD_SIZE = 8 +MIN_BOARD_SIZE = 4 +MAX_BOARD_SIZE = 13 main_bp = Blueprint("main", __name__) + +def parse_board_size(raw_size, default=DEFAULT_BOARD_SIZE): + try: + board_size = int(raw_size) + except (TypeError, ValueError): + board_size = default + + return max(MIN_BOARD_SIZE, min(MAX_BOARD_SIZE, board_size)) + + +def solve_queens(board_size): + queen_positions = [-1] * board_size + queen_solutions = [] + + def is_queen_position_safe(row, col): + for previous_row in range(row): + previous_col = queen_positions[previous_row] + + if previous_col == col: + return False + + if abs(previous_col - col) == abs(previous_row - row): + return False + + return True + + def place_queen(row): + if row == board_size: + queen_solutions.append(queen_positions[:]) + return + + for col in range(board_size): + if is_queen_position_safe(row, col): + queen_positions[row] = col + place_queen(row + 1) + queen_positions[row] = -1 + + place_queen(0) + return queen_solutions + + +def solve_rooks(board_size): + rook_positions = list(range(board_size)) + random.shuffle(rook_positions) + return rook_positions + + +def serialize_game_state(game_state): + board = json.loads(game_state.board) + + return { + "id": game_state.id, + "board": board, + "mode": game_state.mode, + "board_size": len(board), + "pieces_placed": sum(1 for column in board if column != -1), + "created_at": game_state.created_at.strftime("%Y-%m-%d %H:%M:%S") + } + + @main_bp.route("/") def home(): return render_template("index.html") -@main_bp.route('/dashboard') + +@main_bp.route("/dashboard") @login_required def dashboard(): - return render_template('dashboard.html', user=current_user) + return render_template("dashboard.html", user=current_user) + @main_bp.route("/check", methods=["POST"]) def check(): @@ -22,71 +90,48 @@ def check(): board = data["board"] row = data["row"] col = data["col"] + piece_mode = data.get("mode", "queens") - for r in range(len(board)): - c = board[r] - - if c == -1 or r == row: + for current_row, current_col in enumerate(board): + if current_col == -1 or current_row == row: continue - # gleiche Spalte - if c == col: + same_column = current_col == col + same_diagonal = abs(current_col - col) == abs(current_row - row) + + if same_column: return jsonify({"valid": False}) - # gleiche Diagonale - if abs(c - col) == abs(r - row): + if piece_mode == "queens" and same_diagonal: return jsonify({"valid": False}) return jsonify({"valid": True}) @main_bp.route("/solve") -def solve(): - n = 8 - solutions = [] - board = [-1] * n +def solve_queens_route(): + board_size = parse_board_size(request.args.get("size")) + return jsonify(solve_queens(board_size)) - def is_safe(row, col): - for r in range(row): - c = board[r] - if c == col or abs(c - col) == abs(r - row): - return False - return True - - def backtrack(row): - if row == n: - solutions.append(board[:]) - return - - for col in range(n): - if is_safe(row, col): - board[row] = col - backtrack(row + 1) - board[row] = -1 - - backtrack(0) - return jsonify(solutions) @main_bp.route("/solve_rooks") def solve_rooks_route(): - return jsonify(solve_rooks(8)) + board_size = parse_board_size(request.args.get("size")) + return jsonify(solve_rooks(board_size)) -def solve_rooks(n=8): - solution = list(range(n)) - random.shuffle(solution) - return solution @main_bp.route("/save", methods=["POST"]) +@login_required def save_game(): data = request.json board = data["board"] - mode = data["mode"] + piece_mode = data["mode"] game = GameState( user_id=current_user.id, board=json.dumps(board), - mode=mode + mode=piece_mode ) db.session.add(game) @@ -94,15 +139,43 @@ def save_game(): return jsonify({"status": "saved"}) -@main_bp.route("/load") -def load_game(): - game = GameState.query.filter_by(user_id=current_user.id)\ - .order_by(GameState.id.desc()).first() - if not game: - return jsonify({"board": [-1]*8, "mode": "queens"}) +@main_bp.route("/save_points") +@login_required +def list_save_points(): + saved_games = ( + GameState.query.filter_by(user_id=current_user.id) + .order_by(GameState.id.desc()) + .all() + ) + + return jsonify([serialize_game_state(game_state) for game_state in saved_games]) + + +@main_bp.route("/load") +@login_required +def load_game(): + latest_game = ( + GameState.query.filter_by(user_id=current_user.id) + .order_by(GameState.id.desc()) + .first() + ) + + if not latest_game: + return jsonify({"board": [-1] * DEFAULT_BOARD_SIZE, "mode": "queens"}) return jsonify({ - "board": json.loads(game.board), - "mode": game.mode + "board": json.loads(latest_game.board), + "mode": latest_game.mode }) + + +@main_bp.route("/load/") +@login_required +def load_specific_game(game_id): + selected_game = GameState.query.filter_by(id=game_id, user_id=current_user.id).first() + + if not selected_game: + return jsonify({"error": "Save point not found"}), 404 + + return jsonify(serialize_game_state(selected_game)) diff --git a/app/static/css/dashboard.css b/app/static/css/dashboard.css index 19f7438..ee0e1b3 100644 --- a/app/static/css/dashboard.css +++ b/app/static/css/dashboard.css @@ -44,16 +44,24 @@ body { text-align: center; } +.board { + overflow-x: auto; + padding-bottom: 8px; +} + /* ♟️ Board */ .board-grid { + --label-size: 30px; + --cell-size: 60px; display: grid; - grid-template-columns: 30px repeat(8, 60px); - grid-template-rows: 30px repeat(8, 60px); + grid-template-columns: var(--label-size) repeat(var(--board-size, 8), var(--cell-size)); + grid-template-rows: var(--label-size) repeat(var(--board-size, 8), var(--cell-size)); margin: 20px auto; width: max-content; border: 6px solid #1e293b; border-radius: 10px; overflow: hidden; + max-width: 100%; } .label { @@ -63,17 +71,18 @@ body { font-weight: bold; background: #aec8eb; color: #1e293b; + width: var(--label-size); + height: var(--label-size); + font-size: 14px; } - - .cell { - width: 60px; - height: 60px; + width: var(--cell-size); + height: var(--cell-size); display: flex; align-items: center; justify-content: center; - font-size: 30px; + font-size: calc(var(--cell-size) * 0.5); cursor: pointer; transition: 0.2s; } @@ -152,4 +161,119 @@ body { .btn.active { background: #22c55e; -} \ No newline at end of file +} + +.board-size-info { + margin: 0 0 10px; + color: #475569; +} + +.sidebar { + position: fixed; + top: 0; + right: 0; + width: min(320px, 85vw); + height: 100vh; + padding: 24px; + background: white; + box-shadow: -10px 0 24px rgba(15, 23, 42, 0.18); + transform: translateX(100%); + transition: transform 0.25s ease; + z-index: 30; +} + +.sidebar.open { + transform: translateX(0); +} + +.save-points-sidebar { + width: min(380px, 92vw); +} + +.sidebar-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; +} + +.sidebar-close { + margin: 0; +} + +.sidebar-label { + display: block; + margin-top: 18px; + margin-bottom: 8px; + font-weight: bold; +} + +.sidebar input[type="range"] { + width: 100%; +} + +.sidebar-size-row { + margin: 12px 0 18px; + font-size: 18px; +} + +.sidebar-hint { + color: #64748b; + font-size: 14px; +} + +.save-points-list { + display: grid; + gap: 12px; + margin-top: 18px; +} + +.save-point-card { + border: 1px solid #dbe4ee; + border-radius: 10px; + padding: 14px; + background: #f8fafc; +} + +.save-point-card h4 { + margin: 0 0 8px; + color: #1e293b; +} + +.save-point-meta { + margin: 4px 0; + color: #475569; + font-size: 14px; +} + +.save-point-preview { + margin: 10px 0 0; + color: #334155; + font-size: 14px; + word-break: break-word; +} + +.sidebar-backdrop { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.3); + opacity: 0; + pointer-events: none; + transition: opacity 0.25s ease; + z-index: 20; +} + +.sidebar-backdrop.visible { + opacity: 1; + pointer-events: auto; +} + +@media (max-width: 720px) { + .container { + padding: 12px; + } + + .navbar { + padding: 15px 18px; + } +} diff --git a/app/static/js/dashboard.js b/app/static/js/dashboard.js index c8c744a..abb2334 100644 --- a/app/static/js/dashboard.js +++ b/app/static/js/dashboard.js @@ -1,53 +1,95 @@ -let mode = "queens"; // oder "rooks" +const DEFAULT_BOARD_SIZE = 8; +const MIN_BOARD_SIZE = 4; +const MAX_BOARD_SIZE = 13; +const EMPTY_CELL = -1; + +let mode = "queens"; +let boardSize = DEFAULT_BOARD_SIZE; +let pendingBoardSize = DEFAULT_BOARD_SIZE; +let board = createEmptyBoard(boardSize); + +const boardDiv = document.getElementById("board"); +const statusMessage = document.getElementById("statusMessage"); +const queenList = document.getElementById("queenList"); +const btnQueens = document.getElementById("btnQueens"); +const btnRooks = document.getElementById("btnRooks"); +const settingsSidebar = document.getElementById("settingsSidebar"); +const sidebarBackdrop = document.getElementById("sidebarBackdrop"); +const savePointsSidebar = document.getElementById("savePointsSidebar"); +const savePointsBackdrop = document.getElementById("savePointsBackdrop"); +const boardSizeInput = document.getElementById("boardSizeInput"); +const boardSizeValue = document.getElementById("boardSizeValue"); +const boardSizeValueMirror = document.getElementById("boardSizeValueMirror"); +const sidebarBoardSizeValue = document.getElementById("sidebarBoardSizeValue"); +const sidebarBoardSizeValueMirror = document.getElementById("sidebarBoardSizeValueMirror"); +const savePointsList = document.getElementById("savePointsList"); + +function createEmptyBoard(size) { + return Array(size).fill(EMPTY_CELL); +} + +function clampBoardSize(size) { + return Math.min(MAX_BOARD_SIZE, Math.max(MIN_BOARD_SIZE, Number(size))); +} + +function getColumnLabel(index) { + return String.fromCharCode(65 + index); +} + +function updateModeButtons() { + btnQueens.classList.toggle("active", mode === "queens"); + btnRooks.classList.toggle("active", mode === "rooks"); +} + +function updateBoardSizeDisplays(size) { + boardSizeValue.textContent = size; + boardSizeValueMirror.textContent = size; + sidebarBoardSizeValue.textContent = size; + sidebarBoardSizeValueMirror.textContent = size; + boardSizeInput.value = size; +} + +function setStatus(message = "", color = "") { + statusMessage.textContent = message; + statusMessage.style.color = color; +} + +function updateBoardLayout() { + const cellSize = Math.max(32, Math.min(60, Math.floor(480 / boardSize))); + + boardDiv.style.setProperty("--board-size", boardSize); + boardDiv.style.setProperty("--cell-size", `${cellSize}px`); +} function setMode(newMode) { mode = newMode; resetBoard(); - - document.getElementById("btnQueens").classList.remove("active"); - document.getElementById("btnRooks").classList.remove("active"); - - if (mode === "queens") { - document.getElementById("btnQueens").classList.add("active"); - } else { - document.getElementById("btnRooks").classList.add("active"); - } - - drawBoard(); + updateModeButtons(); } -let board = Array(8).fill(-1); -const boardDiv = document.getElementById("board"); - function getConflicts() { - let conflicts = []; + const conflicts = []; - for (let r1 = 0; r1 < 8; r1++) { - if (board[r1] === -1) continue; + for (let rowA = 0; rowA < boardSize; rowA++) { + if (board[rowA] === EMPTY_CELL) { + continue; + } - for (let r2 = r1 + 1; r2 < 8; r2++) { - if (board[r2] === -1) continue; - - let c1 = board[r1]; - let c2 = board[r2]; - - // 🔴 Türme: nur gleiche Spalte - if (mode === "rooks") { - if (c1 === c2) { - conflicts.push([r1, c1]); - conflicts.push([r2, c2]); - } + for (let rowB = rowA + 1; rowB < boardSize; rowB++) { + if (board[rowB] === EMPTY_CELL) { + continue; } - // 🔴 Damen: Spalte + Diagonale - else if (mode === "queens") { - if ( - c1 === c2 || - Math.abs(c1 - c2) === Math.abs(r1 - r2) - ) { - conflicts.push([r1, c1]); - conflicts.push([r2, c2]); - } + const colA = board[rowA]; + const colB = board[rowB]; + const sameColumn = colA === colB; + const sameDiagonal = Math.abs(colA - colB) === Math.abs(rowA - rowB); + + // Rooks only attack vertically, queens also attack diagonally. + const hasConflict = mode === "rooks" ? sameColumn : sameColumn || sameDiagonal; + + if (hasConflict) { + conflicts.push([rowA, colA], [rowB, colB]); } } } @@ -56,71 +98,48 @@ function getConflicts() { } function checkSolution() { - const status = document.getElementById("statusMessage"); - - const allPlaced = board.every(col => col !== -1); + const allPlaced = board.every((col) => col !== EMPTY_CELL); if (!allPlaced) { - status.textContent = "❗ Noch nicht alle Figuren platziert."; - status.style.color = "orange"; + setStatus("❗ Noch nicht alle Figuren platziert.", "orange"); return; } - const conflicts = getConflicts(); - - if (conflicts.length > 0) { - status.textContent = "❌ Es gibt Konflikte!"; - status.style.color = "red"; + if (getConflicts().length > 0) { + setStatus("❌ Es gibt Konflikte!", "red"); return; } - status.textContent = "🎉 Perfekt gelöst!"; - status.style.color = "green"; + setStatus("🎉 Perfekt geloest!", "green"); } function drawBoard() { boardDiv.innerHTML = ""; boardDiv.className = "board-grid"; + updateBoardLayout(); const conflicts = getConflicts(); - const letters = ["A", "B", "C", "D", "E", "F", "G", "H"]; - - for (let row = -1; row < 8; row++) { - for (let col = -1; col < 8; col++) { + for (let row = -1; row < boardSize; row++) { + for (let col = -1; col < boardSize; col++) { const cell = document.createElement("div"); - // 🔹 Ecke oben links if (row === -1 && col === -1) { cell.classList.add("label"); - } - - // 🔹 Buchstaben (oben) - else if (row === -1) { - cell.textContent = letters[col]; + } else if (row === -1) { + cell.textContent = getColumnLabel(col); cell.classList.add("label"); - } - - // 🔹 Zahlen (links) - else if (col === -1) { - cell.textContent = 8 - row; + } else if (col === -1) { + cell.textContent = boardSize - row; cell.classList.add("label"); - } - - // 🔹 echtes Spielfeld - else { - cell.classList.add("cell"); - - if ((row + col) % 2 === 0) { - cell.classList.add("white"); - } else { - cell.classList.add("black"); - } + } else { + cell.classList.add("cell", (row + col) % 2 === 0 ? "white" : "black"); if (board[row] === col) { - cell.textContent = (mode === "queens") ? "♛" : "♜"; + cell.textContent = mode === "queens" ? "♛" : "♜"; - if (conflicts.some(([r, c]) => r === row && c === col)) { + // Highlight pieces that are part of at least one conflict pair. + if (conflicts.some(([conflictRow, conflictCol]) => conflictRow === row && conflictCol === col)) { cell.classList.add("invalid"); } } @@ -136,65 +155,163 @@ function drawBoard() { } function placeQueen(row, col) { - if (board[row] === col) { - board[row] = -1; - } else { - board[row] = col; - } + board[row] = board[row] === col ? EMPTY_CELL : col; drawBoard(); - updateQueenList(); } -async function solve() { +function previewBoardSize(size) { + pendingBoardSize = clampBoardSize(size); + sidebarBoardSizeValue.textContent = pendingBoardSize; + sidebarBoardSizeValueMirror.textContent = pendingBoardSize; +} - if (mode === "rooks") { - const res = await fetch("/solve_rooks"); - const solution = await res.json(); +function applyBoardSize() { + boardSize = pendingBoardSize; + board = createEmptyBoard(boardSize); + updateBoardSizeDisplays(boardSize); + setStatus(`Brettgroesse auf ${boardSize} x ${boardSize} gesetzt.`, "#2563eb"); + drawBoard(); + toggleSidebar(false); +} - board = solution; - drawBoard(); +function toggleSidebar(forceOpen) { + const shouldOpen = typeof forceOpen === "boolean" ? forceOpen : !settingsSidebar.classList.contains("open"); + + if (shouldOpen) { + toggleSavePointsPanel(false); + } + + settingsSidebar.classList.toggle("open", shouldOpen); + sidebarBackdrop.classList.toggle("visible", shouldOpen); + settingsSidebar.setAttribute("aria-hidden", String(!shouldOpen)); +} + +function toggleSavePointsPanel(forceOpen) { + const shouldOpen = typeof forceOpen === "boolean" ? forceOpen : !savePointsSidebar.classList.contains("open"); + + savePointsSidebar.classList.toggle("open", shouldOpen); + savePointsBackdrop.classList.toggle("visible", shouldOpen); + savePointsSidebar.setAttribute("aria-hidden", String(!shouldOpen)); +} + +function getPieceLabel(selectedMode) { + return selectedMode === "queens" ? "Dame" : "Turm"; +} + +function getBoardPreview(savedBoard, limit = 4) { + const preview = []; + + for (let row = 0; row < savedBoard.length; row++) { + if (savedBoard[row] === EMPTY_CELL) { + continue; + } + + preview.push(`${getColumnLabel(savedBoard[row])}${savedBoard.length - row}`); + + if (preview.length === limit) { + break; + } + } + + return preview.length > 0 ? preview.join(", ") : "Noch keine Figuren gesetzt"; +} + +function renderSavePoints(savePoints) { + savePointsList.innerHTML = ""; + + if (savePoints.length === 0) { + savePointsList.innerHTML = ''; return; } - // Damen (wie vorher) - const res = await fetch("/solve"); - const solution = await res.json(); + for (const savePoint of savePoints) { + const item = document.createElement("article"); + const title = document.createElement("h4"); + const createdAt = document.createElement("p"); + const boardMeta = document.createElement("p"); + const preview = document.createElement("p"); + const loadButton = document.createElement("button"); - board = Array.isArray(solution[0]) ? solution[0] : solution; + item.className = "save-point-card"; + title.textContent = `Save Point #${savePoint.id}`; + createdAt.className = "save-point-meta"; + createdAt.textContent = `Gespeichert: ${savePoint.created_at}`; + boardMeta.className = "save-point-meta"; + boardMeta.textContent = `${getPieceLabel(savePoint.mode)} | ${savePoint.board_size} x ${savePoint.board_size} | ${savePoint.pieces_placed}/${savePoint.board_size} gesetzt`; + preview.className = "save-point-preview"; + preview.textContent = `Vorschau: ${getBoardPreview(savePoint.board)}`; + + loadButton.className = "btn"; + loadButton.type = "button"; + loadButton.textContent = "Diesen laden"; + loadButton.onclick = () => loadSavePoint(savePoint.id); + + item.appendChild(title); + item.appendChild(createdAt); + item.appendChild(boardMeta); + item.appendChild(preview); + item.appendChild(loadButton); + savePointsList.appendChild(item); + } +} + +async function refreshSavePoints() { + const response = await fetch("/save_points"); + const savePoints = await response.json(); + renderSavePoints(savePoints); +} + +async function openSavePointsPanel() { + await refreshSavePoints(); + toggleSidebar(false); + toggleSavePointsPanel(true); +} + +function applyLoadedGame(data) { + board = data.board; + boardSize = Array.isArray(data.board) ? clampBoardSize(data.board.length) : DEFAULT_BOARD_SIZE; + pendingBoardSize = boardSize; + mode = data.mode; + + updateModeButtons(); + updateBoardSizeDisplays(boardSize); drawBoard(); } +async function solve() { + const endpoint = mode === "rooks" ? "/solve_rooks" : "/solve"; + const response = await fetch(`${endpoint}?size=${boardSize}`); + const solution = await response.json(); + + // The queens endpoint may return multiple solutions; we display the first one. + board = mode === "queens" && Array.isArray(solution[0]) ? solution[0] : solution; + setStatus(`Loesung fuer ${boardSize} x ${boardSize} geladen.`, "green"); + drawBoard(); +} function resetBoard() { - const status = document.getElementById("statusMessage"); - board = Array(8).fill(-1); + board = createEmptyBoard(boardSize); + setStatus(); drawBoard(); - updateQueenList(); - status.textContent = ""; } -drawBoard(); - function updateQueenList() { - const list = document.getElementById("queenList"); - list.innerHTML = ""; - - const letters = ["A", "B", "C", "D", "E", "F", "G", "H"]; + queenList.innerHTML = ""; let count = 1; - for (let row = 0; row < 8; row++) { - if (board[row] !== -1) { - let col = board[row]; - - let pos = letters[col] + (8 - row); - - const li = document.createElement("li"); - li.textContent = `${mode === "queens" ? "Dame" : "Turm"} ${count}: ${pos}`; - - list.appendChild(li); - count++; + for (let row = 0; row < boardSize; row++) { + if (board[row] === EMPTY_CELL) { + continue; } + + const col = board[row]; + const position = `${getColumnLabel(col)}${boardSize - row}`; + const li = document.createElement("li"); + + li.textContent = `${mode === "queens" ? "Dame" : "Turm"} ${count}: ${position}`; + queenList.appendChild(li); + count++; } } @@ -205,30 +322,38 @@ async function saveGame() { "Content-Type": "application/json" }, body: JSON.stringify({ - board: board, - mode: mode + board, + mode, + boardSize }) }); + await refreshSavePoints(); alert("💾 Spiel gespeichert!"); } async function loadGame() { - const res = await fetch("/load"); - const data = await res.json(); + const response = await fetch("/load"); + const data = await response.json(); + applyLoadedGame(data); +} - board = data.board; - mode = data.mode; +async function loadSavePoint(savePointId) { + const response = await fetch(`/load/${savePointId}`); - // Buttons richtig setzen - document.getElementById("btnQueens").classList.remove("active"); - document.getElementById("btnRooks").classList.remove("active"); - - if (mode === "queens") { - document.getElementById("btnQueens").classList.add("active"); - } else { - document.getElementById("btnRooks").classList.add("active"); + if (!response.ok) { + setStatus("❌ Dieser Speicherstand konnte nicht geladen werden.", "red"); + return; } - drawBoard(); + const data = await response.json(); + + applyLoadedGame(data); + setStatus(`Speicherstand #${savePointId} geladen.`, "green"); + toggleSavePointsPanel(false); } + +pendingBoardSize = boardSize; +updateModeButtons(); +updateBoardSizeDisplays(boardSize); +drawBoard(); diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index dcf526a..9c988c1 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -48,12 +48,15 @@
+
-

♟️ 8-Damen Spiel

+

♟️ Damen- und Tuerme-Spiel

+

Aktuelle Brettgroesse: 8 x 8

@@ -64,18 +67,51 @@ - +
-

📋 Damen Positionen

+

📋 Figuren Positionen

+ + + + + + + - \ No newline at end of file +