diff --git a/app/__init__.py b/app/__init__.py index 5fd7acc..ddb86b3 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -20,6 +20,7 @@ def create_app(): login_manager.login_view = "login.login" from .models.user import User + from .models.gamestate import GameState with app.app_context(): db.create_all() diff --git a/app/__pycache__/__init__.cpython-311.pyc b/app/__pycache__/__init__.cpython-311.pyc index 7124496..c75a2cb 100644 Binary files a/app/__pycache__/__init__.cpython-311.pyc and b/app/__pycache__/__init__.cpython-311.pyc differ diff --git a/app/__pycache__/__init__.cpython-312.pyc b/app/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..ae97128 Binary files /dev/null and b/app/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/app.db b/app/app.db index 479ce3a..d8d2fca 100644 Binary files a/app/app.db and b/app/app.db differ diff --git a/app/models/__pycache__/gamestate.cpython-311.pyc b/app/models/__pycache__/gamestate.cpython-311.pyc new file mode 100644 index 0000000..9a1f79a Binary files /dev/null and b/app/models/__pycache__/gamestate.cpython-311.pyc differ diff --git a/app/models/__pycache__/gamestate.cpython-312.pyc b/app/models/__pycache__/gamestate.cpython-312.pyc new file mode 100644 index 0000000..db13518 Binary files /dev/null and b/app/models/__pycache__/gamestate.cpython-312.pyc differ diff --git a/app/models/gamestate.py b/app/models/gamestate.py new file mode 100644 index 0000000..7aef035 --- /dev/null +++ b/app/models/gamestate.py @@ -0,0 +1,21 @@ +from .. import db + +class GameState(db.Model): + __tablename__ = "game_state" + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, nullable=False) + + board = db.Column(db.Text, nullable=False) + mode = db.Column(db.String(10), nullable=False) + board_size = db.Column(db.Integer, nullable=False, default=8) + 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) + created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now()) + updated_at = db.Column( + db.DateTime, + nullable=False, + server_default=db.func.now(), + onupdate=db.func.now() + ) diff --git a/app/routes/__pycache__/login_routes.cpython-311.pyc b/app/routes/__pycache__/login_routes.cpython-311.pyc index 1b6cba1..60142fb 100644 Binary files a/app/routes/__pycache__/login_routes.cpython-311.pyc and b/app/routes/__pycache__/login_routes.cpython-311.pyc differ diff --git a/app/routes/__pycache__/main_routes.cpython-311.pyc b/app/routes/__pycache__/main_routes.cpython-311.pyc index e097a85..ec65ad5 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..237fe6e Binary files /dev/null and b/app/routes/__pycache__/main_routes.cpython-312.pyc differ diff --git a/app/routes/login_routes.py b/app/routes/login_routes.py index a849065..4525a07 100644 --- a/app/routes/login_routes.py +++ b/app/routes/login_routes.py @@ -46,7 +46,7 @@ def login(): return render_template("login.html") -@login_bp.route('/logout') +@login_bp.route('/logout', methods=['POST']) @login_required def logout(): logout_user() diff --git a/app/routes/main_routes.py b/app/routes/main_routes.py index 102dcaa..c1b77b2 100644 --- a/app/routes/main_routes.py +++ b/app/routes/main_routes.py @@ -1,17 +1,123 @@ -from flask import Blueprint, jsonify, render_template, request -from flask_login import login_required, current_user +import json import random +from flask import Blueprint, jsonify, render_template, request +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 parse_favorite_flag(raw_value): + return str(raw_value).lower() in {"1", "true", "yes", "on"} + + +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": game_state.board_size, + "save_name": game_state.save_name, + "save_note": game_state.save_note, + "is_favorite": game_state.is_favorite, + "pieces_placed": sum(1 for column in board if column != -1), + "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") + } + + +def build_save_point_query(): + query = GameState.query.filter_by(user_id=current_user.id) + + selected_mode = request.args.get("mode") + board_size = request.args.get("board_size") + favorites_only = request.args.get("favorites_only") + sort_by = request.args.get("sort", "newest") + + if selected_mode in {"queens", "rooks"}: + query = query.filter_by(mode=selected_mode) + + if board_size not in {None, "", "all"}: + query = query.filter_by(board_size=parse_board_size(board_size)) + + if parse_favorite_flag(favorites_only): + query = query.filter_by(is_favorite=True) + + sort_options = { + "newest": GameState.created_at.desc(), + "oldest": GameState.created_at.asc(), + "updated": GameState.updated_at.desc(), + "favorites": GameState.is_favorite.desc() + } + + return query.order_by(sort_options.get(sort_by, GameState.created_at.desc()), GameState.id.desc()) + + @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(): @@ -19,57 +125,123 @@ 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"] + piece_mode = data["mode"] + board_size = parse_board_size(data.get("boardSize"), default=len(board)) + 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")) + + game_state = GameState( + user_id=current_user.id, + board=json.dumps(board), + mode=piece_mode, + board_size=board_size, + save_name=save_name, + save_note=save_note, + is_favorite=is_favorite + ) + + db.session.add(game_state) + db.session.commit() + + return jsonify({ + "status": "saved", + "save_point": serialize_game_state(game_state) + }) + + +@main_bp.route("/save_points") +@login_required +def list_save_points(): + save_points = build_save_point_query().all() + return jsonify([serialize_game_state(game_state) for game_state in save_points]) + + +@main_bp.route("/save_points//favorite", methods=["POST"]) +@login_required +def toggle_save_point_favorite(game_id): + game_state = GameState.query.filter_by(id=game_id, user_id=current_user.id).first() + + if not game_state: + return jsonify({"error": "Save point not found"}), 404 + + game_state.is_favorite = not game_state.is_favorite + db.session.commit() + + return jsonify(serialize_game_state(game_state)) + + +@main_bp.route("/save_points/", methods=["DELETE"]) +@login_required +def delete_save_point(game_id): + game_state = GameState.query.filter_by(id=game_id, user_id=current_user.id).first() + + if not game_state: + return jsonify({"error": "Save point not found"}), 404 + + db.session.delete(game_state) + db.session.commit() + + return jsonify({"status": "deleted"}) + + +@main_bp.route("/load") +@login_required +def load_game(): + latest_game = ( + GameState.query.filter_by(user_id=current_user.id) + .order_by(GameState.created_at.desc(), GameState.id.desc()) + .first() + ) + + if not latest_game: + return jsonify({"board": [-1] * DEFAULT_BOARD_SIZE, "mode": "queens"}) + + return jsonify(serialize_game_state(latest_game)) + + +@main_bp.route("/load/") +@login_required +def load_specific_game(game_id): + game_state = GameState.query.filter_by(id=game_id, user_id=current_user.id).first() + + if not game_state: + return jsonify({"error": "Save point not found"}), 404 + + return jsonify(serialize_game_state(game_state)) diff --git a/app/static/css/dashboard.css b/app/static/css/dashboard.css index 19f7438..c3ca90f 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,180 @@ 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); +} + +.save-sidebar { + width: min(360px, 90vw); +} + +.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-input { + width: 100%; + padding: 10px 12px; + border: 1px solid #cbd5e1; + border-radius: 8px; + font: inherit; + box-sizing: border-box; + background: #fff; +} + +.sidebar-textarea { + resize: vertical; + min-height: 96px; +} + +.sidebar-checkbox { + display: flex; + align-items: center; + gap: 10px; + margin: 16px 0 18px; + color: #334155; +} + +.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-filters { + display: grid; + gap: 8px; + margin-top: 18px; + padding-bottom: 16px; + border-bottom: 1px solid #e2e8f0; +} + +.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; +} + +.save-point-note { + margin: 10px 0 0; + color: #475569; + font-size: 14px; +} + +.save-point-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 14px; +} + +.save-point-actions .btn { + margin: 0; +} + +.btn.ghost { + background: #e2e8f0; + color: #1e293b; +} + +.btn.ghost:hover { + background: #cbd5e1; +} + +.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 d51e68c..437d712 100644 --- a/app/static/js/dashboard.js +++ b/app/static/js/dashboard.js @@ -1,53 +1,110 @@ -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 saveSidebar = document.getElementById("saveSidebar"); +const saveBackdrop = document.getElementById("saveBackdrop"); +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"); +const saveNameInput = document.getElementById("saveNameInput"); +const saveNoteInput = document.getElementById("saveNoteInput"); +const saveFavoriteInput = document.getElementById("saveFavoriteInput"); +const savePointModeFilter = document.getElementById("savePointModeFilter"); +const savePointSizeFilter = document.getElementById("savePointSizeFilter"); +const savePointSortFilter = document.getElementById("savePointSortFilter"); +const savePointFavoritesOnly = document.getElementById("savePointFavoritesOnly"); + +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 closeAllPanels() { + toggleSidebar(false); + toggleSavePanel(false); + toggleSavePointsPanel(false); +} 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 +113,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,64 +170,325 @@ 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) { + toggleSavePanel(false); + toggleSavePointsPanel(false); + } + + settingsSidebar.classList.toggle("open", shouldOpen); + sidebarBackdrop.classList.toggle("visible", shouldOpen); + settingsSidebar.setAttribute("aria-hidden", String(!shouldOpen)); +} + +function toggleSavePanel(forceOpen) { + const shouldOpen = typeof forceOpen === "boolean" ? forceOpen : !saveSidebar.classList.contains("open"); + + if (shouldOpen) { + toggleSidebar(false); + toggleSavePointsPanel(false); + } + + saveSidebar.classList.toggle("open", shouldOpen); + saveBackdrop.classList.toggle("visible", shouldOpen); + saveSidebar.setAttribute("aria-hidden", String(!shouldOpen)); +} + +function toggleSavePointsPanel(forceOpen) { + const shouldOpen = typeof forceOpen === "boolean" ? forceOpen : !savePointsSidebar.classList.contains("open"); + + if (shouldOpen) { + toggleSidebar(false); + toggleSavePanel(false); + } + + 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 getPlacedPieceCount() { + return board.filter((col) => col !== EMPTY_CELL).length; +} + +function resetSaveForm() { + saveNameInput.value = `${getPieceLabel(mode)} ${boardSize}x${boardSize} - ${getPlacedPieceCount()} gesetzt`; + saveNoteInput.value = ""; + saveFavoriteInput.checked = false; +} + +function openSavePanel() { + resetSaveForm(); + toggleSavePanel(true); +} + +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 getSavePointFilters() { + const params = new URLSearchParams(); + + if (savePointModeFilter.value !== "all") { + params.set("mode", savePointModeFilter.value); + } + + if (savePointSizeFilter.value !== "all") { + params.set("board_size", savePointSizeFilter.value); + } + + params.set("sort", savePointSortFilter.value); + + if (savePointFavoritesOnly.checked) { + params.set("favorites_only", "true"); + } + + return params.toString(); +} + +function createSavePointActionButton(label, className, onClick) { + const button = document.createElement("button"); + button.className = className; + button.type = "button"; + button.textContent = label; + button.onclick = onClick; + return button; +} + +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 updatedAt = document.createElement("p"); + const boardMeta = document.createElement("p"); + const preview = document.createElement("p"); + const note = document.createElement("p"); + const actions = document.createElement("div"); - board = Array.isArray(solution[0]) ? solution[0] : solution; + item.className = "save-point-card"; + title.textContent = `${savePoint.is_favorite ? "★ " : ""}${savePoint.save_name}`; + createdAt.className = "save-point-meta"; + createdAt.textContent = `Erstellt: ${savePoint.created_at}`; + updatedAt.className = "save-point-meta"; + updatedAt.textContent = `Aktualisiert: ${savePoint.updated_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)}`; + note.className = "save-point-note"; + note.textContent = savePoint.save_note ? `Notiz: ${savePoint.save_note}` : "Notiz: Keine Notiz"; + actions.className = "save-point-actions"; + + actions.appendChild(createSavePointActionButton("Laden", "btn", () => loadSavePoint(savePoint.id))); + actions.appendChild(createSavePointActionButton( + savePoint.is_favorite ? "Favorit entfernen" : "Als Favorit", + "btn secondary", + () => toggleSavePointFavorite(savePoint.id) + )); + actions.appendChild(createSavePointActionButton("Loeschen", "btn ghost", () => deleteSavePoint(savePoint.id))); + + item.appendChild(title); + item.appendChild(createdAt); + item.appendChild(updatedAt); + item.appendChild(boardMeta); + item.appendChild(preview); + item.appendChild(note); + item.appendChild(actions); + savePointsList.appendChild(item); + } +} + +async function refreshSavePoints() { + const queryString = getSavePointFilters(); + const response = await fetch(`/save_points${queryString ? `?${queryString}` : ""}`); + const savePoints = await response.json(); + renderSavePoints(savePoints); +} + +async function openSavePointsPanel() { + await refreshSavePoints(); + 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++; } -} \ No newline at end of file +} + +async function saveGame() { + const response = await fetch("/save", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + board, + mode, + boardSize, + saveName: saveNameInput.value, + saveNote: saveNoteInput.value, + isFavorite: saveFavoriteInput.checked + }) + }); + + if (!response.ok) { + setStatus("❌ Der Speicherstand konnte nicht gespeichert werden.", "red"); + return; + } + + const data = await response.json(); + + toggleSavePanel(false); + await refreshSavePoints(); + setStatus(`Speicherpunkt "${data.save_point.save_name}" gespeichert.`, "green"); +} + +async function loadGame() { + const response = await fetch("/load"); + const data = await response.json(); + applyLoadedGame(data); +} + +async function loadSavePoint(savePointId) { + const response = await fetch(`/load/${savePointId}`); + + if (!response.ok) { + setStatus("❌ Dieser Speicherstand konnte nicht geladen werden.", "red"); + return; + } + + const data = await response.json(); + + applyLoadedGame(data); + setStatus(`Speicherpunkt "${data.save_name}" geladen.`, "green"); + toggleSavePointsPanel(false); +} + +async function toggleSavePointFavorite(savePointId) { + const response = await fetch(`/save_points/${savePointId}/favorite`, { + method: "POST" + }); + + if (!response.ok) { + setStatus("❌ Favorit konnte nicht aktualisiert werden.", "red"); + return; + } + + await refreshSavePoints(); + setStatus("Favoritenstatus aktualisiert.", "#2563eb"); +} + +async function deleteSavePoint(savePointId) { + const confirmed = window.confirm("Diesen Speicherpunkt wirklich loeschen?"); + + if (!confirmed) { + return; + } + + const response = await fetch(`/save_points/${savePointId}`, { + method: "DELETE" + }); + + if (!response.ok) { + setStatus("❌ Speicherpunkt konnte nicht geloescht werden.", "red"); + return; + } + + await refreshSavePoints(); + setStatus("Speicherpunkt geloescht.", "#2563eb"); +} + +pendingBoardSize = boardSize; +updateModeButtons(); +updateBoardSizeDisplays(boardSize); +drawBoard(); diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index 9db3807..1c82d34 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

@@ -62,17 +65,108 @@ + + +
-

📋 Damen Positionen

+

📋 Figuren Positionen

    + + + + + + + + + + - \ No newline at end of file + diff --git a/database_setup.sql b/database_setup.sql index e1be1a2..161cd8b 100644 --- a/database_setup.sql +++ b/database_setup.sql @@ -5,3 +5,16 @@ CREATE TABLE users ( email VARCHAR(120) NOT NULL UNIQUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); + +CREATE TABLE game_state ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + board TEXT NOT NULL, -- JSON String (z.B. "[0,4,7,5,2,6,1,3]") + mode TEXT NOT NULL, -- "queens" oder "rooks" + board_size INTEGER NOT NULL DEFAULT 8, + save_name VARCHAR(80) NOT NULL DEFAULT 'Unnamed Save', + save_note TEXT NOT NULL DEFAULT '', + is_favorite BOOLEAN NOT NULL DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +);