From b808b0017b8a44d84567b1d3dcdfe85d5ff16931 Mon Sep 17 00:00:00 2001 From: ScriptSorcerer20 Date: Mon, 30 Mar 2026 13:22:04 +0200 Subject: [PATCH 1/3] implemented new model for database, new routes for save. Fixed logout not allowing post requests --- app/__init__.py | 1 + app/__pycache__/__init__.cpython-311.pyc | Bin 2405 -> 2477 bytes app/app.db | Bin 16384 -> 20480 bytes .../__pycache__/gamestate.cpython-311.pyc | Bin 0 -> 1108 bytes app/models/gamestate.py | 11 ++++++ .../__pycache__/login_routes.cpython-311.pyc | Bin 3503 -> 3515 bytes .../__pycache__/main_routes.cpython-311.pyc | Bin 3910 -> 5694 bytes app/routes/login_routes.py | 2 +- app/routes/main_routes.py | 35 ++++++++++++++++- app/static/js/dashboard.js | 37 +++++++++++++++++- app/templates/dashboard.html | 3 ++ database_setup.sql | 8 ++++ 12 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 app/models/__pycache__/gamestate.cpython-311.pyc create mode 100644 app/models/gamestate.py 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 7124496d8c86a27c42540541fabdbb5374b3b83e..3a6324cf054d49b95230387a0b02f799fe94505e 100644 GIT binary patch delta 508 zcmaDVv{smRIWI340}yyboyz<_k@pbW8zu&ZcD9XIH!}*eF*2lZrf}!*=L*y^F*4LJ zED)UR$RslPIuoCeDFY)zi4aI91S}AON_pD~!^Aq+(il^CS~!*g1y;iZ8ETn%H~9vuwXi^LeoAUiv0ge*VKGQy5zl0PHhV^~$^L8- z_2NKh+!88EP0uU_%ETw-l%^IGW#*M=vfpA(EGW3e;p7v6RKx>RStJ4?#6Sc{ zV-YuyC}sx|3Si(+1oG-FuH>Rrpgr+G{YAP!0Zx#r)r<@bKN=YBa`8{)=+y7gZ*YFf zFW58r0ozQW8!XxvP|@T{_AiQNQZ6#gM`T!B6xfd_GXhCXu3M~WIf=#DMa)1GizFxa xap!f}+g45>2*S%!vgBw>X>}gI(iYJcEja zfhvncK?F!o5jT)1W&;umAmC6W1Z3XgN-j!GEJ=+A>MYU*3UGoH&7920zHahC_AiPC zQZ8c5N5oiMq}Y$hG6G3Wu3M~WIf=#DMa)2zMG}**ap*0%XKWh9VIV8)W`3 d4x8Nkl+v73yCQ?hnw%LjGK~Bm7%&O24ggW{UXlO+ diff --git a/app/app.db b/app/app.db index 479ce3a3ce8b1227654035b3a039b314d6429240..d8d2fca1212c056c3d10a85c44c6d47bf2a8e670 100644 GIT binary patch delta 500 zcmb7AO-mb56rBl1QJg#lB@_{#prA03cjvvC_a=eDL4;y&ofi(AbM8H7al2i+dtv{Ynao?3^?>Ke{dVbe-dZ{Q z=b$@l$hoo#J#>Qx&edh_qf_bChDM6>^Y%~GN#v0^NLpt9xRZ4LWBJv395rIxh^n<1 zzh3nJ6z1&x{$-&^WIOqxaU=fRz?)kcwre#PkBzq4+Tfk2zWOPuF9BJ8S@vg9v2V~+ zZfyCwSGb)ximmBJ(>u9XaE2J delta 228 zcmXxTD^3JK5CG7g1U zL4x5WZ~Ew_se3zFEsQaTfB$vw=l*zMF1BAs!;d-WXVcHa=djzamP5av`|suJ?aTGu z&E?frp5+1VpB_(ZYA8;$5;bWKNuv;PCRPWWLkldKOSM``g(j#u=YR#!tLIn?*_1Uv zsI@tem@?OBiL@XC22I|gFmerrAfA<`!ZBL*RT6^}bk?P{^=nHbgiTBv>v!Y$3lDWX A82|tP diff --git a/app/models/__pycache__/gamestate.cpython-311.pyc b/app/models/__pycache__/gamestate.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..02fc1f9e2bb7ad5c99298bffc0f3636e79837b3f GIT binary patch literal 1108 zcmaJi(Qw+;d_FH_Gm^T}6FGHPz!9><{19Jd`}8DzX@6`&F$qZW%9mR>Jq z^58GA;$y`vON3PYfj!Am1BUWh?@1$-k?Lq|vOc{#)s`kY(nMF9D9`l5w<_7H*5G#n zyUXoxyc3Rh!}0RmLGWf}raE8C*7LjGb}-fn#=5~+`B_iCQJJhhsIAmq*H;?Wht>Kj z@Z@+$j(6pFIe8GdT^p&7HYRr-e&P4U@A5bKYiK{zjwCvfL^qNszhK?E>q=dW`Y0Nl zvb?6Nr42=Bk!7>)#v<9aC49{zf0 ctUdf4c|`8<5xP9oB+!2UH}m|t!rhnf7vUZSivR!s literal 0 HcmV?d00001 diff --git a/app/models/gamestate.py b/app/models/gamestate.py new file mode 100644 index 0000000..d68aa08 --- /dev/null +++ b/app/models/gamestate.py @@ -0,0 +1,11 @@ +from .. import db +from flask_login import UserMixin + +class GameState(UserMixin, db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, nullable=False) + + board = db.Column(db.Text, nullable=False) # JSON String + mode = db.Column(db.String(10), nullable=False) # queens / rooks + + created_at = db.Column(db.DateTime, default=db.func.now()) diff --git a/app/routes/__pycache__/login_routes.cpython-311.pyc b/app/routes/__pycache__/login_routes.cpython-311.pyc index 1b6cba17cad548f85c93d767e1173fe1dfb453fc..60142fbac202d7a59c2ad6e912f83c4bf4ab1ffc 100644 GIT binary patch delta 94 zcmZ24y<3`hIWI340}xopoXWg3kyny2WTUz?BToufI^!DdWlRhVtC==?Fsg7fI&9A6 o;bG)i;I_b`LGB`>&J{+T$@RPv61qUf2Nq@^!2lzQG=T;J0Ez_{ivR!s delta 117 zcmdljy)3=FFoH~TTFaC2D!*&mo085wU( zp2#CLc><5<{z-q=9lDSYQ%h*&NoY_aSNj=P*xsUIh zd*|Hmocs0GFLt?axLghb^6K&1@t>5d?ht>FzuLR^C?!-#C}Rp0sg!Ue)iISL(3TU< zq-)G&wQUJ^(lh4a2v0PQ+O>vdo{*dHmSxPVd2>F4;~FD`D$5+q!CMyUm?O*3yjdL} zV~tg`pHQ_Pt;y7R_4{_IsS5EB>Z0yRZpBkx(s-Y1mHgFNSk7DTfciEq7pS8932oef zZrX$n(pDOn`dc!rzh_0pl?4@Xegp_=V55H$b0xQTafb3XSR~CsI*H zSpfQO<1Jwu=l$$s;U7+IK~yWMEygX$@3jo1*hwum&AMT@(fC&S0o;Brf55dFqqZ;i zbRf7BR79#6;y@Tx7b^6qOQ1SHW|_}8Xb*G!#-x1$+}yVR+}{Rs5#E%er%!9ADC@wk z?XfFUnTW=w(+rg5i|sJ(D_g_3G1dp_d`M3v&S-suiBv3_(D(P%;LjP`91BOR|1L+3 ziRfFh%qqUn%WhPAKv~;-5v3d`$@0wlVQQ!Gm*Wa-_@WvQ_d-9yTh2_?f{e3NYFcOO z;%vB`L1`}l4;wq2?XtCrUgMbaU9Q8p?+kX~nx-9$GwFEhl#cuB=&)90Pea>M!$zyC zzY|T*OS|>x87&6Qn#j-+;Qiw%<2zwrHjN>!NE+uVGKK$aNgB}=nW~{@xO)E0@GHVH zAaB;t7lgF4YDdVFt=i7nt1b8%PR_oQ%n0?)IPyQH3f6$^hI|LK|FKJ~GVu+YgEWvw ztdldvWQuAL!L((7`{Qw1KFJ*K{4u!W5#EnsO|aOvck?#)xb?+F7|_tsqW9_URZ|By}aLaJ6xqV25Dh7`*yL zP_yKL>YE>&9s1GPun?SoW%kgj>RFIsBs=s#ZCY#CvglcJ`{t*X`~`P=(cQl4@|J9( zQ+`N5Jwi1*^rw?Zwz=WUBbP=N_7=oYQ4Hn9(3&XE4c`;}cSV0e3>L*;UJSxw3rCkG z^NrmFwWp}|f{-AvL|Pw;)r zvz&~xw#wlechmr%v*FO>hV#BaW{4h0Bf%>|`ALon=YgDf`q%h+HjR??*fdtTicDGe z6?SIGl!{Acp;2S)lUxCjA{-k86NUwdVN#p&n;A_zr9*Pp&N||rpHX_L_Ev%GI=cCnG(icU+0x$Ltv@n?N{XwLdyIZfzc>V zFM&e-G|eBlr-tvU;ZNRJ8PBWXg1WD$?z^ve-wwSOS=@EcxBaefd%@RL^mP@K?xNCt zBb)g!bN%c`XG;Q+l#)!G4JE=!gKM5Zt~1w^i(U_W8p;P>fT!RYD0&9+_5ondn%BSB zvNUvUxZv$6dV5wIniiFkBs9q-0_q_VoVG`(pexxdT0so0a@Yz#orvnw49f;&jd9Wy ze&$(hW8Rq!n5_xD&&nITwaP(NE5n$>Ohq7JPU9Wd)|c>bt=q}Eb*vK1u$-2wBPJNb zD6KEK(EZy!2p%&DZ9h8>U5u{&yYZXr@XL*P@`PuV^ycfIRkAf-|Ev;kzWyHW$d*XWI%~q{u}|yoc*85l{~W!tbb!bk@)XE?DG(CEme76hR>_R5xM` HTfzMg-o^b4 delta 914 zcmZWoL1+^}6rI^5o9u38(QMHJeT7tbQ-|C2~<`82R4f@>LUyw%+soHxju>GpsK17bS$p+!E7Fk}V z$73Bl!Q-oRrjrM*5+^AawQF)mdy$Dj^I;fX2SAs)#Hs zcgV;3SIXp_@FSX%y^%MgJ-9gy;Dq*sL?5p9BVYwD1b~8dVauqIqbFs-m<5?NW2caU zoTK@AacRk2Solve + + + diff --git a/database_setup.sql b/database_setup.sql index e1be1a2..c2ece83 100644 --- a/database_setup.sql +++ b/database_setup.sql @@ -5,3 +5,11 @@ 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" + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file From bc377b6e33ef7e7cf83ed2ea8ff48c441d8d9d87 Mon Sep 17 00:00:00 2001 From: ScriptSorcerer20 Date: Mon, 30 Mar 2026 19:03:13 +0200 Subject: [PATCH 2/3] improved javascript structure, added load selection and made chessboard selectable --- .../__pycache__/main_routes.cpython-311.pyc | Bin 5694 -> 9286 bytes .../__pycache__/main_routes.cpython-312.pyc | Bin 0 -> 8027 bytes app/routes/main_routes.py | 179 +++++--- app/static/css/dashboard.css | 140 ++++++- app/static/js/dashboard.js | 387 ++++++++++++------ app/templates/dashboard.html | 44 +- 6 files changed, 554 insertions(+), 196 deletions(-) create mode 100644 app/routes/__pycache__/main_routes.cpython-312.pyc diff --git a/app/routes/__pycache__/main_routes.cpython-311.pyc b/app/routes/__pycache__/main_routes.cpython-311.pyc index c3c7267c6a8196fe1cbc5349f6d8047d4668ab3b..c5a0bbff6a3d1f6c64b0d3e6c19412e50f45f3d0 100644 GIT binary patch literal 9286 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 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 0000000000000000000000000000000000000000..72826714a6570b1c1c9ef42e0cbed4c8cb908f78 GIT binary patch literal 8027 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 LJ&y&bGJE|maJxr~ literal 0 HcmV?d00001 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 + From b90d6d0f03cf7ebc81b966879f2a884160d23ce9 Mon Sep 17 00:00:00 2001 From: ScriptSorcerer20 Date: Mon, 30 Mar 2026 19:22:14 +0200 Subject: [PATCH 3/3] Improved save and load system, added description and sort options --- app/__pycache__/__init__.cpython-311.pyc | Bin 2477 -> 2477 bytes app/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 4439 bytes .../__pycache__/gamestate.cpython-311.pyc | Bin 1108 -> 1747 bytes .../__pycache__/gamestate.cpython-312.pyc | Bin 0 -> 1743 bytes app/models/gamestate.py | 22 ++- .../__pycache__/main_routes.cpython-311.pyc | Bin 9286 -> 13583 bytes .../__pycache__/main_routes.cpython-312.pyc | Bin 8027 -> 11962 bytes app/routes/main_routes.py | 106 ++++++++--- app/static/css/dashboard.css | 61 +++++++ app/static/js/dashboard.js | 165 ++++++++++++++++-- app/templates/dashboard.html | 57 +++++- database_setup.sql | 13 +- 12 files changed, 378 insertions(+), 46 deletions(-) create mode 100644 app/__pycache__/__init__.cpython-312.pyc create mode 100644 app/models/__pycache__/gamestate.cpython-312.pyc diff --git a/app/__pycache__/__init__.cpython-311.pyc b/app/__pycache__/__init__.cpython-311.pyc index 3a6324cf054d49b95230387a0b02f799fe94505e..c75a2cb53ecd25c07bde4182f6d7f95ab4362cfc 100644 GIT binary patch delta 22 ccmZ20yjGZZIWI340}$L_e=76lM&1RS082~bk#_+n07Mf8lmGw# diff --git a/app/__pycache__/__init__.cpython-312.pyc b/app/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ae97128424de79a8757e3b913bbd82e78df38ceb GIT binary patch literal 4439 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 n)bIe+{T|i#o2AOKcrAz!T6P(*X literal 0 HcmV?d00001 diff --git a/app/models/__pycache__/gamestate.cpython-311.pyc b/app/models/__pycache__/gamestate.cpython-311.pyc index 02fc1f9e2bb7ad5c99298bffc0f3636e79837b3f..9a1f79a2f5a8535fcb9975de9cf305e3db026cce 100644 GIT binary patch literal 1747 zcmb_cO=#Oz9G7I-mi$#qO&YSSqXgE@1BVdruw^3L&uZB=QmE7&O7d z5zOAmxDcHpBJNYHkB~Nna(PFGzupVx{1*Tm5E79g5s{$~nQ>8sL_c!EW7^{wBI?-H z1pHAyXvX_5X!_Vagl0z2Y~S}N+Bb;qa{fNv(4Cmw1SF)#09vIn;;WCNM{R!Es=y z{_+3Q`+v?R*rXvln7xs?hC*z_s7tnQ14yBa>3eXd{UxnX6t!*P2Wfj5 zct)wnvY3;lQ+%yX8CW8ehCj9!N$X-o)=!hbJqe^01w5e}DREP>`=C%+(l#YcxWGnS zU0W@L9Wbg=DIo}aH!@)}f`IJ>!S)G4QAHIQ(*Z$vToL69O;8YYoJ)_@@Wr$}->Pz@ zsMy|wqDvc+W>W=Zdn*uD0D`_J{ixgSoGNO__M~*JplsMQxQD{KLZMuEBH3&ilJ3TL zNEKS+jjO6GiHhwnl!bM1Q`HI}WtMQO3Ps7L*DFfib}8zX?aONth>;-bwy#$drhg;O zAxGlfQhq+0%0GmfvUgJ7WfM!u#jKcrs7Dc$tVH#Sq!p!TNmHS?Vpc4bvN%P#oW+~d z-J6-xwub*7-31q3PQW!a`j0hs!&rOG@_T%BvNmVVAAZqdXWQ&-hn+RzR%l``S1r`3 zhVL-l3dP!?SSJ)SR$d3j_u|z=EoW{Vx?6#nc3`Fxm@#f!{;|Ee>Q}Y?f@yCoMi~Fd?o0Ijq#{7x<`QsM1*ya{H+@i5+g(qv2$<`zF=|<#P{O4#R zdh+RuPg>z*JDluB<#8>=VdFWfDDxy>(k z_+{hHWxTIJqrKdEwe#oBAG*Ujr^tef||Az*on7h@Ga+AXKIcO$eG#7y^ex!3SM^g4eNOS`zwfO^M# t(>tXL+ULOHe`a|NmNOSgk`}?W2H!K9Cp~Ax=mPCx-eCFLe^7SAy$5p?km~>d literal 1108 zcmaJi(Qw+;d_FH_Gm^T}6FGHPz!9><{19Jd`}8DzX@6`&F$qZW%9mR>Jq z^58GA;$y`vON3PYfj!Am1BUWh?@1$-k?Lq|vOc{#)s`kY(nMF9D9`l5w<_7H*5G#n zyUXoxyc3Rh!}0RmLGWf}raE8C*7LjGb}-fn#=5~+`B_iCQJJhhsIAmq*H;?Wht>Kj z@Z@+$j(6pFIe8GdT^p&7HYRr-e&P4U@A5bKYiK{zjwCvfL^qNszhK?E>q=dW`Y0Nl zvb?6Nr42=Bk!7>)#v<9aC49{zf0 ctUdf4c|`8<5xP9oB+!2UH}m|t!rhnf7vUZSivR!s diff --git a/app/models/__pycache__/gamestate.cpython-312.pyc b/app/models/__pycache__/gamestate.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..db13518392aa5ee34fb3b8da4780cd331dbc795d GIT binary patch literal 1743 zcmcIkO=ufO7@b}1N~_gREH$>Ox(%*r>u#Y*ae9eT0#%b3yCj4vpd-`jcL z_swHxev8FqK!^Ry3;o>y0DrJxG{hk|&QRb22q0&GBA4Shz=I_K`NseT5Vs|a1gC3{ z=3*lks0oUA#8aqngopVJpB_%3;d*?DZg&9!ISzsx54l``2T0iRTN@Vu#zz0iun6)8 zfgUSDM=ta)wZegTqLt3kCI|V)XE?5vCux0tW?VPGFxcaUXgp7JQl1HC7hvc>Ix##p zNjkX-c z=!s|3|9`dr&|j=KyjZ!JT3oq6!b@5at=gK6h+LphEi-(d-m6Fn>qQOM)bCL3HjO2- zVi;Q9Ku7Eb9Z7Ujc+3_Of@o8#81~UU8a<4znI_Ey=T^0CL{2uv%Ls2HtPb}o0bh&b zEP-U1l@g?d?tzsP_Arp(SC&yJnk2Mr z+Ngjq;dMwt6-w~3gyGz>{_)yBG6AOcNK5AKs{S$~K~xJ;Y&>yMyCtqEl< z%Rrh*B6?Zf)V3|GQ&zgktkR2!2%8mig9J>gO5_cUC=FH3Ci37=4F3#{X^-G_N-LGF zu&PG+3iI2y`*Nv<*}LdZ>@4N?=^cIyyo+=7J1@h@-DES}NVgW1A|!H``opEVuG*!71#E{pjWTEiZC$cd>b`ajmu9`o>*% z*4udRTdIg$t>5$#@3fL`+DW$;yO~brP<-;BpZK!A;!UMnyc>4HZKXZmRXWN~*&jbW z{P5|e{?xtt^6A(4?m}l_|JIYm{?rZ1CudvpuHq=|Y?d#6zaFo0y>p%N z_<8kN_4&@TonIgI6Kk~Is1Hw)n~_GuOU!twv|Dwm?vAsw_sC1U<))mJJL}Buz4t0C zCWAk$;DwQAcNGE0Sm6H%;vMGiz)^O82m3yz=(Y64;D@?_KcOLeD9dlt>CXVialeDa SDA59G`U_C#EB^*Ewp?xi literal 0 HcmV?d00001 diff --git a/app/models/gamestate.py b/app/models/gamestate.py index d68aa08..7aef035 100644 --- a/app/models/gamestate.py +++ b/app/models/gamestate.py @@ -1,11 +1,21 @@ from .. import db -from flask_login import UserMixin -class GameState(UserMixin, db.Model): +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) # JSON String - mode = db.Column(db.String(10), nullable=False) # queens / rooks - - created_at = db.Column(db.DateTime, default=db.func.now()) + 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__/main_routes.cpython-311.pyc b/app/routes/__pycache__/main_routes.cpython-311.pyc index c5a0bbff6a3d1f6c64b0d3e6c19412e50f45f3d0..ec65ad5f4128bc316292c594bb4bf37706520277 100644 GIT binary patch delta 5741 zcmb7HeQX=ab>AU(Ns&wOWBDmcqDa}4Oi~stAN^6;)!BFFvrcEp7fa3|mk);4u4JmT zsLoP8%cVosFd8Q@8r$QV zB=?X1xxQIar09Ud-Kcj?=INi>Fl&J(B8TCFWC=**X#lG{|n&+tuG@> zh6LBQx$J#IiVL?SBW;o$#sn8>FuNIXo4EbSJ;TRE^^fas2*URdv~LbX26d5bEs-5h zh{ULfdAo?FnOOY(sPX#-ml2MIKEE3Wa+4__^0_r=SVhzdzv^<7M0Az`X~WbQ3j!J% z(G2-T&A1}T8e722Qp}(kec7$B<^hnK#pCGr46+rq&7L9;)YF%3xVa&t=Z$ zakc=RTxK2*04E5K90pQm)_MEom2Zt*8LRPqRlZN*BP#ERRHDyU_~$F^^S=Wj=Keo% z|B6sQA?9TR9wgR||JWacY1oA+v$P~Fia0@sX1o*r@GvFal(HrzlTl(fL{f^Ck!A%xzBfK6v zbZ(WW#(B@?B3->U!{*3?tfXoix6$LkDIimLKN;x!0-7S{I+I;=Q|DdLFYSz;*%=kX z(ecsfCnNHAZBIzGmSh&1=Tmn-K#D-&>3mxQN!t0H zo5Ih~w?DzRik&}TFL3aQ4p@<6irlIhI^9f>ea4&hFoB&l&Xmk|^`XtVQu{dr!r7

    FLRV^P8-SSV(~r|1HmvXql% zX)K?~151z5*D1qHKNSQuo8HMU6|(t7S*C9d>#dIMzEjKDoS4=jWGSCrEKr2SD>w&o zGw{gg01N@%ZQd)#Yqn_B7QL4%-OX2QQN=c<+Q!O9KJj$lI9Bs)uX?txeo^rZsh*+o z{Yz`RPVm>)Vke!C0DbDs`k*W zv#aM6`!3bKt9;x;;5Q9N(IMN=lEsr6Lav^m|~8r=J*DKxZMre+b>a(5!E?znfuJdxO-G#pe7`$ zLP8OCs>057p@*OYwn?H$vvdFhgJ9Rx;LbH<27%x>P;%%5!E}g zVc|RgSwPCiHtdYe_1&>=kKG!o1>@CV{Pw<|y`rYZYpKapr2pYWK@BB6Tx{m*? zT&yR7FM|xGX-eauPRNQc&wwXVr@~W|NR(rd7MEc&6!(YdBrQH-7LA2Glc^__`l4i7 z_&WCuV4f!rgwG)%KXCubhefJF*U|MbP1yKrjmOk!W(W2E^!9U?pnD6{$3S(;=jSaju4fMeWRG!FyzU7gBiZHZCjahqxbL#< zFmu631-x84v@Rjhw8}ML(zN6&8dgn>k>#%$(fpPH(g4+a4EML{Iqh7*0h?(weIFL` z1y9fJ3#P5fpdf6DKw@ixc!7RHqb=~{a4?W68j8juTjYu+$QOV(?^4v!+XZtWu-S)2 zT1ak(O(&S5x&7-9^9*qc(L6I+PYmFXXO|+52z3x)yRNV=vKWP@-^`^7u zC4g62PeL65<;)b64)`lDJkN!vVceLdg^e{Uzuc%qp;Y`L$h4MwYK_9t1w~qA(pqGB zT-K~DxQhu}yjS-k-Gz-g3CbX-XtGNhSI8D}lE$9O=W{x&H$a^Hw?AIOw5Ex5;B(D= ztkB+5Y9)LE7&0vk>-ZaPRM+*Ijn+!|d%*k~Ds@-~UBD%q`^v%cOAl?%tNtsKl6OZWpdB-0~{+KGoj0*6Ck!_pS-u^ylf@ zaC8W!PZ=P;r=&dbnTO%bmnObBb#dzIsKSO+HdJ9lBk9;0Fx4VcInvrjV;xpYG6=;HJv;_);dRw(N z!rBR8-NXh+3Vu>?21(lEDsfYH4E6ejMxMq=80YaL&oqQsSg#+#i}^x$F2B4e{`LlV zam-c^nNuK*X%cHyKpHof#SmijK=twlQ%u9xDY-!jHOU==W^Q~u4WKzjHo)v=>;wz> z`S~1NGT`;;dik<>K=|)K?jv~QzW|~eMCiWJS98UyuK4Zw_lo~ms<!jj(U3I-)o_NT6$)ATq_J@rfIr-B+Ga*&y}AnUCFrx2=l%Io17~!lzU|Rbf+g zn+2b3GtKnUkz+?@j6@RmI25@fPAy+NbEWdHOf?$@XOKcjh)NVKt zE+tvij_FV?o2T$w@P16O2LF(fRZ6}CL^F#}P(nQ!~!0R~HmEqOy?+8v#279odR|hr-8H zK2~94fD0dXhtZ|RL8lSD6AqDlm!CZ7+O~PYSha=7y&kX24V!5+(J+q+9`bKJ(atTi z5DcGNcN~Xfxft%~_k$3eI;E$5^${BvLcR59`hMC68%{C(tS0z!un+!tE3 zgQWXI5j%p^(2j}vj;iRWu4_SiHW(mH%@Si6 zN5I`+DJ#%aP5k8aPu`%ahg5W^p$eBG3yL>Y8N*@FJVZ6)P0h=1D#))Qzpi3Mdp8&$ sTT}wT+h8dpsK4vL#omfzyMm%Bit0Khx delta 2675 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 C1s!Vu delta 2366 zcmZ`)du&^06~CYF!}0Yaj$d*7N*ph7ZsIhZC~bkRTdO{3*-~|FS=Fhyce!_|tt3vq zYZp4wkxGbWAn2@v)Jhvn`v+9gO`D1Y0)aFM@sv;^4<))pz{G^aKj5zY10f_h=UO+0 zpwVOU8(nahb2V7)6YQ$#Q-T0cPT0uh*@YJThSL6%>{$!N%Zmp{_sbLq=}3nvHgp4`ixm# zYMAB%FM{eDWs~qI(s4NNyIjx+NHw`5Xma-&pCo;e$S(M^FE8&#HU&QQQwrUfVX#z; z!ROQ}%fqkKqw-!7^}1=4 z^}6P>rK(k1u;sa$Zdm*=a&v&lK_s>~XD-;H)tKi`kl16@`SbP4yop6sXZ3lrY}L^b ze+*5mrMb?{;F~_6SsAD^v*F4|)SiTky&X*WgC{RB8%w+^K4Y97~sL z^Cmxu?32)rCzuA_L}TznAfKUIm!GTY+~7|k@-)1XNHgu;4-%_v#)-SzSSNVJ>88c0 zG`4(2pQ{+h&T0x*Q09k7DbAX8+h;B`@Q+g-!(@?=P+1JUu6OFu@e8jdFUilM@fnZ? z)-lB24SavVmUKRA@iXp4P7`p4@ie@aD#$ttHTZkF2!Bfj@y5SW-x*Ajk%s|6_TD&o{p5}5>(ejqc~?qqOA5)GQhY;-zbo~>?~lCv>D#mG z(b1N_xGoiOb|d*B?wIEAKKM!TID9)2V?kJoq#1+0v0>?&Q164Ke40h!>-pUQ%`=Uo zU}8*~3YBPf4?Qfqr^XGe%X3$8#7t|(7WKI~XXrRRAbeSVr#Mbh7C#1m%@3COD3Vh|3DjXqrmhe+7^ARjN|>!&QRcYNgV4U;I|8nY#U% z&CKLRX0p|{Yb%`I3=eIDhwjT_OnD%P0dHFnWiNJH9sfr^ri1Z2E0H>B%O>YF{sL?i z;wLKK#Q(cm&qrtU1qzX}aC_@@wr0h!gO)jG&d?Gj4eXuBe{uzJZ{V@cB3Th0DndMW z>s0g9W^8gJHu+Zn&q}|TdS_}qHra|jy%{^d5j)?CU08YQJ%19K6WL_272VtN?^~Dl zIg3@xcdGO;`1!=gnGOG*$g!)CD~+C@6~&!3Ljz~aywz_dQ;Aj_sfZ+Y-s`m<#)Si}gFOAoK2^l5Gja_7F68 R#p3&S)T8$$h3$8W@E>KLEdl@l diff --git a/app/routes/main_routes.py b/app/routes/main_routes.py index cf8405c..c1b77b2 100644 --- a/app/routes/main_routes.py +++ b/app/routes/main_routes.py @@ -23,6 +23,10 @@ def parse_board_size(raw_size, default=DEFAULT_BOARD_SIZE): 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 = [] @@ -67,12 +71,43 @@ def serialize_game_state(game_state): "id": game_state.id, "board": board, "mode": game_state.mode, - "board_size": len(board), + "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") + "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") @@ -127,29 +162,63 @@ def save_game(): 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 = GameState( + game_state = GameState( user_id=current_user.id, board=json.dumps(board), - mode=piece_mode + mode=piece_mode, + board_size=board_size, + save_name=save_name, + save_note=save_note, + is_favorite=is_favorite ) - db.session.add(game) + db.session.add(game_state) db.session.commit() - return jsonify({"status": "saved"}) + return jsonify({ + "status": "saved", + "save_point": serialize_game_state(game_state) + }) @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() - ) + save_points = build_save_point_query().all() + return jsonify([serialize_game_state(game_state) for game_state in save_points]) - return jsonify([serialize_game_state(game_state) for game_state in saved_games]) + +@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") @@ -157,25 +226,22 @@ def list_save_points(): def load_game(): latest_game = ( GameState.query.filter_by(user_id=current_user.id) - .order_by(GameState.id.desc()) + .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({ - "board": json.loads(latest_game.board), - "mode": latest_game.mode - }) + return jsonify(serialize_game_state(latest_game)) @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() + game_state = GameState.query.filter_by(id=game_id, user_id=current_user.id).first() - if not selected_game: + if not game_state: return jsonify({"error": "Save point not found"}), 404 - return jsonify(serialize_game_state(selected_game)) + return jsonify(serialize_game_state(game_state)) diff --git a/app/static/css/dashboard.css b/app/static/css/dashboard.css index ee0e1b3..c3ca90f 100644 --- a/app/static/css/dashboard.css +++ b/app/static/css/dashboard.css @@ -190,6 +190,10 @@ body { width: min(380px, 92vw); } +.save-sidebar { + width: min(360px, 90vw); +} + .sidebar-header { display: flex; justify-content: space-between; @@ -212,6 +216,29 @@ body { 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; @@ -228,6 +255,14 @@ body { 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; @@ -253,6 +288,32 @@ body { 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; diff --git a/app/static/js/dashboard.js b/app/static/js/dashboard.js index abb2334..437d712 100644 --- a/app/static/js/dashboard.js +++ b/app/static/js/dashboard.js @@ -15,6 +15,8 @@ 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"); @@ -23,6 +25,13 @@ 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); @@ -67,6 +76,12 @@ function setMode(newMode) { updateModeButtons(); } +function closeAllPanels() { + toggleSidebar(false); + toggleSavePanel(false); + toggleSavePointsPanel(false); +} + function getConflicts() { const conflicts = []; @@ -178,6 +193,7 @@ function toggleSidebar(forceOpen) { const shouldOpen = typeof forceOpen === "boolean" ? forceOpen : !settingsSidebar.classList.contains("open"); if (shouldOpen) { + toggleSavePanel(false); toggleSavePointsPanel(false); } @@ -186,9 +202,27 @@ function toggleSidebar(forceOpen) { 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)); @@ -198,6 +232,21 @@ 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 = []; @@ -216,11 +265,40 @@ function getBoardPreview(savedBoard, limit = 4) { 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 = '

    '; + savePointsList.innerHTML = ''; return; } @@ -228,42 +306,54 @@ function renderSavePoints(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 loadButton = document.createElement("button"); + const note = document.createElement("p"); + const actions = document.createElement("div"); item.className = "save-point-card"; - title.textContent = `Save Point #${savePoint.id}`; + title.textContent = `${savePoint.is_favorite ? "★ " : ""}${savePoint.save_name}`; createdAt.className = "save-point-meta"; - createdAt.textContent = `Gespeichert: ${savePoint.created_at}`; + 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"; - loadButton.className = "btn"; - loadButton.type = "button"; - loadButton.textContent = "Diesen laden"; - loadButton.onclick = () => loadSavePoint(savePoint.id); + 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(loadButton); + item.appendChild(note); + item.appendChild(actions); savePointsList.appendChild(item); } } async function refreshSavePoints() { - const response = await fetch("/save_points"); + const queryString = getSavePointFilters(); + const response = await fetch(`/save_points${queryString ? `?${queryString}` : ""}`); const savePoints = await response.json(); renderSavePoints(savePoints); } async function openSavePointsPanel() { await refreshSavePoints(); - toggleSidebar(false); toggleSavePointsPanel(true); } @@ -316,7 +406,7 @@ function updateQueenList() { } async function saveGame() { - await fetch("/save", { + const response = await fetch("/save", { method: "POST", headers: { "Content-Type": "application/json" @@ -324,12 +414,23 @@ async function saveGame() { body: JSON.stringify({ board, mode, - boardSize + 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(); - alert("💾 Spiel gespeichert!"); + setStatus(`Speicherpunkt "${data.save_point.save_name}" gespeichert.`, "green"); } async function loadGame() { @@ -349,10 +450,44 @@ async function loadSavePoint(savePointId) { const data = await response.json(); applyLoadedGame(data); - setStatus(`Speicherstand #${savePointId} geladen.`, "green"); + 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); diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index 9c988c1..1c82d34 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -66,7 +66,7 @@ - + @@ -95,6 +95,29 @@ + + + diff --git a/database_setup.sql b/database_setup.sql index c2ece83..161cd8b 100644 --- a/database_setup.sql +++ b/database_setup.sql @@ -9,7 +9,12 @@ CREATE TABLE users ( 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" - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); \ No newline at end of file + 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 +);