Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 02cb1d0a64 | |||
| b90d6d0f03 | |||
| bc377b6e33 | |||
| b808b0017b | |||
| 5781eef189 | |||
| cfcb495d17 | |||
| 79cf3240e1 | |||
| 81d689c1ba | |||
| ecfe5c09a8 | |||
| 10e36d9aee | |||
| cb05b091d8 | |||
| 34dfa118d9 |
Binary file not shown.
+4
-2
@@ -1,7 +1,8 @@
|
|||||||
from flask import Flask
|
|
||||||
import os
|
import os
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
|
||||||
|
from flask import Flask
|
||||||
from flask_login import LoginManager
|
from flask_login import LoginManager
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
login_manager = LoginManager()
|
login_manager = LoginManager()
|
||||||
@@ -20,6 +21,7 @@ def create_app():
|
|||||||
login_manager.login_view = "login.login"
|
login_manager.login_view = "login.login"
|
||||||
|
|
||||||
from .models.user import User
|
from .models.user import User
|
||||||
|
from .models.gamestate import GameState
|
||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
db.create_all()
|
db.create_all()
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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()
|
||||||
|
)
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -46,7 +46,7 @@ def login():
|
|||||||
|
|
||||||
return render_template("login.html")
|
return render_template("login.html")
|
||||||
|
|
||||||
@login_bp.route('/logout')
|
@login_bp.route('/logout', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def logout():
|
def logout():
|
||||||
logout_user()
|
logout_user()
|
||||||
|
|||||||
+356
-7
@@ -1,17 +1,366 @@
|
|||||||
from flask import Blueprint, render_template
|
import json
|
||||||
from flask_login import login_required, current_user
|
import random
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
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__)
|
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 get_position_label(row, col, board_size):
|
||||||
|
return f"{chr(65 + col)}{board_size - row}"
|
||||||
|
|
||||||
|
|
||||||
|
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 build_queens_trace(board_size):
|
||||||
|
queen_positions = [-1] * board_size
|
||||||
|
steps = []
|
||||||
|
|
||||||
|
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:
|
||||||
|
steps.append({
|
||||||
|
"type": "solution",
|
||||||
|
"row": None,
|
||||||
|
"col": None,
|
||||||
|
"board": queen_positions[:],
|
||||||
|
"message": f"Loesung gefunden. Alle {board_size} Damen stehen ohne Konflikte."
|
||||||
|
})
|
||||||
|
return True
|
||||||
|
|
||||||
|
for col in range(board_size):
|
||||||
|
if not is_queen_position_safe(row, col):
|
||||||
|
continue
|
||||||
|
|
||||||
|
queen_positions[row] = col
|
||||||
|
position = get_position_label(row, col, board_size)
|
||||||
|
steps.append({
|
||||||
|
"type": "place",
|
||||||
|
"row": row,
|
||||||
|
"col": col,
|
||||||
|
"board": queen_positions[:],
|
||||||
|
"message": f"Dame auf {position} gesetzt."
|
||||||
|
})
|
||||||
|
|
||||||
|
if place_queen(row + 1):
|
||||||
|
return True
|
||||||
|
|
||||||
|
queen_positions[row] = -1
|
||||||
|
steps.append({
|
||||||
|
"type": "remove",
|
||||||
|
"row": row,
|
||||||
|
"col": col,
|
||||||
|
"board": queen_positions[:],
|
||||||
|
"message": f"Backtracking: Dame von {position} entfernt."
|
||||||
|
})
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
solved = place_queen(0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"mode": "queens",
|
||||||
|
"board_size": board_size,
|
||||||
|
"initial_board": [-1] * board_size,
|
||||||
|
"steps": steps,
|
||||||
|
"solved": solved
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_rooks_trace(board_size):
|
||||||
|
rook_positions = solve_rooks(board_size)
|
||||||
|
board = [-1] * board_size
|
||||||
|
steps = []
|
||||||
|
|
||||||
|
for row, col in enumerate(rook_positions):
|
||||||
|
board[row] = col
|
||||||
|
position = get_position_label(row, col, board_size)
|
||||||
|
steps.append({
|
||||||
|
"type": "place",
|
||||||
|
"row": row,
|
||||||
|
"col": col,
|
||||||
|
"board": board[:],
|
||||||
|
"message": f"Turm auf {position} gesetzt."
|
||||||
|
})
|
||||||
|
|
||||||
|
steps.append({
|
||||||
|
"type": "solution",
|
||||||
|
"row": None,
|
||||||
|
"col": None,
|
||||||
|
"board": board[:],
|
||||||
|
"message": f"Loesung gefunden. Alle {board_size} Tuerme stehen ohne Konflikte."
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"mode": "rooks",
|
||||||
|
"board_size": board_size,
|
||||||
|
"initial_board": [-1] * board_size,
|
||||||
|
"steps": steps,
|
||||||
|
"solved": True
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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("/")
|
@main_bp.route("/")
|
||||||
def home():
|
def home():
|
||||||
return render_template("index.html")
|
return render_template("index.html")
|
||||||
|
|
||||||
@main_bp.route('/dashboard')
|
|
||||||
|
@main_bp.route("/dashboard")
|
||||||
@login_required
|
@login_required
|
||||||
def dashboard():
|
def dashboard():
|
||||||
return render_template('dashboard.html', user=current_user)
|
return render_template("dashboard.html", user=current_user)
|
||||||
|
|
||||||
@main_bp.route('/solver')
|
|
||||||
def solver():
|
@main_bp.route("/check", methods=["POST"])
|
||||||
return render_template('solver.html')
|
def check():
|
||||||
|
data = request.json
|
||||||
|
board = data["board"]
|
||||||
|
row = data["row"]
|
||||||
|
col = data["col"]
|
||||||
|
piece_mode = data.get("mode", "queens")
|
||||||
|
|
||||||
|
for current_row, current_col in enumerate(board):
|
||||||
|
if current_col == -1 or current_row == row:
|
||||||
|
continue
|
||||||
|
|
||||||
|
same_column = current_col == col
|
||||||
|
same_diagonal = abs(current_col - col) == abs(current_row - row)
|
||||||
|
|
||||||
|
if same_column:
|
||||||
|
return jsonify({"valid": False})
|
||||||
|
|
||||||
|
if piece_mode == "queens" and same_diagonal:
|
||||||
|
return jsonify({"valid": False})
|
||||||
|
|
||||||
|
return jsonify({"valid": True})
|
||||||
|
|
||||||
|
|
||||||
|
@main_bp.route("/solve")
|
||||||
|
def solve_queens_route():
|
||||||
|
board_size = parse_board_size(request.args.get("size"))
|
||||||
|
return jsonify(solve_queens(board_size))
|
||||||
|
|
||||||
|
|
||||||
|
@main_bp.route("/solve_trace")
|
||||||
|
def solve_queens_trace_route():
|
||||||
|
board_size = parse_board_size(request.args.get("size"))
|
||||||
|
return jsonify(build_queens_trace(board_size))
|
||||||
|
|
||||||
|
|
||||||
|
@main_bp.route("/solve_rooks")
|
||||||
|
def solve_rooks_route():
|
||||||
|
board_size = parse_board_size(request.args.get("size"))
|
||||||
|
return jsonify(solve_rooks(board_size))
|
||||||
|
|
||||||
|
|
||||||
|
@main_bp.route("/solve_rooks_trace")
|
||||||
|
def solve_rooks_trace_route():
|
||||||
|
board_size = parse_board_size(request.args.get("size"))
|
||||||
|
return jsonify(build_rooks_trace(board_size))
|
||||||
|
|
||||||
|
|
||||||
|
@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"))
|
||||||
|
timestamp = datetime.utcnow()
|
||||||
|
|
||||||
|
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,
|
||||||
|
created_at=timestamp,
|
||||||
|
updated_at=timestamp
|
||||||
|
)
|
||||||
|
|
||||||
|
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/<int:game_id>/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/<int:game_id>", 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/<int:game_id>")
|
||||||
|
@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))
|
||||||
|
|||||||
@@ -0,0 +1,473 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 🔝 Navbar */
|
||||||
|
.navbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: #1e293b;
|
||||||
|
color: white;
|
||||||
|
padding: 15px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar a {
|
||||||
|
color: white;
|
||||||
|
margin-left: 15px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 📦 Layout */
|
||||||
|
.container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 30px auto;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 📇 Cards */
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-card {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#statusMessage {
|
||||||
|
min-height: 24px;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board {
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ♟️ Board */
|
||||||
|
.board-grid {
|
||||||
|
--label-size: 30px;
|
||||||
|
--cell-size: 60px;
|
||||||
|
display: grid;
|
||||||
|
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 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
background: #aec8eb;
|
||||||
|
color: #1e293b;
|
||||||
|
width: var(--label-size);
|
||||||
|
height: var(--label-size);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
width: var(--cell-size);
|
||||||
|
height: var(--cell-size);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: calc(var(--cell-size) * 0.5);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell.solver-place {
|
||||||
|
box-shadow: inset 0 0 0 4px #22c55e;
|
||||||
|
animation: solverPulse 0.35s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell.solver-remove {
|
||||||
|
box-shadow: inset 0 0 0 4px #f59e0b;
|
||||||
|
animation: solverPulse 0.35s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell.solver-solution {
|
||||||
|
box-shadow: inset 0 0 0 4px #2563eb;
|
||||||
|
animation: solverPulse 0.35s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.white {
|
||||||
|
background: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.black {
|
||||||
|
background: #475569;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid {
|
||||||
|
background: #ef4444 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 🎛️ Buttons */
|
||||||
|
.controls {
|
||||||
|
margin-top: 15px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin: 5px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled,
|
||||||
|
.btn:disabled:hover {
|
||||||
|
background: #94a3b8;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.secondary {
|
||||||
|
background: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.secondary:hover {
|
||||||
|
background: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.danger {
|
||||||
|
background: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.danger:hover {
|
||||||
|
background: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
#queenList {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#queenList li {
|
||||||
|
padding: 5px;
|
||||||
|
background: #f1f5f9;
|
||||||
|
margin: 4px 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-switch {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-switch .btn {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.active {
|
||||||
|
background: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.solver-panel {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #dbe4ee;
|
||||||
|
background: #f8fafc;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.solver-panel-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.solver-panel h3 {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.solver-message {
|
||||||
|
margin: 0;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.solver-step-info {
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1d4ed8;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.solver-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.solver-controls .btn,
|
||||||
|
.controls .btn {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.solver-speed {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.solver-speed input[type="range"] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.solver-speed-label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.solver-speed-value {
|
||||||
|
min-width: 62px;
|
||||||
|
text-align: right;
|
||||||
|
color: #475569;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-size-info {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: min(320px, 85vw);
|
||||||
|
height: 100vh;
|
||||||
|
padding: 24px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: white;
|
||||||
|
box-shadow: -10px 0 24px rgba(15, 23, 42, 0.18);
|
||||||
|
transform: translateX(100%);
|
||||||
|
transition: transform 0.25s ease;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes solverPulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.container {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
padding: 15px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.solver-panel-header,
|
||||||
|
.solver-speed {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.solver-step-info,
|
||||||
|
.solver-speed-value {
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
|
}
|
||||||
+56
-46
@@ -21,40 +21,11 @@ body {
|
|||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
font-family: "Geist Sans", system-ui, sans-serif;
|
font-family: "Geist Sans", system-ui, sans-serif;
|
||||||
|
|
||||||
|
background: linear-gradient(135deg, #c6dff4, #7c8384);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* MAGNET EFFECT */
|
|
||||||
.magnetic {
|
|
||||||
padding: 10px 20px;
|
|
||||||
border: none;
|
|
||||||
background: #4facfe;
|
|
||||||
color: white;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 16px;
|
|
||||||
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.magnetic:hover {
|
|
||||||
background: #2d8cf0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reset */
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Page setup */
|
|
||||||
body {
|
|
||||||
min-height: 100vh;
|
|
||||||
background: var(--primary-bg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navbar */
|
|
||||||
nav {
|
nav {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: var(--nav-bg-color);
|
background: var(--nav-bg-color);
|
||||||
@@ -93,17 +64,38 @@ main h1 {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
animation: fadeIn 0.8s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(15px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Form box */
|
/* Form box */
|
||||||
form {
|
form {
|
||||||
background: var(--secondary-bg-color);
|
background: rgba(255, 255, 255, 0.15);
|
||||||
padding: 30px;
|
backdrop-filter: blur(12px);
|
||||||
border-radius: 8px;
|
padding: 35px;
|
||||||
width: 300px;
|
border-radius: 12px;
|
||||||
|
width: 320px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
|
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Labels */
|
/* Labels */
|
||||||
label {
|
label {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
@@ -112,30 +104,48 @@ label {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Inputs */
|
/* Inputs */
|
||||||
input {
|
form {
|
||||||
padding: 10px;
|
background: rgba(255, 255, 255, 0.15);
|
||||||
border: 1px solid #ccc;
|
backdrop-filter: blur(12px);
|
||||||
border-radius: 6px;
|
padding: 35px;
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 320px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Button */
|
/* Button */
|
||||||
.button {
|
button {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
padding: 10px;
|
padding: 12px;
|
||||||
border: none;
|
border: none;
|
||||||
background: #4facfe;
|
background: linear-gradient(135deg, #e9eff5, #daebf0);
|
||||||
color: white;
|
color: rgb(0, 0, 0);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
background: #2d8cf0;
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Error message */
|
/* Error message */
|
||||||
.error {
|
.error {
|
||||||
color: red;
|
color: red;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* button anim */
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
:root {
|
|
||||||
--bg-color: #f4f6f8;
|
|
||||||
--text-color: #333;
|
|
||||||
--board-border: #2c3e50;
|
|
||||||
--square-light: #ecf0f1;
|
|
||||||
--square-dark: #95a5a6;
|
|
||||||
--accent-color: #3498db;
|
|
||||||
--btn-hover: #2980b9;
|
|
||||||
--shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
||||||
background-color: var(--bg-color);
|
|
||||||
color: var(--text-color);
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-weight: 300;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
color: var(--board-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
background: white;
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chessboard {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(8, 50px);
|
|
||||||
grid-template-rows: repeat(8, 50px);
|
|
||||||
border: 5px solid var(--board-border);
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.square {
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.square.light {
|
|
||||||
background-color: var(--square-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.square.dark {
|
|
||||||
background-color: var(--square-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.queen {
|
|
||||||
color: var(--board-border);
|
|
||||||
text-shadow: 0 2px 2px rgba(0,0,0,0.2);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.queen:hover {
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background-color: var(--accent-color);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 0.6rem 1.2rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: var(--btn-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
button.secondary {
|
|
||||||
background-color: transparent;
|
|
||||||
border: 1px solid var(--accent-color);
|
|
||||||
color: var(--accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
button.secondary:hover {
|
|
||||||
background-color: rgba(52, 152, 219, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.chessboard {
|
|
||||||
grid-template-columns: repeat(8, 1fr);
|
|
||||||
grid-template-rows: repeat(8, 1fr);
|
|
||||||
width: 100%;
|
|
||||||
max-width: 400px;
|
|
||||||
height: auto;
|
|
||||||
aspect-ratio: 1/1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.square {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,880 @@
|
|||||||
|
const DEFAULT_BOARD_SIZE = 8;
|
||||||
|
const MIN_BOARD_SIZE = 4;
|
||||||
|
const MAX_BOARD_SIZE = 13;
|
||||||
|
const EMPTY_CELL = -1;
|
||||||
|
const DEFAULT_SOLVER_MESSAGE = "Bereite einen Ablauf vor und spiele ihn Schritt fuer Schritt ab.";
|
||||||
|
|
||||||
|
let mode = "queens";
|
||||||
|
let boardSize = DEFAULT_BOARD_SIZE;
|
||||||
|
let pendingBoardSize = DEFAULT_BOARD_SIZE;
|
||||||
|
let board = createEmptyBoard(boardSize);
|
||||||
|
|
||||||
|
let solverSteps = [];
|
||||||
|
let solverStepIndex = -1;
|
||||||
|
let solverTimerId = null;
|
||||||
|
let solverSpeedMs = 320;
|
||||||
|
let solverLoading = false;
|
||||||
|
let solverRequestId = 0;
|
||||||
|
let currentSolverHighlight = null;
|
||||||
|
|
||||||
|
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 btnPrepareSolve = document.getElementById("btnPrepareSolve");
|
||||||
|
const btnInstantSolve = document.getElementById("btnInstantSolve");
|
||||||
|
const btnNextStep = document.getElementById("btnNextStep");
|
||||||
|
const btnPlayPause = document.getElementById("btnPlayPause");
|
||||||
|
const solverMessage = document.getElementById("solverMessage");
|
||||||
|
const solverStepInfo = document.getElementById("solverStepInfo");
|
||||||
|
const solverSpeedInput = document.getElementById("solverSpeedInput");
|
||||||
|
const solverSpeedValue = document.getElementById("solverSpeedValue");
|
||||||
|
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 setSolverMessage(message = DEFAULT_SOLVER_MESSAGE) {
|
||||||
|
solverMessage.textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSolverSpeed(rawValue) {
|
||||||
|
solverSpeedMs = Number(rawValue);
|
||||||
|
solverSpeedInput.value = String(solverSpeedMs);
|
||||||
|
solverSpeedValue.textContent = `${solverSpeedMs} ms`;
|
||||||
|
|
||||||
|
if (isSolverPlaying()) {
|
||||||
|
stopSolverPlayback(false);
|
||||||
|
scheduleNextSolverPlaybackStep();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 isSolverPlaying() {
|
||||||
|
return solverTimerId !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasPreparedSolverTrace() {
|
||||||
|
return solverSteps.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCompletedSolverTrace() {
|
||||||
|
return hasPreparedSolverTrace() && solverStepIndex >= solverSteps.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopSolverPlayback(refreshControls = true) {
|
||||||
|
if (solverTimerId !== null) {
|
||||||
|
window.clearTimeout(solverTimerId);
|
||||||
|
solverTimerId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refreshControls) {
|
||||||
|
updateSolverControls();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSolverControls() {
|
||||||
|
const currentStep = Math.max(0, solverStepIndex + 1);
|
||||||
|
|
||||||
|
btnPrepareSolve.disabled = solverLoading;
|
||||||
|
btnInstantSolve.disabled = solverLoading;
|
||||||
|
btnNextStep.disabled = solverLoading || hasCompletedSolverTrace();
|
||||||
|
btnPlayPause.disabled = solverLoading;
|
||||||
|
|
||||||
|
btnPrepareSolve.textContent = solverLoading ? "Preparing..." : "Solve Step-by-Step";
|
||||||
|
|
||||||
|
if (isSolverPlaying()) {
|
||||||
|
btnPlayPause.textContent = "Pause";
|
||||||
|
} else if (hasCompletedSolverTrace()) {
|
||||||
|
btnPlayPause.textContent = "Replay Animation";
|
||||||
|
} else {
|
||||||
|
btnPlayPause.textContent = "Play Animation";
|
||||||
|
}
|
||||||
|
|
||||||
|
solverStepInfo.textContent = `Schritt ${currentStep} / ${solverSteps.length}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function discardSolverTrace(message = DEFAULT_SOLVER_MESSAGE) {
|
||||||
|
solverRequestId += 1;
|
||||||
|
solverLoading = false;
|
||||||
|
stopSolverPlayback(false);
|
||||||
|
|
||||||
|
solverSteps = [];
|
||||||
|
solverStepIndex = -1;
|
||||||
|
currentSolverHighlight = null;
|
||||||
|
|
||||||
|
setSolverMessage(message);
|
||||||
|
updateSolverControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
function rewindSolverTrace() {
|
||||||
|
stopSolverPlayback(false);
|
||||||
|
solverStepIndex = -1;
|
||||||
|
currentSolverHighlight = null;
|
||||||
|
board = createEmptyBoard(boardSize);
|
||||||
|
setSolverMessage("Ablauf zurueckgesetzt. Starte die Animation erneut oder gehe weiter mit Next Step.");
|
||||||
|
drawBoard();
|
||||||
|
updateSolverControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMode(newMode) {
|
||||||
|
if (mode === newMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mode = newMode;
|
||||||
|
updateModeButtons();
|
||||||
|
resetBoard({
|
||||||
|
statusMessageText: `${getPieceLabel(mode)}-Modus aktiviert.`,
|
||||||
|
statusColor: "#2563eb",
|
||||||
|
solverMessageText: `${getPieceLabel(mode)}-Solver bereit. ${DEFAULT_SOLVER_MESSAGE}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAllPanels() {
|
||||||
|
toggleSidebar(false);
|
||||||
|
toggleSavePanel(false);
|
||||||
|
toggleSavePointsPanel(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConflicts() {
|
||||||
|
const conflicts = [];
|
||||||
|
|
||||||
|
for (let rowA = 0; rowA < boardSize; rowA++) {
|
||||||
|
if (board[rowA] === EMPTY_CELL) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let rowB = rowA + 1; rowB < boardSize; rowB++) {
|
||||||
|
if (board[rowB] === EMPTY_CELL) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colA = board[rowA];
|
||||||
|
const colB = board[rowB];
|
||||||
|
const sameColumn = colA === colB;
|
||||||
|
const sameDiagonal = Math.abs(colA - colB) === Math.abs(rowA - rowB);
|
||||||
|
const hasConflict = mode === "rooks" ? sameColumn : sameColumn || sameDiagonal;
|
||||||
|
|
||||||
|
if (hasConflict) {
|
||||||
|
conflicts.push([rowA, colA], [rowB, colB]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return conflicts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkSolution() {
|
||||||
|
const allPlaced = board.every((col) => col !== EMPTY_CELL);
|
||||||
|
|
||||||
|
if (!allPlaced) {
|
||||||
|
setStatus("Noch nicht alle Figuren platziert.", "#d97706");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getConflicts().length > 0) {
|
||||||
|
setStatus("Es gibt Konflikte auf dem Brett.", "red");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus("Perfekt geloest.", "green");
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawBoard() {
|
||||||
|
boardDiv.innerHTML = "";
|
||||||
|
boardDiv.className = "board-grid";
|
||||||
|
updateBoardLayout();
|
||||||
|
|
||||||
|
const conflicts = getConflicts();
|
||||||
|
const highlight = currentSolverHighlight;
|
||||||
|
|
||||||
|
for (let row = -1; row < boardSize; row++) {
|
||||||
|
for (let col = -1; col < boardSize; col++) {
|
||||||
|
const cell = document.createElement("div");
|
||||||
|
|
||||||
|
if (row === -1 && col === -1) {
|
||||||
|
cell.classList.add("label");
|
||||||
|
} else if (row === -1) {
|
||||||
|
cell.textContent = getColumnLabel(col);
|
||||||
|
cell.classList.add("label");
|
||||||
|
} else if (col === -1) {
|
||||||
|
cell.textContent = boardSize - row;
|
||||||
|
cell.classList.add("label");
|
||||||
|
} else {
|
||||||
|
cell.classList.add("cell", (row + col) % 2 === 0 ? "white" : "black");
|
||||||
|
|
||||||
|
if (highlight && highlight.row === row && highlight.col === col) {
|
||||||
|
cell.classList.add(
|
||||||
|
highlight.type === "remove"
|
||||||
|
? "solver-remove"
|
||||||
|
: highlight.type === "solution"
|
||||||
|
? "solver-solution"
|
||||||
|
: "solver-place"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (board[row] === col) {
|
||||||
|
cell.textContent = mode === "queens" ? "♛" : "♜";
|
||||||
|
|
||||||
|
if (conflicts.some(([conflictRow, conflictCol]) => conflictRow === row && conflictCol === col)) {
|
||||||
|
cell.classList.add("invalid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cell.onclick = () => placeQueen(row, col);
|
||||||
|
}
|
||||||
|
|
||||||
|
boardDiv.appendChild(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateQueenList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function placeQueen(row, col) {
|
||||||
|
if (solverLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasPreparedSolverTrace() || isSolverPlaying()) {
|
||||||
|
discardSolverTrace("Solver-Ablauf verworfen, weil das Brett manuell geaendert wurde.");
|
||||||
|
setStatus("Solver-Ablauf verworfen. Bereite ihn bei Bedarf neu vor.", "#d97706");
|
||||||
|
} else {
|
||||||
|
setStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSolverHighlight = null;
|
||||||
|
board[row] = board[row] === col ? EMPTY_CELL : col;
|
||||||
|
drawBoard();
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewBoardSize(size) {
|
||||||
|
pendingBoardSize = clampBoardSize(size);
|
||||||
|
sidebarBoardSizeValue.textContent = pendingBoardSize;
|
||||||
|
sidebarBoardSizeValueMirror.textContent = pendingBoardSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyBoardSize() {
|
||||||
|
boardSize = pendingBoardSize;
|
||||||
|
discardSolverTrace(`${getPieceLabel(mode)}-Solver bereit. ${DEFAULT_SOLVER_MESSAGE}`);
|
||||||
|
board = createEmptyBoard(boardSize);
|
||||||
|
currentSolverHighlight = null;
|
||||||
|
updateBoardSizeDisplays(boardSize);
|
||||||
|
setStatus(`Brettgroesse auf ${boardSize} x ${boardSize} gesetzt.`, "#2563eb");
|
||||||
|
drawBoard();
|
||||||
|
toggleSidebar(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = '<p class="sidebar-hint">Keine Speicherstaende fuer diese Filter gefunden.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
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) {
|
||||||
|
discardSolverTrace(`${getPieceLabel(data.mode || "queens")}-Solver bereit. ${DEFAULT_SOLVER_MESSAGE}`);
|
||||||
|
|
||||||
|
board = Array.isArray(data.board) ? data.board.slice() : createEmptyBoard(DEFAULT_BOARD_SIZE);
|
||||||
|
boardSize = Array.isArray(data.board) ? clampBoardSize(data.board.length) : DEFAULT_BOARD_SIZE;
|
||||||
|
pendingBoardSize = boardSize;
|
||||||
|
mode = data.mode === "rooks" ? "rooks" : "queens";
|
||||||
|
currentSolverHighlight = null;
|
||||||
|
|
||||||
|
updateModeButtons();
|
||||||
|
updateBoardSizeDisplays(boardSize);
|
||||||
|
drawBoard();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySolverStep(step) {
|
||||||
|
board = Array.isArray(step.board) ? step.board.slice() : createEmptyBoard(boardSize);
|
||||||
|
|
||||||
|
if (Number.isInteger(step.row) && Number.isInteger(step.col)) {
|
||||||
|
currentSolverHighlight = {
|
||||||
|
row: step.row,
|
||||||
|
col: step.col,
|
||||||
|
type: step.type
|
||||||
|
};
|
||||||
|
} else if (step.type === "solution" && solverStepIndex > 0) {
|
||||||
|
const previousStep = solverSteps[solverStepIndex - 1];
|
||||||
|
currentSolverHighlight = Number.isInteger(previousStep.row) && Number.isInteger(previousStep.col)
|
||||||
|
? {
|
||||||
|
row: previousStep.row,
|
||||||
|
col: previousStep.col,
|
||||||
|
type: "solution"
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
} else {
|
||||||
|
currentSolverHighlight = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSolverMessage(step.message || DEFAULT_SOLVER_MESSAGE);
|
||||||
|
drawBoard();
|
||||||
|
updateSolverControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
function advanceSolverStep() {
|
||||||
|
if (!hasPreparedSolverTrace() || hasCompletedSolverTrace()) {
|
||||||
|
stopSolverPlayback(false);
|
||||||
|
updateSolverControls();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
solverStepIndex += 1;
|
||||||
|
applySolverStep(solverSteps[solverStepIndex]);
|
||||||
|
|
||||||
|
if (hasCompletedSolverTrace()) {
|
||||||
|
stopSolverPlayback(false);
|
||||||
|
setStatus(`Loesung fuer ${boardSize} x ${boardSize} Schritt fuer Schritt aufgebaut.`, "green");
|
||||||
|
updateSolverControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleNextSolverPlaybackStep() {
|
||||||
|
stopSolverPlayback(false);
|
||||||
|
|
||||||
|
solverTimerId = window.setTimeout(() => {
|
||||||
|
solverTimerId = null;
|
||||||
|
|
||||||
|
const advanced = advanceSolverStep();
|
||||||
|
|
||||||
|
if (advanced && !hasCompletedSolverTrace()) {
|
||||||
|
scheduleNextSolverPlaybackStep();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSolverControls();
|
||||||
|
}, solverSpeedMs);
|
||||||
|
|
||||||
|
updateSolverControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startSolverPlayback(stepImmediately = true) {
|
||||||
|
if (!hasPreparedSolverTrace() || hasCompletedSolverTrace()) {
|
||||||
|
updateSolverControls();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stepImmediately) {
|
||||||
|
const advanced = advanceSolverStep();
|
||||||
|
|
||||||
|
if (!advanced || hasCompletedSolverTrace()) {
|
||||||
|
updateSolverControls();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleNextSolverPlaybackStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prepareSolutionTrace() {
|
||||||
|
const endpoint = mode === "rooks" ? "/solve_rooks_trace" : "/solve_trace";
|
||||||
|
const requestId = ++solverRequestId;
|
||||||
|
|
||||||
|
solverLoading = true;
|
||||||
|
stopSolverPlayback(false);
|
||||||
|
solverSteps = [];
|
||||||
|
solverStepIndex = -1;
|
||||||
|
currentSolverHighlight = null;
|
||||||
|
setSolverMessage(`Berechne ${getPieceLabel(mode)}-Ablauf fuer ${boardSize} x ${boardSize}...`);
|
||||||
|
setStatus(`Solver berechnet die Schritte fuer ${boardSize} x ${boardSize}.`, "#2563eb");
|
||||||
|
updateSolverControls();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${endpoint}?size=${boardSize}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("trace request failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const trace = await response.json();
|
||||||
|
|
||||||
|
if (requestId !== solverRequestId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
solverSteps = Array.isArray(trace.steps) ? trace.steps : [];
|
||||||
|
solverStepIndex = -1;
|
||||||
|
board = Array.isArray(trace.initial_board) ? trace.initial_board.slice() : createEmptyBoard(boardSize);
|
||||||
|
currentSolverHighlight = null;
|
||||||
|
|
||||||
|
if (!trace.solved || solverSteps.length === 0) {
|
||||||
|
setSolverMessage("Keine Loesung gefunden.");
|
||||||
|
setStatus("Keine Loesung fuer dieses Brett gefunden.", "red");
|
||||||
|
drawBoard();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSolverMessage(
|
||||||
|
mode === "queens"
|
||||||
|
? `Ablauf bereit mit ${solverSteps.length} Schritten. Nutze Next Step oder Play Animation, um das Backtracking zu verfolgen.`
|
||||||
|
: `Ablauf bereit mit ${solverSteps.length} Schritten. Nutze Next Step oder starte die Animation.`
|
||||||
|
);
|
||||||
|
setStatus("Solver-Ablauf bereit. Nutze Next Step oder Play Animation.", "#2563eb");
|
||||||
|
drawBoard();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (requestId !== solverRequestId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSolverMessage("Der Solver-Ablauf konnte nicht geladen werden.");
|
||||||
|
setStatus("Der Solver-Ablauf konnte nicht geladen werden.", "red");
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
if (requestId === solverRequestId) {
|
||||||
|
solverLoading = false;
|
||||||
|
updateSolverControls();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function nextSolverStep() {
|
||||||
|
if (solverLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPreparedSolverTrace()) {
|
||||||
|
const prepared = await prepareSolutionTrace();
|
||||||
|
|
||||||
|
if (!prepared) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSolverPlaying()) {
|
||||||
|
stopSolverPlayback(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!advanceSolverStep()) {
|
||||||
|
setStatus("Der Solver-Ablauf ist bereits abgeschlossen.", "#2563eb");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleSolverPlayback() {
|
||||||
|
if (solverLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSolverPlaying()) {
|
||||||
|
stopSolverPlayback();
|
||||||
|
setStatus("Solver-Animation pausiert.", "#2563eb");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPreparedSolverTrace()) {
|
||||||
|
const prepared = await prepareSolutionTrace();
|
||||||
|
|
||||||
|
if (!prepared) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasCompletedSolverTrace()) {
|
||||||
|
rewindSolverTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus("Solver-Animation laeuft.", "#2563eb");
|
||||||
|
startSolverPlayback(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function solve() {
|
||||||
|
await prepareSolutionTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function instantSolve() {
|
||||||
|
if (solverLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = mode === "rooks" ? "/solve_rooks" : "/solve";
|
||||||
|
const requestId = ++solverRequestId;
|
||||||
|
|
||||||
|
solverLoading = true;
|
||||||
|
stopSolverPlayback(false);
|
||||||
|
solverSteps = [];
|
||||||
|
solverStepIndex = -1;
|
||||||
|
currentSolverHighlight = null;
|
||||||
|
setSolverMessage(`Lade sofortige ${getPieceLabel(mode)}-Loesung fuer ${boardSize} x ${boardSize}...`);
|
||||||
|
setStatus(`Sofortige Loesung wird geladen.`, "#2563eb");
|
||||||
|
updateSolverControls();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${endpoint}?size=${boardSize}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("instant solve request failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const solution = await response.json();
|
||||||
|
|
||||||
|
if (requestId !== solverRequestId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
board = mode === "queens" && Array.isArray(solution[0]) ? solution[0].slice() : solution.slice();
|
||||||
|
currentSolverHighlight = null;
|
||||||
|
setSolverMessage("Sofortige Loesung geladen. Starte Solve Step-by-Step, wenn du den Ablauf sehen moechtest.");
|
||||||
|
setStatus(`Loesung fuer ${boardSize} x ${boardSize} sofort geladen.`, "green");
|
||||||
|
drawBoard();
|
||||||
|
} catch (error) {
|
||||||
|
if (requestId !== solverRequestId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSolverMessage("Die sofortige Loesung konnte nicht geladen werden.");
|
||||||
|
setStatus("Die sofortige Loesung konnte nicht geladen werden.", "red");
|
||||||
|
} finally {
|
||||||
|
if (requestId === solverRequestId) {
|
||||||
|
solverLoading = false;
|
||||||
|
updateSolverControls();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetBoard(options = {}) {
|
||||||
|
const {
|
||||||
|
statusMessageText = "",
|
||||||
|
statusColor = "",
|
||||||
|
solverMessageText = `${getPieceLabel(mode)}-Solver bereit. ${DEFAULT_SOLVER_MESSAGE}`
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
discardSolverTrace(solverMessageText);
|
||||||
|
board = createEmptyBoard(boardSize);
|
||||||
|
currentSolverHighlight = null;
|
||||||
|
|
||||||
|
if (statusMessageText) {
|
||||||
|
setStatus(statusMessageText, statusColor);
|
||||||
|
} else {
|
||||||
|
setStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
drawBoard();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateQueenList() {
|
||||||
|
queenList.innerHTML = "";
|
||||||
|
|
||||||
|
let count = 1;
|
||||||
|
|
||||||
|
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 += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
updateSolverSpeed(solverSpeedMs);
|
||||||
|
setSolverMessage(`${getPieceLabel(mode)}-Solver bereit. ${DEFAULT_SOLVER_MESSAGE}`);
|
||||||
|
updateSolverControls();
|
||||||
|
drawBoard();
|
||||||
+27
-9
@@ -1,14 +1,32 @@
|
|||||||
const button = document.querySelector(".magnetic")
|
const button = document.querySelector(".magnetic");
|
||||||
|
|
||||||
button.addEventListener("mousemove", (e) => {
|
button.addEventListener("mousemove", (e) => {
|
||||||
const rect = button.getBoundingClientRect()
|
const rect = button.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left - rect.width / 2;
|
||||||
|
const y = e.clientY - rect.top - rect.height / 2;
|
||||||
|
|
||||||
const x = e.clientX - rect.left - rect.width / 2
|
button.style.transform = `translate(${x * 0.2}px, ${y * 0.2}px)`;
|
||||||
const y = e.clientY - rect.top - rect.height / 2
|
});
|
||||||
|
|
||||||
button.style.transform = `translate(${x * 0.3}px, ${y * 0.3}px)`
|
|
||||||
})
|
|
||||||
|
|
||||||
button.addEventListener("mouseleave", () => {
|
button.addEventListener("mouseleave", () => {
|
||||||
button.style.transform = "translate(0,0)"
|
button.style.transform = "translate(0,0)";
|
||||||
})
|
});
|
||||||
|
|
||||||
|
const inputs = document.querySelectorAll("input");
|
||||||
|
|
||||||
|
inputs.forEach(input => {
|
||||||
|
input.addEventListener("input", () => {
|
||||||
|
input.style.boxShadow = "0 0 8px rgba(79,172,254,0.6)";
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
input.style.boxShadow = "";
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = document.querySelector("form");
|
||||||
|
|
||||||
|
form.addEventListener("submit", () => {
|
||||||
|
button.innerText = "Logging in...";
|
||||||
|
button.style.opacity = "0.7";
|
||||||
|
});
|
||||||
|
|||||||
+168
-24
@@ -1,51 +1,195 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Dashboard</title>
|
<title>Dashboard</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="../static/css/dashboard.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<nav>
|
<!-- 🔝 Navbar -->
|
||||||
<a href="{{ url_for('main.dashboard') }}">Dashboard</a>
|
<nav class="navbar">
|
||||||
<a href="{{ url_for('login.logout') }}">Logout</a>
|
<div class="nav-left">♛ 8-Queens App</div>
|
||||||
|
<div class="nav-right">
|
||||||
|
<a href="{{ url_for('main.dashboard') }}">Dashboard</a>
|
||||||
|
<a href="{{ url_for('login.logout') }}">Logout</a>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<!-- 📦 Container -->
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
||||||
```
|
<!-- 👤 User Card -->
|
||||||
<h1>Dashboard</h1>
|
<div class="card">
|
||||||
|
<h2>👋 Willkommen, {{ current_user.username }}</h2>
|
||||||
|
|
||||||
<p>Welcome back <strong>{{ current_user.username }}</strong> 👋</p>
|
<div class="user-info">
|
||||||
|
<p><strong>ID:</strong> {{ current_user.id }}</p>
|
||||||
|
<p><strong>Username:</strong> {{ current_user.username }}</p>
|
||||||
|
|
||||||
<div class="user-info">
|
{% if current_user.email %}
|
||||||
<h3>Your Account</h3>
|
<p><strong>Email:</strong> {{ current_user.email }}</p>
|
||||||
|
{% else %}
|
||||||
|
<p><strong>Email:</strong> Not set</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<p><strong>User ID:</strong> {{ current_user.id }}</p>
|
<p><strong>Erstellt:</strong> {{ current_user.created_at }}</p>
|
||||||
<p><strong>Username:</strong> {{ current_user.username }}</p>
|
</div>
|
||||||
|
|
||||||
{% if current_user.email %}
|
|
||||||
<p><strong>Email:</strong> {{ current_user.email }}</p>
|
|
||||||
{% else %}
|
|
||||||
<p><strong>Email:</strong> Not set</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<p><strong>Account created:</strong> {{ current_user.created_at }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="logout">
|
|
||||||
<form action="{{ url_for('login.logout') }}" method="post">
|
<form action="{{ url_for('login.logout') }}" method="post">
|
||||||
<button type="submit">Logout</button>
|
<button class="btn danger" type="submit">Logout</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
```
|
|
||||||
|
|
||||||
|
<div class="mode-switch">
|
||||||
|
<button class="btn active" onclick="setMode('queens')" id="btnQueens">♛ Damen</button>
|
||||||
|
<button class="btn secondary" onclick="setMode('rooks')" id="btnRooks">♜ Türme</button>
|
||||||
|
<button class="btn secondary" onclick="toggleSidebar()" id="btnSidebar">Board Settings</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ♟️ Game Card -->
|
||||||
|
<div class="card game-card">
|
||||||
|
<h2>♟️ Damen- und Tuerme-Spiel</h2>
|
||||||
|
<p class="board-size-info">Aktuelle Brettgroesse: <span id="boardSizeValue">8</span> x <span
|
||||||
|
id="boardSizeValueMirror">8</span></p>
|
||||||
|
|
||||||
|
<p id="statusMessage"></p>
|
||||||
|
<div class="board" id="board"></div>
|
||||||
|
|
||||||
|
<div class="solver-panel">
|
||||||
|
<div class="solver-panel-header">
|
||||||
|
<div>
|
||||||
|
<h3>Solver-Ablauf</h3>
|
||||||
|
<p class="solver-message" id="solverMessage">Bereite einen Ablauf vor und spiele ihn Schritt fuer Schritt ab.</p>
|
||||||
|
</div>
|
||||||
|
<p class="solver-step-info" id="solverStepInfo">Schritt 0 / 0</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="solver-controls">
|
||||||
|
<button class="btn" onclick="prepareSolutionTrace()" id="btnPrepareSolve" type="button">Solve Step-by-Step</button>
|
||||||
|
<button class="btn secondary" onclick="instantSolve()" id="btnInstantSolve" type="button">Instant Solve</button>
|
||||||
|
<button class="btn secondary" onclick="nextSolverStep()" id="btnNextStep" type="button">Next Step</button>
|
||||||
|
<button class="btn secondary" onclick="toggleSolverPlayback()" id="btnPlayPause" type="button">Play Animation</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="solver-speed">
|
||||||
|
<label class="solver-speed-label" for="solverSpeedInput">Speed</label>
|
||||||
|
<input id="solverSpeedInput" type="range" min="80" max="900" step="20" value="320"
|
||||||
|
oninput="updateSolverSpeed(this.value)">
|
||||||
|
<span class="solver-speed-value" id="solverSpeedValue">320 ms</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button class="btn" onclick="checkSolution()">Check</button>
|
||||||
|
<button class="btn secondary" onclick="resetBoard()">Reset</button>
|
||||||
|
|
||||||
|
<button class="btn" onclick="openSavePanel()">💾 Save</button>
|
||||||
|
<button class="btn" onclick="openSavePointsPanel()">📂 Load</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>📋 Figuren Positionen</h3>
|
||||||
|
<ul id="queenList"></ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<aside class="sidebar" id="settingsSidebar" aria-hidden="true">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<h3>Brettgroesse</h3>
|
||||||
|
<button class="btn secondary sidebar-close" onclick="toggleSidebar(false)" type="button">Schliessen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Wähle eine Grösse zwischen 4 und 13.</p>
|
||||||
|
|
||||||
|
<label class="sidebar-label" for="boardSizeInput">Groesse</label>
|
||||||
|
<input id="boardSizeInput" type="range" min="4" max="13" value="8" oninput="previewBoardSize(this.value)">
|
||||||
|
<div class="sidebar-size-row">
|
||||||
|
<strong><span id="sidebarBoardSizeValue">8</span> x <span id="sidebarBoardSizeValueMirror">8</span></strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn" onclick="applyBoardSize()" type="button">Apply Size</button>
|
||||||
|
<p class="sidebar-hint">Das Brett wird beim Ändern zur Sicherheit zurückgesetzt.</p>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<aside class="sidebar save-sidebar" id="saveSidebar" aria-hidden="true">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<h3>Speicherpunkt</h3>
|
||||||
|
<button class="btn secondary sidebar-close" onclick="toggleSavePanel(false)" type="button">Schliessen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Gib dem Spielstand einen klaren Namen und optional eine kurze Notiz.</p>
|
||||||
|
|
||||||
|
<label class="sidebar-label" for="saveNameInput">Name</label>
|
||||||
|
<input class="sidebar-input" id="saveNameInput" type="text" maxlength="80" placeholder="z. B. Fast geloeste Dame 8x8">
|
||||||
|
|
||||||
|
<label class="sidebar-label" for="saveNoteInput">Notiz</label>
|
||||||
|
<textarea class="sidebar-input sidebar-textarea" id="saveNoteInput" rows="4" maxlength="240"
|
||||||
|
placeholder="z. B. Gute Startposition fuer Tuermetest"></textarea>
|
||||||
|
|
||||||
|
<label class="sidebar-checkbox">
|
||||||
|
<input id="saveFavoriteInput" type="checkbox">
|
||||||
|
Als Favorit markieren
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button class="btn" onclick="saveGame()" type="button">Jetzt speichern</button>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<aside class="sidebar save-points-sidebar" id="savePointsSidebar" aria-hidden="true">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<h3>Save Points</h3>
|
||||||
|
<button class="btn secondary sidebar-close" onclick="toggleSavePointsPanel(false)" type="button">Schliessen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Waehle einen Speicherstand aus der Liste.</p>
|
||||||
|
|
||||||
|
<div class="save-point-filters">
|
||||||
|
<label class="sidebar-label" for="savePointModeFilter">Modus</label>
|
||||||
|
<select class="sidebar-input" id="savePointModeFilter" onchange="refreshSavePoints()">
|
||||||
|
<option value="all">Alle</option>
|
||||||
|
<option value="queens">Damen</option>
|
||||||
|
<option value="rooks">Tuerme</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label class="sidebar-label" for="savePointSizeFilter">Brettgroesse</label>
|
||||||
|
<select class="sidebar-input" id="savePointSizeFilter" onchange="refreshSavePoints()">
|
||||||
|
<option value="all">Alle</option>
|
||||||
|
{% for board_size in range(4, 14) %}
|
||||||
|
<option value="{{ board_size }}">{{ board_size }} x {{ board_size }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label class="sidebar-label" for="savePointSortFilter">Sortierung</label>
|
||||||
|
<select class="sidebar-input" id="savePointSortFilter" onchange="refreshSavePoints()">
|
||||||
|
<option value="newest">Neueste zuerst</option>
|
||||||
|
<option value="oldest">Aelteste zuerst</option>
|
||||||
|
<option value="updated">Zuletzt bearbeitet</option>
|
||||||
|
<option value="favorites">Favoriten zuerst</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label class="sidebar-checkbox">
|
||||||
|
<input id="savePointFavoritesOnly" type="checkbox" onchange="refreshSavePoints()">
|
||||||
|
Nur Favoriten anzeigen
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="save-points-list" id="savePointsList">
|
||||||
|
<p class="sidebar-hint">Noch keine Speicherstaende gefunden.</p>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="sidebar-backdrop" id="sidebarBackdrop" onclick="toggleSidebar(false)"></div>
|
||||||
|
<div class="sidebar-backdrop" id="saveBackdrop" onclick="toggleSavePanel(false)"></div>
|
||||||
|
<div class="sidebar-backdrop" id="savePointsBackdrop" onclick="toggleSavePointsPanel(false)"></div>
|
||||||
|
|
||||||
|
<script src="../static/js/dashboard.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
<a href="/solver">Solver</a>
|
<a href="/dashboard">Solver</a>
|
||||||
|
|
||||||
{% if session.get('user_id') %}
|
{% if session.get('user_id') %}
|
||||||
<a href="/dashboard">Dashboard</a>
|
<a href="/dashboard">Dashboard</a>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{{ url_for('login.login') }}"><span>Login</span></a></li>
|
<li><a href="{{ url_for('login.login') }}"><span>Login</span></a></li>
|
||||||
<li><a href="{{ url_for('login.register') }}"><span></span>Create account</span></a></li>
|
<li><a href="{{ url_for('login.register') }}"><span>Create account</span></a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>8-Damen-Problem</title>
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/solver.css') }}">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>8-Damen-Problem Test</h1>
|
|
||||||
<div class="chessboard">
|
|
||||||
{% for row in range(8) %}
|
|
||||||
{% for col in range(8) %}
|
|
||||||
<div class="square {{ 'light' if (row + col) % 2 == 0 else 'dark' }}" data-row="{{ row }}" data-col="{{ col }}">
|
|
||||||
<!-- Queens will be placed here -->
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="controls">
|
|
||||||
<button id="solveBtn">Solve</button>
|
|
||||||
<button id="resetBtn" class="secondary">Reset</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -5,3 +5,16 @@ CREATE TABLE users (
|
|||||||
email VARCHAR(120) NOT NULL UNIQUE,
|
email VARCHAR(120) NOT NULL UNIQUE,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
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
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user