improved javascript structure, added load selection and made chessboard selectable

This commit is contained in:
2026-03-30 19:03:13 +02:00
parent b808b0017b
commit bc377b6e33
6 changed files with 554 additions and 196 deletions
+132 -8
View File
@@ -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;
}
}
.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;
}
}
+256 -131
View File
@@ -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 = '<p class="sidebar-hint">Noch keine Speicherstaende gefunden.</p>';
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();