1727 lines
64 KiB
JavaScript
1727 lines
64 KiB
JavaScript
const DEFAULT_BOARD_SIZE = 8;
|
|
const MIN_BOARD_SIZE = 4;
|
|
const MAX_BOARD_SIZE = 13;
|
|
const EMPTY_CELL = -1;
|
|
const GAME_STORAGE_KEY = "eight-queens-dashboard-session-v1";
|
|
|
|
let mode = "queens";
|
|
let boardSize = DEFAULT_BOARD_SIZE;
|
|
let pendingBoardSize = DEFAULT_BOARD_SIZE;
|
|
let board = createEmptyBoard(boardSize);
|
|
let stepCount = 0;
|
|
let elapsedBeforeTimerStartMs = 0;
|
|
let timerStartedAtMs = Date.now();
|
|
let gameTimerId = null;
|
|
let isSolved = false;
|
|
|
|
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 pieceCountBadge = document.getElementById("pieceCountBadge");
|
|
const navDashboardLink = document.getElementById("navDashboardLink");
|
|
const langSwitch = document.getElementById("langSwitch");
|
|
const accountLabel = document.getElementById("accountLabel");
|
|
const introKicker = document.getElementById("introKicker");
|
|
const introTitle = document.getElementById("introTitle");
|
|
const introText = document.getElementById("introText");
|
|
const infoButton = document.getElementById("infoButton");
|
|
const gameModeEyebrow = document.getElementById("gameModeEyebrow");
|
|
const gameTitle = document.getElementById("gameTitle");
|
|
const boardModeText = document.getElementById("boardModeText");
|
|
const boardSizeInfoPrefix = document.getElementById("boardSizeInfoPrefix");
|
|
const pieceListTitle = document.getElementById("pieceListTitle");
|
|
const infoModalTitle = document.getElementById("infoModalTitle");
|
|
const infoModeRuleText = document.getElementById("infoModeRuleText");
|
|
const infoModalProblemHeading = document.getElementById("infoModalProblemHeading");
|
|
const infoModalProblemText = document.getElementById("infoModalProblemText");
|
|
const stepsLabel = document.getElementById("stepsLabel");
|
|
const timeLabel = document.getElementById("timeLabel");
|
|
const statusLabel = document.getElementById("statusLabel");
|
|
const stepCounterValue = document.getElementById("stepCounterValue");
|
|
const timerValue = document.getElementById("timerValue");
|
|
const gameProgressState = document.getElementById("gameProgressState");
|
|
const solvedCelebration = document.getElementById("solvedCelebration");
|
|
const solvedCelebrationTitle = document.getElementById("solvedCelebrationTitle");
|
|
const solvedCelebrationText = document.getElementById("solvedCelebrationText");
|
|
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 btnReset = document.getElementById("btnReset");
|
|
const btnSave = document.getElementById("btnSave");
|
|
const btnLoad = document.getElementById("btnLoad");
|
|
const solverMessage = document.getElementById("solverMessage");
|
|
const solverStepInfo = document.getElementById("solverStepInfo");
|
|
const solverKicker = document.getElementById("solverKicker");
|
|
const solverTitle = document.getElementById("solverTitle");
|
|
const animationSolverTitle = document.getElementById("animationSolverTitle");
|
|
const animationSolverText = document.getElementById("animationSolverText");
|
|
const instantSolverTitle = document.getElementById("instantSolverTitle");
|
|
const instantSolverText = document.getElementById("instantSolverText");
|
|
const solverSpeedInput = document.getElementById("solverSpeedInput");
|
|
const solverSpeedValue = document.getElementById("solverSpeedValue");
|
|
const solverSpeedLabel = document.getElementById("solverSpeedLabel");
|
|
const settingsSidebar = document.getElementById("settingsSidebar");
|
|
const settingsTrigger = document.getElementById("settingsTrigger");
|
|
const settingsTriggerArrow = document.getElementById("settingsTriggerArrow");
|
|
const settingsTriggerLabel = document.getElementById("settingsTriggerLabel");
|
|
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 settingsSidebarTitle = document.getElementById("settingsSidebarTitle");
|
|
const settingsSidebarClose = document.getElementById("settingsSidebarClose");
|
|
const settingsSidebarText = document.getElementById("settingsSidebarText");
|
|
const boardSizeInputLabel = document.getElementById("boardSizeInputLabel");
|
|
const applyBoardSizeButton = document.getElementById("applyBoardSizeButton");
|
|
const settingsSidebarHint = document.getElementById("settingsSidebarHint");
|
|
const savePointsList = document.getElementById("savePointsList");
|
|
const saveNameInput = document.getElementById("saveNameInput");
|
|
const saveNoteInput = document.getElementById("saveNoteInput");
|
|
const saveFavoriteInput = document.getElementById("saveFavoriteInput");
|
|
const saveSidebarTitle = document.getElementById("saveSidebarTitle");
|
|
const saveSidebarClose = document.getElementById("saveSidebarClose");
|
|
const saveSidebarText = document.getElementById("saveSidebarText");
|
|
const saveNameLabel = document.getElementById("saveNameLabel");
|
|
const saveNoteLabel = document.getElementById("saveNoteLabel");
|
|
const saveFavoriteLabel = document.getElementById("saveFavoriteLabel");
|
|
const saveNowButton = document.getElementById("saveNowButton");
|
|
const savePointModeFilter = document.getElementById("savePointModeFilter");
|
|
const savePointSizeFilter = document.getElementById("savePointSizeFilter");
|
|
const savePointSortFilter = document.getElementById("savePointSortFilter");
|
|
const savePointFavoritesOnly = document.getElementById("savePointFavoritesOnly");
|
|
const savePointsSidebarTitle = document.getElementById("savePointsSidebarTitle");
|
|
const savePointsSidebarClose = document.getElementById("savePointsSidebarClose");
|
|
const savePointsSidebarText = document.getElementById("savePointsSidebarText");
|
|
const savePointModeFilterLabel = document.getElementById("savePointModeFilterLabel");
|
|
const savePointSizeFilterLabel = document.getElementById("savePointSizeFilterLabel");
|
|
const savePointSortFilterLabel = document.getElementById("savePointSortFilterLabel");
|
|
const savePointFavoritesOnlyLabel = document.getElementById("savePointFavoritesOnlyLabel");
|
|
const savePointsEmptyState = document.getElementById("savePointsEmptyState");
|
|
const historyKicker = document.getElementById("historyKicker");
|
|
const historyTitle = document.getElementById("historyTitle");
|
|
const historySubtitle = document.getElementById("historySubtitle");
|
|
const btnRefreshHistory = document.getElementById("btnRefreshHistory");
|
|
const historyHeadSave = document.getElementById("historyHeadSave");
|
|
const historyHeadMode = document.getElementById("historyHeadMode");
|
|
const historyHeadBoard = document.getElementById("historyHeadBoard");
|
|
const historyHeadSteps = document.getElementById("historyHeadSteps");
|
|
const historyHeadTime = document.getElementById("historyHeadTime");
|
|
const historyHeadStatus = document.getElementById("historyHeadStatus");
|
|
const historyHeadSaved = document.getElementById("historyHeadSaved");
|
|
const timeHistoryTableBody = document.getElementById("timeHistoryTableBody");
|
|
const infoModal = document.getElementById("infoModal");
|
|
const infoModalBackdrop = document.getElementById("infoModalBackdrop");
|
|
|
|
const SUPPORTED_LANGUAGES = ["de", "en"];
|
|
let currentLanguage = "de";
|
|
|
|
const MODE_COPY = {
|
|
de: {
|
|
queens: {
|
|
singular: "Dame",
|
|
plural: "Damen",
|
|
eyebrow: "Damen-Modus",
|
|
gameTitle: "Damen-Spielbrett",
|
|
boardText: "Platziere Damen so, dass keine zwei dieselbe Spalte oder Diagonale teilen.",
|
|
infoTitle: "Spielregeln und Acht-Damen-Problem",
|
|
infoRuleText: "Im Damen-Modus dürfen sich Damen weder in derselben Spalte noch diagonal angreifen. Mit einem Klick setzt oder entfernst du eine Figur.",
|
|
infoProblemHeading: "Was ist das Acht-Damen-Problem?",
|
|
infoProblemText: "Das klassische Acht-Damen-Problem fragt nach einer Anordnung von acht Damen auf einem 8x8-Brett, bei der sich keine zwei Damen schlagen. Es ist ein bekanntes Beispiel für kombinatorische Suche und Backtracking."
|
|
},
|
|
rooks: {
|
|
singular: "Turm",
|
|
plural: "Türme",
|
|
eyebrow: "Türme-Modus",
|
|
gameTitle: "Türme-Spielbrett",
|
|
boardText: "Platziere Türme so, dass keine zwei dieselbe Spalte teilen.",
|
|
infoTitle: "Spielregeln und Acht-Türme-Problem",
|
|
infoRuleText: "Im Türme-Modus duerfen sich Türme nicht in derselben Spalte stehen. Diagonalen spielen hier keine Rolle. Mit einem Klick setzt oder entfernst du eine Figur.",
|
|
infoProblemHeading: "Was ist das Acht-Türme-Problem?",
|
|
infoProblemText: "Beim Acht-Türme-Problem suchst du eine Anordnung von acht Türmen auf einem 8x8-Brett, bei der sich keine zwei Türme angreifen. Es ist eine einfachere Variante, um systematische Suche und Konfliktprüfung sichtbar zu machen."
|
|
}
|
|
},
|
|
en: {
|
|
queens: {
|
|
singular: "Queen",
|
|
plural: "Queens",
|
|
eyebrow: "Queens Mode",
|
|
gameTitle: "Queens Board",
|
|
boardText: "Place queens so that no two share the same column or diagonal.",
|
|
infoTitle: "Rules and the Eight Queens Puzzle",
|
|
infoRuleText: "In queens mode, queens may not attack each other in the same column or on diagonals. Click once to place or remove a piece.",
|
|
infoProblemHeading: "What is the Eight Queens puzzle?",
|
|
infoProblemText: "The classic Eight Queens puzzle asks for eight queens on an 8x8 board so that no two queens attack each other. It is a well-known example of combinatorial search and backtracking."
|
|
},
|
|
rooks: {
|
|
singular: "Rook",
|
|
plural: "Rooks",
|
|
eyebrow: "Rooks Mode",
|
|
gameTitle: "Rooks Board",
|
|
boardText: "Place rooks so that no two share the same column.",
|
|
infoTitle: "Rules and the Eight Rooks Puzzle",
|
|
infoRuleText: "In rooks mode, rooks may not stand in the same column. Diagonals do not matter here. Click once to place or remove a piece.",
|
|
infoProblemHeading: "What is the Eight Rooks puzzle?",
|
|
infoProblemText: "In the Eight Rooks puzzle, you arrange eight rooks on an 8x8 board so that no two rooks attack each other. It is a simpler variant that helps visualize systematic search and conflict checking."
|
|
}
|
|
}
|
|
};
|
|
|
|
const UI_TRANSLATIONS = {
|
|
de: {
|
|
navDashboard: "Dashboard",
|
|
accountLabel: "Account",
|
|
introKicker: "Puzzle-Modus",
|
|
introTitle: "Wähl deinen Modus und löse das Brett auf deine Art.",
|
|
introText: "Wechsle zwischen Damen und Türmen, nutze Animation oder Sofort-Lösung und öffne die Brett-Einstellungen jederzeit ueber den linken Seiten-Tab.",
|
|
infoButton: "Regeln & Hilfe",
|
|
stepsLabel: "Schritte",
|
|
timeLabel: "Zeit",
|
|
statusLabel: "Status",
|
|
progressRunning: "Läuft",
|
|
progressSolved: "Gelöst",
|
|
boardSizePrefix: ({ modeLabel }) => `Aktuelle Brettgrösse im ${modeLabel}-Modus:`,
|
|
solverKicker: "Solver",
|
|
solverTitle: "Lösungswerkzeuge",
|
|
animationSolverTitle: "Animations-Solver",
|
|
animationSolverText: "Bereite den Lösungsweg vor, gehe ihn manuell Schritt für Schritt durch oder spiele die Animation automatisch ab.",
|
|
instantSolverTitle: "Sofort-Lösung",
|
|
instantSolverText: "Lade direkt eine gültige Lösung, ohne Zwischenschritte oder Animation.",
|
|
defaultSolverMessage: "Bereite einen Ablauf vor und spiele ihn Schritt für Schritt ab.",
|
|
solverReady: ({ modeLabel }) => `Solver für ${modeLabel} bereit. ${t("defaultSolverMessage")}`,
|
|
solverSpeedLabel: "Animationsgeschwindigkeit",
|
|
btnPrepareSolve: "Animation vorbereiten",
|
|
btnPrepareSolveLoading: "Animation wird vorbereitet...",
|
|
btnNextStep: "Nächster Schritt",
|
|
btnPlayPause: "Animation abspielen",
|
|
btnPlayReplay: "Animation erneut abspielen",
|
|
btnPlayPauseRunning: "Animation pausieren",
|
|
btnInstantSolve: "Sofort lösen",
|
|
btnReset: "Brett zurücksetzen",
|
|
settingsTrigger: "Brett-Einstellungen",
|
|
settingsSidebarTitle: "Brett-Einstellungen",
|
|
settingsSidebarClose: "Schliessen",
|
|
settingsSidebarText: "Passe die Brettgroesse zwischen 4 und 13 an.",
|
|
boardSizeInputLabel: "Grösse",
|
|
applyBoardSizeButton: "Grösse anwenden",
|
|
settingsSidebarHint: "Beim ändern wird das Brett zur Sicherheit zurückgesetzt. Auf dem Handy ist 8x8 eingeplant, grössere Bretter können abgeschnitten wirken.",
|
|
btnSave: "Speichern",
|
|
btnLoad: "Save-Points",
|
|
saveSidebarTitle: "Speicherpunkt",
|
|
saveSidebarClose: "Schliessen",
|
|
saveSidebarText: "Gib dem Spielstand einen klaren Namen und optional eine kurze Notiz.",
|
|
saveNameLabel: "Name",
|
|
saveNamePlaceholder: "z. B. Fast gelöste Dame 8x8",
|
|
saveNoteLabel: "Notiz",
|
|
saveNotePlaceholder: "z. B. Gute Startposition fuer Türmetest",
|
|
saveFavoriteLabel: "Als Favorit markieren",
|
|
saveNowButton: "Jetzt speichern",
|
|
savePointsSidebarTitle: "Save-Points",
|
|
savePointsSidebarClose: "Schliessen",
|
|
savePointsSidebarText: "Wähle einen Speicherstand aus der Liste.",
|
|
savePointModeFilterLabel: "Modus",
|
|
savePointSizeFilterLabel: "Brettgrösse",
|
|
savePointSortFilterLabel: "Sortierung",
|
|
savePointFavoritesOnlyLabel: "Nur Favoriten anzeigen",
|
|
savePointsEmptyState: "Noch keine Speicherstände gefunden.",
|
|
filterAll: "Alle",
|
|
filterNewest: "Neueste zuerst",
|
|
filterOldest: "älteste zuerst",
|
|
filterUpdated: "Zuletzt bearbeitet",
|
|
filterFavorites: "Favoriten zuerst",
|
|
historyKicker: "Save-Points",
|
|
historyTitle: "Gespeicherte Zeiten und Fortschritte",
|
|
historySubtitle: "Die Tabelle zeigt gespeicherte Save-Points mit Zeit, Schritten und Status.",
|
|
btnRefreshHistory: "Aktualisieren",
|
|
historyHeadSave: "Save-Point",
|
|
historyHeadMode: "Modus",
|
|
historyHeadBoard: "Brett",
|
|
historyHeadSteps: "Schritte",
|
|
historyHeadTime: "Zeit",
|
|
historyHeadStatus: "Status",
|
|
historyHeadSaved: "Gespeichert",
|
|
pieceListTitle: ({ modeLabel }) => `Positionen der ${modeLabel}`,
|
|
historyEmpty: "Noch keine gespeicherten Zeiten vorhanden.",
|
|
placedWord: "gesetzt",
|
|
noSavePointsFiltered: "Keine Speicherstände fuer diese Filter gefunden.",
|
|
previewEmpty: "Noch keine Figuren gesetzt",
|
|
createdLabel: "Erstellt",
|
|
updatedLabel: "Aktualisiert",
|
|
previewLabel: "Vorschau",
|
|
noteLabel: "Notiz",
|
|
noNote: "Keine Notiz",
|
|
stepsMeta: "Schritte",
|
|
timeMeta: "Zeit",
|
|
solvedMeta: "Gelöst",
|
|
openMeta: "Offen",
|
|
loadButton: "Laden",
|
|
favoriteAddButton: "Als Favorit",
|
|
favoriteRemoveButton: "Favorit entfernen",
|
|
deleteButton: "Löschen",
|
|
celebrationTitle: "Richtig geloöst!",
|
|
celebrationText: ({ modeLabel, duration }) => `Stark gemacht. Deine ${modeLabel.toLowerCase()}-Lösung ist konfliktfrei in ${duration}.`,
|
|
settingsApplied: ({ size }) => `Brettgrösse auf ${size} x ${size} gesetzt.`,
|
|
modeActivated: ({ piece }) => `${piece}-Modus aktiviert.`,
|
|
solverDiscardedManual: "Solver-Ablauf verworfen, weil das Brett manuell geändert wurde.",
|
|
solverDiscardedStatus: "Solver-Ablauf verworfen. Bereite ihn bei Bedarf neu vor.",
|
|
solvedPerfect: ({ duration }) => `Perfekt gelöst in ${duration}.`,
|
|
animationReset: "Animations-Ablauf zurückgesetzt. Starte die Animation erneut oder gehe weiter mit Nächster Schritt.",
|
|
animationPreparing: ({ modeLabel, size }) => `Berechne den Animations-Ablauf für ${modeLabel.toLowerCase()} auf ${size} x ${size}...`,
|
|
animationPreparingStatus: ({ size }) => `Animations-Solver berechnet die Schritte fuer ${size} x ${size}.`,
|
|
noSolution: "Keine Lösung gefunden.",
|
|
noSolutionStatus: "Keine Lösung fuer dieses Brett gefunden.",
|
|
animationReady: ({ steps, queensMode }) => queensMode
|
|
? `Animations-Ablauf bereit mit ${steps} Schritten. Nutze Nächster Schritt oder Animation abspielen, um das Backtracking zu verfolgen.`
|
|
: `Animations-Ablauf bereit mit ${steps} Schritten. Nutze Nächster Schritt oder Animation abspielen.`,
|
|
animationReadyStatus: "Animations-Solver bereit. Nutze Nächster Schritt oder Animation abspielen.",
|
|
animationLoadError: "Der Solver-Ablauf konnte nicht geladen werden.",
|
|
animationFinishedStatus: ({ size }) => `Loesung für ${size} x ${size} Schritt fuer Schritt aufgebaut.`,
|
|
animationAlreadyFinished: "Der Animations-Ablauf ist bereits abgeschlossen.",
|
|
animationPaused: "Animations-Solver pausiert.",
|
|
animationRunning: "Animations-Solver läuft.",
|
|
instantLoading: ({ modeLabel, size }) => `Lade die Sofort-Lösung für ${modeLabel.toLowerCase()} auf ${size} x ${size}...`,
|
|
instantLoadingStatus: "Sofort-Lösung wird geladen.",
|
|
instantLoaded: "Sofort-Lösung geladen. Starte den Animations-Solver, wenn du den Lösungsweg sehen möchtest.",
|
|
instantLoadedStatus: ({ size }) => `Sofort-Loesung für ${size} x ${size} geladen.`,
|
|
instantLoadError: "Die Sofort-Lösung konnte nicht geladen werden.",
|
|
saveError: "Der Speicherstand konnte nicht gespeichert werden.",
|
|
saveSuccess: ({ name }) => `Speicherpunkt "${name}" gespeichert.`,
|
|
loadError: "Dieser Speicherstand konnte nicht geladen werden.",
|
|
loadSuccess: ({ name }) => `Speicherpunkt "${name}" geladen.`,
|
|
favoriteError: "Favorit konnte nicht aktualisiert werden.",
|
|
favoriteSuccess: "Favoritenstatus aktualisiert.",
|
|
deleteConfirm: "Diesen Speicherpunkt wirklich löschen?",
|
|
deleteError: "Speicherpunkt konnte nicht gelöscht werden.",
|
|
deleteSuccess: "Speicherpunkt geloescht.",
|
|
pieceCount: ({ count }) => `${count} gesetzt`,
|
|
pieceEmpty: ({ modeLabel }) => `Noch keine ${modeLabel.toLowerCase()} gesetzt.`,
|
|
saveDefaultName: ({ piece, size, count }) => `${piece} ${size}x${size} - ${count} gesetzt`,
|
|
speedValue: ({ speed }) => `${speed} ms`,
|
|
languageSwitchAria: "Sprache wechseln"
|
|
},
|
|
en: {
|
|
navDashboard: "Dashboard",
|
|
accountLabel: "Account",
|
|
introKicker: "Puzzle Mode",
|
|
introTitle: "Choose your mode and solve the board your way.",
|
|
introText: "Switch between queens and rooks, use animation or instant solve, and open board settings anytime from the left side tab.",
|
|
infoButton: "Rules & Help",
|
|
stepsLabel: "Steps",
|
|
timeLabel: "Time",
|
|
statusLabel: "Status",
|
|
progressRunning: "Running",
|
|
progressSolved: "Solved",
|
|
boardSizePrefix: ({ modeLabel }) => `Current board size in ${modeLabel} mode:`,
|
|
solverKicker: "Solver",
|
|
solverTitle: "Solving Tools",
|
|
animationSolverTitle: "Animation Solver",
|
|
animationSolverText: "Prepare the solving path, step through it manually, or play the animation automatically.",
|
|
instantSolverTitle: "Instant Solve",
|
|
instantSolverText: "Load a valid solution directly, without intermediate steps or animation.",
|
|
defaultSolverMessage: "Prepare a solving trace and play it back step by step.",
|
|
solverReady: ({ modeLabel }) => `Solver for ${modeLabel} is ready. ${t("defaultSolverMessage")}`,
|
|
solverSpeedLabel: "Animation speed",
|
|
btnPrepareSolve: "Prepare animation",
|
|
btnPrepareSolveLoading: "Preparing animation...",
|
|
btnNextStep: "Next step",
|
|
btnPlayPause: "Play animation",
|
|
btnPlayReplay: "Replay animation",
|
|
btnPlayPauseRunning: "Pause animation",
|
|
btnInstantSolve: "Solve instantly",
|
|
btnReset: "Reset board",
|
|
settingsTrigger: "Board settings",
|
|
settingsSidebarTitle: "Board Settings",
|
|
settingsSidebarClose: "Close",
|
|
settingsSidebarText: "Adjust the board size between 4 and 13.",
|
|
boardSizeInputLabel: "Size",
|
|
applyBoardSizeButton: "Apply size",
|
|
settingsSidebarHint: "Changing the size resets the board for safety. On phones, 8x8 is the intended maximum for a reliable fit and larger boards may appear cropped.",
|
|
btnSave: "Save",
|
|
btnLoad: "Save points",
|
|
saveSidebarTitle: "Save Point",
|
|
saveSidebarClose: "Close",
|
|
saveSidebarText: "Give the current run a clear name and optionally a short note.",
|
|
saveNameLabel: "Name",
|
|
saveNamePlaceholder: "e.g. Nearly solved queen 8x8",
|
|
saveNoteLabel: "Note",
|
|
saveNotePlaceholder: "e.g. Strong starting position for rook test",
|
|
saveFavoriteLabel: "Mark as favorite",
|
|
saveNowButton: "Save now",
|
|
savePointsSidebarTitle: "Save Points",
|
|
savePointsSidebarClose: "Close",
|
|
savePointsSidebarText: "Choose a saved state from the list.",
|
|
savePointModeFilterLabel: "Mode",
|
|
savePointSizeFilterLabel: "Board size",
|
|
savePointSortFilterLabel: "Sorting",
|
|
savePointFavoritesOnlyLabel: "Show favorites only",
|
|
savePointsEmptyState: "No saved states yet.",
|
|
filterAll: "All",
|
|
filterNewest: "Newest first",
|
|
filterOldest: "Oldest first",
|
|
filterUpdated: "Recently updated",
|
|
filterFavorites: "Favorites first",
|
|
historyKicker: "Save Points",
|
|
historyTitle: "Saved Times and Progress",
|
|
historySubtitle: "The table shows saved runs with time, steps, and status.",
|
|
btnRefreshHistory: "Refresh",
|
|
historyHeadSave: "Save Point",
|
|
historyHeadMode: "Mode",
|
|
historyHeadBoard: "Board",
|
|
historyHeadSteps: "Steps",
|
|
historyHeadTime: "Time",
|
|
historyHeadStatus: "Status",
|
|
historyHeadSaved: "Saved",
|
|
pieceListTitle: ({ modeLabel }) => `${modeLabel} positions`,
|
|
historyEmpty: "No saved times yet.",
|
|
placedWord: "placed",
|
|
noSavePointsFiltered: "No saved states found for these filters.",
|
|
previewEmpty: "No pieces placed yet",
|
|
createdLabel: "Created",
|
|
updatedLabel: "Updated",
|
|
previewLabel: "Preview",
|
|
noteLabel: "Note",
|
|
noNote: "No note",
|
|
stepsMeta: "Steps",
|
|
timeMeta: "Time",
|
|
solvedMeta: "Solved",
|
|
openMeta: "Open",
|
|
loadButton: "Load",
|
|
favoriteAddButton: "Favorite",
|
|
favoriteRemoveButton: "Remove favorite",
|
|
deleteButton: "Delete",
|
|
celebrationTitle: "Correctly solved!",
|
|
celebrationText: ({ modeLabel, duration }) => `Well done. Your ${modeLabel.toLowerCase()} solution is conflict-free in ${duration}.`,
|
|
settingsApplied: ({ size }) => `Board size set to ${size} x ${size}.`,
|
|
modeActivated: ({ piece }) => `${piece} mode activated.`,
|
|
solverDiscardedManual: "Solver trace discarded because the board was changed manually.",
|
|
solverDiscardedStatus: "Solver trace discarded. Prepare it again if needed.",
|
|
solvedPerfect: ({ duration }) => `Perfectly solved in ${duration}.`,
|
|
animationReset: "Animation trace reset. Start the animation again or continue with Next step.",
|
|
animationPreparing: ({ modeLabel, size }) => `Preparing the animation trace for ${modeLabel.toLowerCase()} on ${size} x ${size}...`,
|
|
animationPreparingStatus: ({ size }) => `Animation solver is preparing the steps for ${size} x ${size}.`,
|
|
noSolution: "No solution found.",
|
|
noSolutionStatus: "No solution found for this board.",
|
|
animationReady: ({ steps, queensMode }) => queensMode
|
|
? `Animation trace ready with ${steps} steps. Use Next step or Play animation to follow the backtracking process.`
|
|
: `Animation trace ready with ${steps} steps. Use Next step or Play animation.`,
|
|
animationReadyStatus: "Animation solver ready. Use Next step or Play animation.",
|
|
animationLoadError: "The solver trace could not be loaded.",
|
|
animationFinishedStatus: ({ size }) => `Solution for ${size} x ${size} was built step by step.`,
|
|
animationAlreadyFinished: "The animation trace is already complete.",
|
|
animationPaused: "Animation solver paused.",
|
|
animationRunning: "Animation solver is running.",
|
|
instantLoading: ({ modeLabel, size }) => `Loading the instant solution for ${modeLabel.toLowerCase()} on ${size} x ${size}...`,
|
|
instantLoadingStatus: "Instant solution is loading.",
|
|
instantLoaded: "Instant solution loaded. Start the animation solver if you want to see the solving path.",
|
|
instantLoadedStatus: ({ size }) => `Instant solution for ${size} x ${size} loaded.`,
|
|
instantLoadError: "The instant solution could not be loaded.",
|
|
saveError: "The save point could not be saved.",
|
|
saveSuccess: ({ name }) => `Save point "${name}" saved.`,
|
|
loadError: "This save point could not be loaded.",
|
|
loadSuccess: ({ name }) => `Save point "${name}" loaded.`,
|
|
favoriteError: "Favorite status could not be updated.",
|
|
favoriteSuccess: "Favorite status updated.",
|
|
deleteConfirm: "Really delete this save point?",
|
|
deleteError: "Save point could not be deleted.",
|
|
deleteSuccess: "Save point deleted.",
|
|
pieceCount: ({ count }) => `${count} placed`,
|
|
pieceEmpty: ({ modeLabel }) => `No ${modeLabel.toLowerCase()} placed yet.`,
|
|
saveDefaultName: ({ piece, size, count }) => `${piece} ${size}x${size} - ${count} placed`,
|
|
speedValue: ({ speed }) => `${speed} ms`,
|
|
languageSwitchAria: "Switch language"
|
|
}
|
|
};
|
|
|
|
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 normalizeLanguage(language) {
|
|
const shortLanguage = String(language || "").trim().slice(0, 2).toLowerCase();
|
|
return SUPPORTED_LANGUAGES.includes(shortLanguage) ? shortLanguage : "de";
|
|
}
|
|
|
|
function getStoredLanguage() {
|
|
try {
|
|
return window.localStorage.getItem("preferredLanguage");
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function storeLanguage(language) {
|
|
try {
|
|
window.localStorage.setItem("preferredLanguage", language);
|
|
} catch (error) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
function detectBrowserLanguage() {
|
|
return normalizeLanguage(navigator.language || navigator.userLanguage || "de");
|
|
}
|
|
|
|
function t(key, params = {}) {
|
|
const value = UI_TRANSLATIONS[currentLanguage][key];
|
|
return typeof value === "function" ? value(params) : value;
|
|
}
|
|
|
|
function getDefaultSolverMessage() {
|
|
return t("defaultSolverMessage");
|
|
}
|
|
|
|
function getModeCopy(selectedMode = mode, language = currentLanguage) {
|
|
return MODE_COPY[language][selectedMode === "rooks" ? "rooks" : "queens"];
|
|
}
|
|
|
|
function sanitizeBoard(candidateBoard, size) {
|
|
if (!Array.isArray(candidateBoard) || candidateBoard.length !== size) {
|
|
return createEmptyBoard(size);
|
|
}
|
|
|
|
return candidateBoard.map((value) => {
|
|
const numericValue = Number(value);
|
|
|
|
if (!Number.isInteger(numericValue)) {
|
|
return EMPTY_CELL;
|
|
}
|
|
|
|
return numericValue >= 0 && numericValue < size ? numericValue : EMPTY_CELL;
|
|
});
|
|
}
|
|
|
|
function formatDuration(totalSeconds) {
|
|
const safeSeconds = Math.max(0, Number(totalSeconds) || 0);
|
|
const hours = Math.floor(safeSeconds / 3600);
|
|
const minutes = Math.floor((safeSeconds % 3600) / 60);
|
|
const seconds = safeSeconds % 60;
|
|
|
|
return [hours, minutes, seconds]
|
|
.map((value) => String(value).padStart(2, "0"))
|
|
.join(":");
|
|
}
|
|
|
|
function getElapsedMilliseconds() {
|
|
if (isSolved) {
|
|
return elapsedBeforeTimerStartMs;
|
|
}
|
|
|
|
return elapsedBeforeTimerStartMs + Math.max(0, Date.now() - timerStartedAtMs);
|
|
}
|
|
|
|
function persistCurrentSession() {
|
|
try {
|
|
window.localStorage.setItem(
|
|
GAME_STORAGE_KEY,
|
|
JSON.stringify({
|
|
mode,
|
|
boardSize,
|
|
board,
|
|
stepCount,
|
|
elapsedMs: getElapsedMilliseconds(),
|
|
isSolved
|
|
})
|
|
);
|
|
} catch (error) {
|
|
// Ignore unavailable storage so the game keeps working normally.
|
|
}
|
|
}
|
|
|
|
function updateProgressDisplay() {
|
|
stepCounterValue.textContent = String(stepCount);
|
|
timerValue.textContent = formatDuration(Math.floor(getElapsedMilliseconds() / 1000));
|
|
gameProgressState.textContent = isSolved ? t("progressSolved") : t("progressRunning");
|
|
gameProgressState.style.color = isSolved ? "#15803d" : "#1d4ed8";
|
|
updateSolvedCelebration();
|
|
}
|
|
|
|
function stopGameTimer() {
|
|
if (gameTimerId !== null) {
|
|
window.clearInterval(gameTimerId);
|
|
gameTimerId = null;
|
|
}
|
|
}
|
|
|
|
function startGameTimer() {
|
|
stopGameTimer();
|
|
|
|
if (isSolved) {
|
|
updateProgressDisplay();
|
|
persistCurrentSession();
|
|
return;
|
|
}
|
|
|
|
timerStartedAtMs = Date.now();
|
|
gameTimerId = window.setInterval(() => {
|
|
updateProgressDisplay();
|
|
persistCurrentSession();
|
|
}, 1000);
|
|
|
|
updateProgressDisplay();
|
|
persistCurrentSession();
|
|
}
|
|
|
|
function resetProgress({ stepCountValue = 0, elapsedMs = 0, solved = false } = {}) {
|
|
stepCount = Math.max(0, Number(stepCountValue) || 0);
|
|
elapsedBeforeTimerStartMs = Math.max(0, Number(elapsedMs) || 0);
|
|
isSolved = Boolean(solved);
|
|
timerStartedAtMs = Date.now();
|
|
|
|
if (isSolved) {
|
|
stopGameTimer();
|
|
updateProgressDisplay();
|
|
persistCurrentSession();
|
|
return;
|
|
}
|
|
|
|
startGameTimer();
|
|
}
|
|
|
|
function ensureFreshRunAfterSolvedBoard() {
|
|
if (!isSolved) {
|
|
return;
|
|
}
|
|
|
|
resetProgress();
|
|
}
|
|
|
|
function updateModeButtons() {
|
|
btnQueens.classList.toggle("active", mode === "queens");
|
|
btnRooks.classList.toggle("active", mode === "rooks");
|
|
btnQueens.classList.toggle("secondary", mode !== "queens");
|
|
btnRooks.classList.toggle("secondary", mode !== "rooks");
|
|
}
|
|
|
|
function updateModeContent() {
|
|
const copy = getModeCopy(mode);
|
|
|
|
gameModeEyebrow.textContent = copy.eyebrow;
|
|
gameTitle.textContent = copy.gameTitle;
|
|
boardModeText.textContent = copy.boardText;
|
|
pieceListTitle.textContent = t("pieceListTitle", { modeLabel: copy.plural });
|
|
infoModalTitle.textContent = copy.infoTitle;
|
|
infoModeRuleText.textContent = copy.infoRuleText;
|
|
infoModalProblemHeading.textContent = copy.infoProblemHeading;
|
|
infoModalProblemText.textContent = copy.infoProblemText;
|
|
boardSizeInfoPrefix.textContent = t("boardSizePrefix", { modeLabel: copy.plural });
|
|
}
|
|
|
|
function updateSolvedCelebration() {
|
|
if (!isSolved) {
|
|
solvedCelebration.hidden = true;
|
|
return;
|
|
}
|
|
|
|
solvedCelebrationTitle.textContent = t("celebrationTitle");
|
|
solvedCelebrationText.textContent = t("celebrationText", {
|
|
modeLabel: getModeLabel(mode),
|
|
duration: formatDuration(Math.floor(getElapsedMilliseconds() / 1000))
|
|
});
|
|
solvedCelebration.hidden = false;
|
|
}
|
|
|
|
function applyLanguageContent() {
|
|
document.documentElement.lang = currentLanguage;
|
|
navDashboardLink.textContent = t("navDashboard");
|
|
accountLabel.textContent = t("accountLabel");
|
|
introKicker.textContent = t("introKicker");
|
|
introTitle.textContent = t("introTitle");
|
|
introText.textContent = t("introText");
|
|
infoButton.textContent = t("infoButton");
|
|
infoButton.setAttribute("aria-label", t("infoButton"));
|
|
stepsLabel.textContent = t("stepsLabel");
|
|
timeLabel.textContent = t("timeLabel");
|
|
statusLabel.textContent = t("statusLabel");
|
|
solverKicker.textContent = t("solverKicker");
|
|
solverTitle.textContent = t("solverTitle");
|
|
animationSolverTitle.textContent = t("animationSolverTitle");
|
|
animationSolverText.textContent = t("animationSolverText");
|
|
instantSolverTitle.textContent = t("instantSolverTitle");
|
|
instantSolverText.textContent = t("instantSolverText");
|
|
solverSpeedLabel.textContent = t("solverSpeedLabel");
|
|
btnNextStep.textContent = t("btnNextStep");
|
|
btnInstantSolve.textContent = t("btnInstantSolve");
|
|
btnReset.textContent = t("btnReset");
|
|
settingsSidebarTitle.textContent = t("settingsSidebarTitle");
|
|
settingsSidebarClose.textContent = t("settingsSidebarClose");
|
|
settingsSidebarText.textContent = t("settingsSidebarText");
|
|
boardSizeInputLabel.textContent = t("boardSizeInputLabel");
|
|
applyBoardSizeButton.textContent = t("applyBoardSizeButton");
|
|
settingsSidebarHint.textContent = t("settingsSidebarHint");
|
|
btnSave.textContent = t("btnSave");
|
|
btnLoad.textContent = t("btnLoad");
|
|
saveSidebarTitle.textContent = t("saveSidebarTitle");
|
|
saveSidebarClose.textContent = t("saveSidebarClose");
|
|
saveSidebarText.textContent = t("saveSidebarText");
|
|
saveNameLabel.textContent = t("saveNameLabel");
|
|
saveNameInput.placeholder = t("saveNamePlaceholder");
|
|
saveNoteLabel.textContent = t("saveNoteLabel");
|
|
saveNoteInput.placeholder = t("saveNotePlaceholder");
|
|
saveFavoriteLabel.textContent = t("saveFavoriteLabel");
|
|
saveNowButton.textContent = t("saveNowButton");
|
|
savePointsSidebarTitle.textContent = t("savePointsSidebarTitle");
|
|
savePointsSidebarClose.textContent = t("savePointsSidebarClose");
|
|
savePointsSidebarText.textContent = t("savePointsSidebarText");
|
|
savePointModeFilterLabel.textContent = t("savePointModeFilterLabel");
|
|
savePointSizeFilterLabel.textContent = t("savePointSizeFilterLabel");
|
|
savePointSortFilterLabel.textContent = t("savePointSortFilterLabel");
|
|
savePointFavoritesOnlyLabel.textContent = t("savePointFavoritesOnlyLabel");
|
|
savePointsEmptyState.textContent = t("savePointsEmptyState");
|
|
historyKicker.textContent = t("historyKicker");
|
|
historyTitle.textContent = t("historyTitle");
|
|
historySubtitle.textContent = t("historySubtitle");
|
|
btnRefreshHistory.textContent = t("btnRefreshHistory");
|
|
historyHeadSave.textContent = t("historyHeadSave");
|
|
historyHeadMode.textContent = t("historyHeadMode");
|
|
historyHeadBoard.textContent = t("historyHeadBoard");
|
|
historyHeadSteps.textContent = t("historyHeadSteps");
|
|
historyHeadTime.textContent = t("historyHeadTime");
|
|
historyHeadStatus.textContent = t("historyHeadStatus");
|
|
historyHeadSaved.textContent = t("historyHeadSaved");
|
|
|
|
savePointModeFilter.options[0].textContent = t("filterAll");
|
|
savePointModeFilter.options[1].textContent = getModeCopy("queens").plural;
|
|
savePointModeFilter.options[2].textContent = getModeCopy("rooks").plural;
|
|
savePointSizeFilter.options[0].textContent = t("filterAll");
|
|
savePointSortFilter.options[0].textContent = t("filterNewest");
|
|
savePointSortFilter.options[1].textContent = t("filterOldest");
|
|
savePointSortFilter.options[2].textContent = t("filterUpdated");
|
|
savePointSortFilter.options[3].textContent = t("filterFavorites");
|
|
|
|
langSwitch.textContent = currentLanguage.toUpperCase();
|
|
langSwitch.setAttribute("aria-label", t("languageSwitchAria"));
|
|
langSwitch.title = currentLanguage === "de" ? "Switch to English" : "Zu Deutsch wechseln";
|
|
|
|
updateModeContent();
|
|
updateQueenList();
|
|
updateProgressDisplay();
|
|
updateSolverSpeed(solverSpeedMs);
|
|
updateSolverControls();
|
|
}
|
|
|
|
function updateSettingsTrigger(isOpen = settingsSidebar.classList.contains("open")) {
|
|
settingsTrigger.setAttribute("aria-expanded", String(isOpen));
|
|
settingsTriggerArrow.textContent = isOpen ? "←" : "→";
|
|
settingsTriggerLabel.textContent = t("settingsTrigger");
|
|
}
|
|
|
|
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 = getDefaultSolverMessage()) {
|
|
solverMessage.textContent = message;
|
|
}
|
|
|
|
function updateSolverSpeed(rawValue) {
|
|
solverSpeedMs = Number(rawValue);
|
|
solverSpeedInput.value = String(solverSpeedMs);
|
|
solverSpeedValue.textContent = t("speedValue", { speed: solverSpeedMs });
|
|
|
|
if (isSolverPlaying()) {
|
|
stopSolverPlayback(false);
|
|
scheduleNextSolverPlaybackStep();
|
|
}
|
|
}
|
|
|
|
function updateBoardLayout() {
|
|
let cellSize = Math.max(32, Math.min(60, Math.floor(480 / boardSize)));
|
|
let labelSize = cellSize;
|
|
|
|
if (window.innerWidth <= 760) {
|
|
const labelRatio = 0.72;
|
|
const availableWidth = Math.max(250, window.innerWidth - 26);
|
|
cellSize = Math.max(24, Math.floor(availableWidth / (boardSize + labelRatio)));
|
|
labelSize = Math.max(18, Math.floor(cellSize * labelRatio));
|
|
}
|
|
|
|
boardDiv.style.setProperty("--board-size", boardSize);
|
|
boardDiv.style.setProperty("--cell-size", `${cellSize}px`);
|
|
boardDiv.style.setProperty("--label-size", `${labelSize}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 ? t("btnPrepareSolveLoading") : t("btnPrepareSolve");
|
|
|
|
if (isSolverPlaying()) {
|
|
btnPlayPause.textContent = t("btnPlayPauseRunning");
|
|
} else if (hasCompletedSolverTrace()) {
|
|
btnPlayPause.textContent = t("btnPlayReplay");
|
|
} else {
|
|
btnPlayPause.textContent = t("btnPlayPause");
|
|
}
|
|
|
|
solverStepInfo.textContent = `Schritt ${currentStep} / ${solverSteps.length}`;
|
|
}
|
|
|
|
function discardSolverTrace(message = getDefaultSolverMessage()) {
|
|
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(t("animationReset"));
|
|
drawBoard();
|
|
updateSolverControls();
|
|
}
|
|
|
|
function setMode(newMode) {
|
|
if (mode === newMode) {
|
|
return;
|
|
}
|
|
|
|
mode = newMode;
|
|
updateModeButtons();
|
|
updateModeContent();
|
|
resetBoard({
|
|
statusMessageText: t("modeActivated", { piece: getPieceLabel(mode) }),
|
|
statusColor: "#2563eb",
|
|
solverMessageText: t("solverReady", { modeLabel: getModeLabel(mode) })
|
|
});
|
|
}
|
|
|
|
function closeAllPanels() {
|
|
toggleSidebar(false);
|
|
toggleSavePanel(false);
|
|
toggleSavePointsPanel(false);
|
|
toggleInfoModal(false);
|
|
}
|
|
|
|
function getConflicts(selectedBoard = board, selectedMode = mode) {
|
|
const conflicts = [];
|
|
|
|
for (let rowA = 0; rowA < boardSize; rowA++) {
|
|
if (selectedBoard[rowA] === EMPTY_CELL) {
|
|
continue;
|
|
}
|
|
|
|
for (let rowB = rowA + 1; rowB < boardSize; rowB++) {
|
|
if (selectedBoard[rowB] === EMPTY_CELL) {
|
|
continue;
|
|
}
|
|
|
|
const colA = selectedBoard[rowA];
|
|
const colB = selectedBoard[rowB];
|
|
const sameColumn = colA === colB;
|
|
const sameDiagonal = Math.abs(colA - colB) === Math.abs(rowA - rowB);
|
|
const hasConflict = selectedMode === "rooks" ? sameColumn : sameColumn || sameDiagonal;
|
|
|
|
if (hasConflict) {
|
|
conflicts.push([rowA, colA], [rowB, colB]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return conflicts;
|
|
}
|
|
|
|
function isBoardSolved(selectedBoard = board, selectedMode = mode) {
|
|
const allPlaced = selectedBoard.every((col) => col !== EMPTY_CELL);
|
|
return allPlaced && getConflicts(selectedBoard, selectedMode).length === 0;
|
|
}
|
|
|
|
function finishSolvedRun() {
|
|
if (isSolved) {
|
|
return;
|
|
}
|
|
|
|
elapsedBeforeTimerStartMs = getElapsedMilliseconds();
|
|
isSolved = true;
|
|
stopGameTimer();
|
|
updateProgressDisplay();
|
|
persistCurrentSession();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
ensureFreshRunAfterSolvedBoard();
|
|
|
|
if (hasPreparedSolverTrace() || isSolverPlaying()) {
|
|
discardSolverTrace(t("solverDiscardedManual"));
|
|
setStatus(t("solverDiscardedStatus"), "#d97706");
|
|
} else {
|
|
setStatus();
|
|
}
|
|
|
|
currentSolverHighlight = null;
|
|
board[row] = board[row] === col ? EMPTY_CELL : col;
|
|
stepCount += 1;
|
|
drawBoard();
|
|
updateProgressDisplay();
|
|
|
|
if (isBoardSolved()) {
|
|
finishSolvedRun();
|
|
setStatus(t("solvedPerfect", { duration: formatDuration(Math.floor(getElapsedMilliseconds() / 1000)) }), "green");
|
|
} else {
|
|
persistCurrentSession();
|
|
}
|
|
}
|
|
|
|
function previewBoardSize(size) {
|
|
pendingBoardSize = clampBoardSize(size);
|
|
sidebarBoardSizeValue.textContent = pendingBoardSize;
|
|
sidebarBoardSizeValueMirror.textContent = pendingBoardSize;
|
|
}
|
|
|
|
function applyBoardSize() {
|
|
boardSize = pendingBoardSize;
|
|
discardSolverTrace(t("solverReady", { modeLabel: getModeLabel(mode) }));
|
|
board = createEmptyBoard(boardSize);
|
|
currentSolverHighlight = null;
|
|
resetProgress();
|
|
updateBoardSizeDisplays(boardSize);
|
|
setStatus(t("settingsApplied", { size: boardSize }), "#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));
|
|
updateSettingsTrigger(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 toggleInfoModal(forceOpen) {
|
|
const shouldOpen = typeof forceOpen === "boolean" ? forceOpen : !infoModal.classList.contains("open");
|
|
|
|
infoModal.classList.toggle("open", shouldOpen);
|
|
infoModalBackdrop.classList.toggle("visible", shouldOpen);
|
|
infoModal.setAttribute("aria-hidden", String(!shouldOpen));
|
|
}
|
|
|
|
function getPieceLabel(selectedMode) {
|
|
return getModeCopy(selectedMode).singular;
|
|
}
|
|
|
|
function getPlacedPieceCount() {
|
|
return board.filter((col) => col !== EMPTY_CELL).length;
|
|
}
|
|
|
|
function resetSaveForm() {
|
|
saveNameInput.value = t("saveDefaultName", {
|
|
piece: getPieceLabel(mode),
|
|
size: boardSize,
|
|
count: getPlacedPieceCount()
|
|
});
|
|
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(", ") : t("previewEmpty");
|
|
}
|
|
|
|
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 getModeLabel(selectedMode) {
|
|
return getModeCopy(selectedMode).plural;
|
|
}
|
|
|
|
function renderSavePoints(savePoints) {
|
|
savePointsList.innerHTML = "";
|
|
|
|
if (savePoints.length === 0) {
|
|
savePointsList.innerHTML = `<p class="sidebar-hint">${t("noSavePointsFiltered")}</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 = `${t("createdLabel")}: ${savePoint.created_at}`;
|
|
updatedAt.className = "save-point-meta";
|
|
updatedAt.textContent = `${t("updatedLabel")}: ${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} ${t("placedWord")}`;
|
|
preview.className = "save-point-preview";
|
|
preview.textContent = `${t("previewLabel")}: ${getBoardPreview(savePoint.board)}`;
|
|
note.className = "save-point-note";
|
|
note.textContent = savePoint.save_note ? `${t("noteLabel")}: ${savePoint.save_note}` : `${t("noteLabel")}: ${t("noNote")}`;
|
|
actions.className = "save-point-actions";
|
|
|
|
const progressMeta = document.createElement("p");
|
|
progressMeta.className = "save-point-meta";
|
|
progressMeta.textContent = `${t("stepsMeta")}: ${savePoint.step_count || 0} | ${t("timeMeta")}: ${formatDuration(savePoint.elapsed_seconds)} | ${t("statusLabel")}: ${savePoint.is_solved ? t("solvedMeta") : t("openMeta")}`;
|
|
|
|
actions.appendChild(createSavePointActionButton(t("loadButton"), "btn", () => loadSavePoint(savePoint.id)));
|
|
actions.appendChild(createSavePointActionButton(
|
|
savePoint.is_favorite ? t("favoriteRemoveButton") : t("favoriteAddButton"),
|
|
"btn secondary",
|
|
() => toggleSavePointFavorite(savePoint.id)
|
|
));
|
|
actions.appendChild(createSavePointActionButton(t("deleteButton"), "btn ghost", () => deleteSavePoint(savePoint.id)));
|
|
|
|
item.appendChild(title);
|
|
item.appendChild(createdAt);
|
|
item.appendChild(updatedAt);
|
|
item.appendChild(boardMeta);
|
|
item.appendChild(progressMeta);
|
|
item.appendChild(preview);
|
|
item.appendChild(note);
|
|
item.appendChild(actions);
|
|
savePointsList.appendChild(item);
|
|
}
|
|
}
|
|
|
|
function renderTimeHistory(savePoints) {
|
|
timeHistoryTableBody.innerHTML = "";
|
|
|
|
if (savePoints.length === 0) {
|
|
timeHistoryTableBody.innerHTML = `<tr><td colspan="7" class="history-empty">${t("historyEmpty")}</td></tr>`;
|
|
return;
|
|
}
|
|
|
|
for (const savePoint of savePoints) {
|
|
const row = document.createElement("tr");
|
|
const values = [
|
|
`${savePoint.is_favorite ? "★ " : ""}${savePoint.save_name}`,
|
|
getModeLabel(savePoint.mode),
|
|
`${savePoint.board_size} x ${savePoint.board_size}`,
|
|
String(savePoint.step_count || 0),
|
|
formatDuration(savePoint.elapsed_seconds),
|
|
savePoint.is_solved ? t("solvedMeta") : t("openMeta"),
|
|
savePoint.updated_at
|
|
];
|
|
|
|
for (const value of values) {
|
|
const cell = document.createElement("td");
|
|
cell.textContent = value;
|
|
row.appendChild(cell);
|
|
}
|
|
|
|
timeHistoryTableBody.appendChild(row);
|
|
}
|
|
}
|
|
|
|
async function refreshSavePoints() {
|
|
const queryString = getSavePointFilters();
|
|
const response = await fetch(`/save_points${queryString ? `?${queryString}` : ""}`);
|
|
const savePoints = await response.json();
|
|
renderSavePoints(savePoints);
|
|
}
|
|
|
|
async function refreshTimeHistory() {
|
|
const response = await fetch("/save_points?sort=updated");
|
|
|
|
if (!response.ok) {
|
|
return;
|
|
}
|
|
|
|
const savePoints = await response.json();
|
|
renderTimeHistory(savePoints);
|
|
}
|
|
|
|
async function openSavePointsPanel() {
|
|
await refreshSavePoints();
|
|
toggleSavePointsPanel(true);
|
|
}
|
|
|
|
function applyLoadedGame(data) {
|
|
discardSolverTrace(t("solverReady", { modeLabel: getModeLabel(data.mode || "queens") }));
|
|
|
|
const restoredBoardSize = Array.isArray(data.board) ? clampBoardSize(data.board.length) : DEFAULT_BOARD_SIZE;
|
|
|
|
boardSize = restoredBoardSize;
|
|
pendingBoardSize = boardSize;
|
|
mode = data.mode === "rooks" ? "rooks" : "queens";
|
|
board = sanitizeBoard(data.board, boardSize);
|
|
currentSolverHighlight = null;
|
|
resetProgress({
|
|
stepCountValue: data.step_count,
|
|
elapsedMs: Number(data.elapsed_seconds || 0) * 1000,
|
|
solved: data.is_solved || isBoardSolved(board, mode)
|
|
});
|
|
|
|
updateModeButtons();
|
|
updateModeContent();
|
|
updateBoardSizeDisplays(boardSize);
|
|
drawBoard();
|
|
updateProgressDisplay();
|
|
persistCurrentSession();
|
|
}
|
|
|
|
function applySolverStep(step) {
|
|
board = sanitizeBoard(step.board, 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 || getDefaultSolverMessage());
|
|
drawBoard();
|
|
persistCurrentSession();
|
|
updateSolverControls();
|
|
}
|
|
|
|
function advanceSolverStep() {
|
|
if (!hasPreparedSolverTrace() || hasCompletedSolverTrace()) {
|
|
stopSolverPlayback(false);
|
|
updateSolverControls();
|
|
return false;
|
|
}
|
|
|
|
solverStepIndex += 1;
|
|
applySolverStep(solverSteps[solverStepIndex]);
|
|
|
|
if (hasCompletedSolverTrace()) {
|
|
finishSolvedRun();
|
|
stopSolverPlayback(false);
|
|
setStatus(t("animationFinishedStatus", { size: boardSize }), "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(t("animationPreparing", { modeLabel: getModeLabel(mode), size: boardSize }));
|
|
setStatus(t("animationPreparingStatus", { size: 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(t("noSolution"));
|
|
setStatus(t("noSolutionStatus"), "red");
|
|
drawBoard();
|
|
persistCurrentSession();
|
|
return false;
|
|
}
|
|
|
|
setSolverMessage(t("animationReady", { steps: solverSteps.length, queensMode: mode === "queens" }));
|
|
setStatus(t("animationReadyStatus"), "#2563eb");
|
|
drawBoard();
|
|
return true;
|
|
} catch (error) {
|
|
if (requestId !== solverRequestId) {
|
|
return false;
|
|
}
|
|
|
|
setSolverMessage(t("animationLoadError"));
|
|
setStatus(t("animationLoadError"), "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(t("animationAlreadyFinished"), "#2563eb");
|
|
}
|
|
}
|
|
|
|
async function toggleSolverPlayback() {
|
|
if (solverLoading) {
|
|
return;
|
|
}
|
|
|
|
if (isSolverPlaying()) {
|
|
stopSolverPlayback();
|
|
setStatus(t("animationPaused"), "#2563eb");
|
|
return;
|
|
}
|
|
|
|
if (!hasPreparedSolverTrace()) {
|
|
const prepared = await prepareSolutionTrace();
|
|
|
|
if (!prepared) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (hasCompletedSolverTrace()) {
|
|
rewindSolverTrace();
|
|
}
|
|
|
|
setStatus(t("animationRunning"), "#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(t("instantLoading", { modeLabel: getModeLabel(mode), size: boardSize }));
|
|
setStatus(t("instantLoadingStatus"), "#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;
|
|
finishSolvedRun();
|
|
setSolverMessage(t("instantLoaded"));
|
|
setStatus(t("instantLoadedStatus", { size: boardSize }), "green");
|
|
drawBoard();
|
|
persistCurrentSession();
|
|
} catch (error) {
|
|
if (requestId !== solverRequestId) {
|
|
return;
|
|
}
|
|
|
|
setSolverMessage(t("instantLoadError"));
|
|
setStatus(t("instantLoadError"), "red");
|
|
} finally {
|
|
if (requestId === solverRequestId) {
|
|
solverLoading = false;
|
|
updateSolverControls();
|
|
}
|
|
}
|
|
}
|
|
|
|
function resetBoard(options = {}) {
|
|
const {
|
|
statusMessageText = "",
|
|
statusColor = "",
|
|
solverMessageText = t("solverReady", { modeLabel: getModeLabel(mode) })
|
|
} = options;
|
|
|
|
discardSolverTrace(solverMessageText);
|
|
board = createEmptyBoard(boardSize);
|
|
currentSolverHighlight = null;
|
|
resetProgress();
|
|
|
|
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 = `${getPieceLabel(mode)} ${count}: ${position}`;
|
|
queenList.appendChild(li);
|
|
count += 1;
|
|
}
|
|
|
|
const placedPieces = count - 1;
|
|
pieceCountBadge.textContent = t("pieceCount", { count: placedPieces });
|
|
|
|
if (placedPieces === 0) {
|
|
const emptyItem = document.createElement("li");
|
|
emptyItem.className = "empty";
|
|
emptyItem.textContent = t("pieceEmpty", { modeLabel: getModeLabel(mode) });
|
|
queenList.appendChild(emptyItem);
|
|
}
|
|
}
|
|
|
|
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,
|
|
stepCount,
|
|
elapsedSeconds: Math.floor(getElapsedMilliseconds() / 1000),
|
|
isSolved
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
setStatus(t("saveError"), "red");
|
|
return;
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
toggleSavePanel(false);
|
|
await refreshSavePoints();
|
|
await refreshTimeHistory();
|
|
setStatus(t("saveSuccess", { name: data.save_point.save_name }), "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(t("loadError"), "red");
|
|
return;
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
applyLoadedGame(data);
|
|
setStatus(t("loadSuccess", { name: data.save_name }), "green");
|
|
toggleSavePointsPanel(false);
|
|
}
|
|
|
|
async function toggleSavePointFavorite(savePointId) {
|
|
const response = await fetch(`/save_points/${savePointId}/favorite`, {
|
|
method: "POST"
|
|
});
|
|
|
|
if (!response.ok) {
|
|
setStatus(t("favoriteError"), "red");
|
|
return;
|
|
}
|
|
|
|
await refreshSavePoints();
|
|
await refreshTimeHistory();
|
|
setStatus(t("favoriteSuccess"), "#2563eb");
|
|
}
|
|
|
|
async function deleteSavePoint(savePointId) {
|
|
const confirmed = window.confirm(t("deleteConfirm"));
|
|
|
|
if (!confirmed) {
|
|
return;
|
|
}
|
|
|
|
const response = await fetch(`/save_points/${savePointId}`, {
|
|
method: "DELETE"
|
|
});
|
|
|
|
if (!response.ok) {
|
|
setStatus(t("deleteError"), "red");
|
|
return;
|
|
}
|
|
|
|
await refreshSavePoints();
|
|
await refreshTimeHistory();
|
|
setStatus(t("deleteSuccess"), "#2563eb");
|
|
}
|
|
|
|
function restoreSessionFromStorage() {
|
|
try {
|
|
const rawSession = window.localStorage.getItem(GAME_STORAGE_KEY);
|
|
|
|
if (!rawSession) {
|
|
return false;
|
|
}
|
|
|
|
const savedSession = JSON.parse(rawSession);
|
|
const restoredBoardSize = clampBoardSize(savedSession.boardSize || DEFAULT_BOARD_SIZE);
|
|
const restoredMode = savedSession.mode === "rooks" ? "rooks" : "queens";
|
|
|
|
boardSize = restoredBoardSize;
|
|
pendingBoardSize = restoredBoardSize;
|
|
mode = restoredMode;
|
|
board = sanitizeBoard(savedSession.board, restoredBoardSize);
|
|
currentSolverHighlight = null;
|
|
|
|
updateModeButtons();
|
|
updateModeContent();
|
|
updateBoardSizeDisplays(boardSize);
|
|
drawBoard();
|
|
resetProgress({
|
|
stepCountValue: savedSession.stepCount,
|
|
elapsedMs: savedSession.elapsedMs,
|
|
solved: savedSession.isSolved || isBoardSolved(board, mode)
|
|
});
|
|
|
|
return true;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function initializeDashboard() {
|
|
currentLanguage = normalizeLanguage(getStoredLanguage() || detectBrowserLanguage());
|
|
pendingBoardSize = boardSize;
|
|
updateModeButtons();
|
|
updateBoardSizeDisplays(boardSize);
|
|
applyLanguageContent();
|
|
updateSettingsTrigger(false);
|
|
setSolverMessage(t("solverReady", { modeLabel: getModeLabel(mode) }));
|
|
|
|
if (!restoreSessionFromStorage()) {
|
|
resetProgress();
|
|
drawBoard();
|
|
}
|
|
|
|
updateProgressDisplay();
|
|
await refreshTimeHistory();
|
|
}
|
|
|
|
if (langSwitch) {
|
|
langSwitch.addEventListener("click", async () => {
|
|
currentLanguage = currentLanguage === "de" ? "en" : "de";
|
|
storeLanguage(currentLanguage);
|
|
applyLanguageContent();
|
|
resetSaveForm();
|
|
await refreshTimeHistory();
|
|
|
|
if (savePointsSidebar.classList.contains("open")) {
|
|
await refreshSavePoints();
|
|
}
|
|
});
|
|
}
|
|
|
|
window.addEventListener("keydown", (event) => {
|
|
if (event.key === "Escape") {
|
|
closeAllPanels();
|
|
}
|
|
});
|
|
|
|
window.addEventListener("resize", () => {
|
|
drawBoard();
|
|
});
|
|
|
|
initializeDashboard();
|