Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Premium Sudoku Pro</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| /* Custom styles for Sudoku board */ | |
| .cell { | |
| width: 50px; | |
| height: 50px; | |
| font-size: 1.5rem; | |
| text-align: center; | |
| border: 1px solid #cbd5e0; | |
| position: relative; | |
| transition: all 0.2s ease; | |
| } | |
| .cell:focus { | |
| outline: none; | |
| background-color: #ebf8ff; | |
| } | |
| .cell:nth-child(3n) { | |
| border-right: 2px solid #4a5568; | |
| } | |
| .cell:nth-child(9n) { | |
| border-right: 1px solid #cbd5e0; | |
| } | |
| .row:nth-child(3n) .cell { | |
| border-bottom: 2px solid #4a5568; | |
| } | |
| .row:nth-child(9n) .cell { | |
| border-bottom: 1px solid #cbd5e0; | |
| } | |
| .given { | |
| font-weight: bold; | |
| color: #2d3748; | |
| background-color: #f7fafc; | |
| } | |
| .error { | |
| color: #e53e3e; | |
| animation: shake 0.5s; | |
| } | |
| @keyframes shake { | |
| 0%, 100% { transform: translateX(0); } | |
| 10%, 30%, 50%, 70%, 90% { transform: translateX(-2px); } | |
| 20%, 40%, 60%, 80% { transform: translateX(2px); } | |
| } | |
| .highlight { | |
| background-color: #bee3f8; | |
| } | |
| .selected { | |
| background-color: #90cdf4; | |
| } | |
| .same-number { | |
| background-color: #d6bcfa; | |
| } | |
| .peers { | |
| background-color: #e9d8fd; | |
| } | |
| .conflict { | |
| background-color: #fed7d7; | |
| } | |
| .notes-grid { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| grid-template-rows: repeat(3, 1fr); | |
| width: 100%; | |
| height: 100%; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| font-size: 0.7rem; | |
| color: #6b46c1; | |
| } | |
| .notes-corner { | |
| position: absolute; | |
| font-size: 0.7rem; | |
| color: #6b46c1; | |
| } | |
| .corner-number { | |
| position: absolute; | |
| width: 10px; | |
| height: 10px; | |
| } | |
| .cell-value { | |
| z-index: 2; | |
| position: relative; | |
| } | |
| .highlight-mode { | |
| box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5); | |
| } | |
| .stats-bar { | |
| height: 6px; | |
| border-radius: 3px; | |
| } | |
| @media (max-width: 640px) { | |
| .cell { | |
| width: 35px; | |
| height: 35px; | |
| font-size: 1.2rem; | |
| } | |
| .notes-grid { | |
| font-size: 0.5rem; | |
| } | |
| .notes-corner { | |
| font-size: 0.5rem; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-100 min-h-screen flex flex-col items-center justify-center p-4"> | |
| <div class="max-w-6xl w-full bg-white rounded-xl shadow-xl p-6"> | |
| <header class="text-center mb-6"> | |
| <h1 class="text-4xl font-bold text-indigo-700 mb-2 flex items-center justify-center"> | |
| <i class="fas fa-th mr-3"></i> Premium Sudoku Pro | |
| </h1> | |
| <div class="flex flex-wrap justify-center items-center gap-4"> | |
| <div class="flex items-center"> | |
| <span class="mr-2 text-gray-700 font-medium">Difficulty:</span> | |
| <select id="difficulty" class="border rounded px-3 py-1 bg-white shadow-sm"> | |
| <option value="easy">Easy</option> | |
| <option value="medium">Medium</option> | |
| <option value="hard">Hard</option> | |
| <option value="expert">Expert</option> | |
| <option value="insane">Insane</option> | |
| </select> | |
| </div> | |
| <button id="new-game" class="bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 transition flex items-center shadow-md"> | |
| <i class="fas fa-sync-alt mr-2"></i>New Game | |
| </button> | |
| <div class="flex items-center bg-indigo-50 px-3 py-1 rounded-lg"> | |
| <span class="mr-2 text-gray-700 font-medium">Theme:</span> | |
| <button id="theme-light" class="w-6 h-6 rounded-full bg-white border-2 border-gray-300 mx-1 shadow-sm"></button> | |
| <button id="theme-dark" class="w-6 h-6 rounded-full bg-gray-800 border-2 border-gray-700 mx-1 shadow-sm"></button> | |
| <button id="theme-blue" class="w-6 h-6 rounded-full bg-blue-500 border-2 border-blue-700 mx-1 shadow-sm"></button> | |
| </div> | |
| <button id="fullscreen-btn" class="text-gray-700 hover:text-indigo-700 p-2 rounded-lg hover:bg-gray-100 transition"> | |
| <i class="fas fa-expand"></i> | |
| </button> | |
| </div> | |
| </header> | |
| <div class="flex flex-col lg:flex-row items-center lg:items-start justify-center gap-8"> | |
| <div class="flex-1"> | |
| <div class="bg-white p-3 rounded-xl border-2 border-gray-300 shadow-inner relative"> | |
| <div id="board" class="grid grid-cols-9 gap-0"></div> | |
| <div class="absolute top-2 right-2 flex space-x-2"> | |
| <button id="undo-btn" class="text-gray-500 hover:text-indigo-700" title="Undo"> | |
| <i class="fas fa-undo"></i> | |
| </button> | |
| <button id="redo-btn" class="text-gray-500 hover:text-indigo-700" title="Redo"> | |
| <i class="fas fa-redo"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mt-4 grid grid-cols-3 gap-4"> | |
| <div class="bg-gray-50 p-3 rounded-lg shadow text-center"> | |
| <div class="text-sm text-gray-600 mb-1">Time</div> | |
| <div id="timer" class="font-mono text-xl font-bold">00:00</div> | |
| </div> | |
| <div class="bg-gray-50 p-3 rounded-lg shadow text-center"> | |
| <div class="text-sm text-gray-600 mb-1">Mistakes</div> | |
| <div id="mistakes" class="text-xl font-bold">0/3</div> | |
| <div class="stats-bar bg-gray-200 mt-1"> | |
| <div id="mistakes-bar" class="stats-bar bg-red-500 h-full w-0"></div> | |
| </div> | |
| </div> | |
| <div class="bg-gray-50 p-3 rounded-lg shadow text-center"> | |
| <div class="text-sm text-gray-600 mb-1">Hints</div> | |
| <div id="hints" class="text-xl font-bold">3</div> | |
| <div class="stats-bar bg-gray-200 mt-1"> | |
| <div id="hints-bar" class="stats-bar bg-green-500 h-full w-full"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex flex-col space-y-6 w-full lg:w-80"> | |
| <div class="bg-gray-50 p-5 rounded-xl shadow-lg"> | |
| <h2 class="text-xl font-semibold text-gray-800 mb-3 flex items-center"> | |
| <i class="fas fa-gamepad mr-2"></i> Controls | |
| </h2> | |
| <div class="grid grid-cols-3 gap-2 mb-4"> | |
| <button class="number-btn bg-indigo-100 hover:bg-indigo-200 text-indigo-800 font-bold py-4 rounded-lg transition transform hover:scale-105 active:scale-95 shadow-sm" data-number="1">1</button> | |
| <button class="number-btn bg-indigo-100 hover:bg-indigo-200 text-indigo-800 font-bold py-4 rounded-lg transition transform hover:scale-105 active:scale-95 shadow-sm" data-number="2">2</button> | |
| <button class="number-btn bg-indigo-100 hover:bg-indigo-200 text-indigo-800 font-bold py-4 rounded-lg transition transform hover:scale-105 active:scale-95 shadow-sm" data-number="3">3</button> | |
| <button class="number-btn bg-indigo-100 hover:bg-indigo-200 text-indigo-800 font-bold py-4 rounded-lg transition transform hover:scale-105 active:scale-95 shadow-sm" data-number="4">4</button> | |
| <button class="number-btn bg-indigo-100 hover:bg-indigo-200 text-indigo-800 font-bold py-4 rounded-lg transition transform hover:scale-105 active:scale-95 shadow-sm" data-number="5">5</button> | |
| <button class="number-btn bg-indigo-100 hover:bg-indigo-200 text-indigo-800 font-bold py-4 rounded-lg transition transform hover:scale-105 active:scale-95 shadow-sm" data-number="6">6</button> | |
| <button class="number-btn bg-indigo-100 hover:bg-indigo-200 text-indigo-800 font-bold py-4 rounded-lg transition transform hover:scale-105 active:scale-95 shadow-sm" data-number="7">7</button> | |
| <button class="number-btn bg-indigo-100 hover:bg-indigo-200 text-indigo-800 font-bold py-4 rounded-lg transition transform hover:scale-105 active:scale-95 shadow-sm" data-number="8">8</button> | |
| <button class="number-btn bg-indigo-100 hover:bg-indigo-200 text-indigo-800 font-bold py-4 rounded-lg transition transform hover:scale-105 active:scale-95 shadow-sm" data-number="9">9</button> | |
| </div> | |
| <div class="flex space-x-3"> | |
| <button id="erase-btn" class="flex-1 bg-red-100 hover:bg-red-200 text-red-800 font-bold py-3 rounded-lg transition flex items-center justify-center shadow-sm"> | |
| <i class="fas fa-eraser mr-2"></i>Erase | |
| </button> | |
| <button id="notes-btn" class="flex-1 bg-yellow-100 hover:bg-yellow-200 text-yellow-800 font-bold py-3 rounded-lg transition flex items-center justify-center shadow-sm"> | |
| <i class="fas fa-pencil-alt mr-2"></i>Notes | |
| </button> | |
| <button id="hint-btn" class="flex-1 bg-green-100 hover:bg-green-200 text-green-800 font-bold py-3 rounded-lg transition flex items-center justify-center shadow-sm"> | |
| <i class="fas fa-lightbulb mr-2"></i>Hint | |
| </button> | |
| </div> | |
| <div class="mt-4 pt-3 border-t border-gray-200"> | |
| <h3 class="text-sm font-medium text-gray-700 mb-2">Notes Style:</h3> | |
| <div class="flex space-x-2"> | |
| <button data-note-style="grid" class="note-style-btn px-2 py-1 text-xs rounded border bg-white hover:bg-gray-50 transition active:bg-gray-100"> | |
| Grid | |
| </button> | |
| <button data-note-style="corner" class="note-style-btn px-2 py-1 text-xs rounded border bg-white hover:bg-gray-50 transition active:bg-gray-100"> | |
| Corners | |
| </button> | |
| <button data-note-style="hidden" class="note-style-btn px-2 py-1 text-xs rounded border bg-white hover:bg-gray-50 transition active:bg-gray-100"> | |
| Hidden | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mt-3 pt-3 border-t border-gray-200"> | |
| <h3 class="text-sm font-medium text-gray-700 mb-2">Highlight:</h3> | |
| <div class="flex flex-wrap gap-2"> | |
| <button data-highlight="number" class="highlight-btn px-2 py-1 text-xs rounded border bg-white hover:bg-gray-50 transition active:bg-gray-100"> | |
| Number | |
| </button> | |
| <button data-highlight="peers" class="highlight-btn px-2 py-1 text-xs rounded border bg-white hover:bg-gray-50 transition active:bg-gray-100"> | |
| Peers | |
| </button> | |
| <button data-highlight="conflicts" class="highlight-btn px-2 py-1 text-xs rounded border bg-white hover:bg-gray-50 transition active:bg-gray-100"> | |
| Conflicts | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-gray-50 p-5 rounded-xl shadow-lg"> | |
| <h2 class="text-xl font-semibold text-gray-800 mb-3 flex items-center"> | |
| <i class="fas fa-chart-line mr-2"></i> Statistics | |
| </h2> | |
| <div class="space-y-3"> | |
| <div> | |
| <div class="flex justify-between text-sm text-gray-600 mb-1"> | |
| <span>Games Played:</span> | |
| <span id="games-played">0</span> | |
| </div> | |
| <div class="stats-bar bg-gray-200"> | |
| <div id="games-played-bar" class="stats-bar bg-indigo-500 h-full w-0"></div> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="flex justify-between text-sm text-gray-600 mb-1"> | |
| <span>Win Rate:</span> | |
| <span id="win-rate">0%</span> | |
| </div> | |
| <div class="stats-bar bg-gray-200"> | |
| <div id="win-rate-bar" class="stats-bar bg-green-500 h-full w-0"></div> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="flex justify-between text-sm text-gray-600 mb-1"> | |
| <span>Average Time:</span> | |
| <span id="average-time">00:00</span> | |
| </div> | |
| <div class="stats-bar bg-gray-200"> | |
| <div id="average-time-bar" class="stats-bar bg-blue-500 h-full w-0"></div> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="flex justify-between text-sm text-gray-600 mb-1"> | |
| <span>Best Time:</span> | |
| <span id="best-time">--:--</span> | |
| </div> | |
| <div class="stats-bar bg-gray-200"> | |
| <div id="best-time-bar" class="stats-bar bg-yellow-500 h-full w-0"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="message" class="mt-6 text-center hidden"> | |
| <div id="message-content" class="inline-block px-8 py-4 rounded-xl font-bold text-white shadow-lg"></div> | |
| </div> | |
| <div id="help-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 hidden z-50"> | |
| <div class="bg-white rounded-xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"> | |
| <div class="p-6"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-2xl font-bold text-indigo-700">Sudoku Pro Help</h2> | |
| <button id="close-help" class="text-gray-500 hover:text-gray-700"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div class="space-y-4"> | |
| <div> | |
| <h3 class="text-lg font-semibold mb-2">How to Play</h3> | |
| <p class="text-gray-700"> | |
| Fill the 9×9 grid with digits so that each column, each row, and each of the nine 3×3 subgrids | |
| that compose the grid (also called "boxes", "blocks", or "regions") contains all of the digits from 1 to 9. | |
| </p> | |
| </div> | |
| <div> | |
| <h3 class="text-lg font-semibold mb-2">Controls</h3> | |
| <ul class="list-disc pl-5 space-y-2 text-gray-700"> | |
| <li><strong>Number Buttons</strong>: Fill the selected cell</li> | |
| <li><strong>Notes Mode</strong>: Toggle to enter small candidate numbers</li> | |
| <li><strong>Erase</strong>: Clear the selected cell</li> | |
| <li><strong>Hint</strong>: Reveals the correct number (limited)</li> | |
| <li><strong>Undo/Redo</strong>: Revert or replay your moves</li> | |
| </ul> | |
| </div> | |
| <div> | |
| <h3 class="text-lg font-semibold mb-2">Advanced Features</h3> | |
| <ul class="list-disc pl-5 space-y-2 text-gray-700"> | |
| <li><strong>Notes Styles</strong>: Choose between grid, corner, or hidden notes</li> | |
| <li><strong>Highlighting</strong>: Visualize number conflicts and relationships</li> | |
| <li><strong>Statistics</strong>: Track your performance over time</li> | |
| <li><strong>Multiple Themes</strong>: Light, dark, and blue color schemes</li> | |
| </ul> | |
| </div> | |
| </div> | |
| <div class="mt-6 pt-4 border-t border-gray-200 flex justify-end"> | |
| <button id="ok-help" class="bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 transition"> | |
| Got It! | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="absolute bottom-4 right-4"> | |
| <button id="help-btn" class="w-12 h-12 bg-indigo-600 text-white rounded-full shadow-lg hover:bg-indigo-700 transition flex items-center justify-center"> | |
| <i class="fas fa-question text-xl"></i> | |
| </button> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Enhanced game state | |
| const gameState = { | |
| board: Array(9).fill().map(() => Array(9).fill(0)), | |
| solution: Array(9).fill().map(() => Array(9).fill(0)), | |
| givenCells: Array(9).fill().map(() => Array(9).fill(false)), | |
| notes: Array(9).fill().map(() => Array(9).fill().map(() => new Set())), | |
| selectedCell: null, | |
| notesMode: false, | |
| noteStyle: 'grid', // 'grid' or 'corner' or 'hidden' | |
| highlightMode: 'number', // 'number' or 'peers' or 'conflicts' | |
| mistakes: 0, | |
| hints: 3, | |
| timer: 0, | |
| timerInterval: null, | |
| gameOver: false, | |
| movesHistory: [], | |
| historyPointer: -1, | |
| stats: { | |
| gamesPlayed: 0, | |
| gamesWon: 0, | |
| totalTime: 0, | |
| bestTime: null, | |
| difficultyStats: { | |
| easy: { played: 0, won: 0, bestTime: null }, | |
| medium: { played: 0, won: 0, bestTime: null }, | |
| hard: { played: 0, won: 0, bestTime: null }, | |
| expert: { played: 0, won: 0, bestTime: null }, | |
| insane: { played: 0, won: 0, bestTime: null } | |
| } | |
| }, | |
| currentTheme: 'light' | |
| }; | |
| // DOM elements | |
| const boardElement = document.getElementById('board'); | |
| const numberButtons = document.querySelectorAll('.number-btn'); | |
| const eraseButton = document.getElementById('erase-btn'); | |
| const notesButton = document.getElementById('notes-btn'); | |
| const hintButton = document.getElementById('hint-btn'); | |
| const newGameButton = document.getElementById('new-game'); | |
| const difficultySelect = document.getElementById('difficulty'); | |
| const timerElement = document.getElementById('timer'); | |
| const mistakesElement = document.getElementById('mistakes'); | |
| const hintsElement = document.getElementById('hints'); | |
| const messageElement = document.getElementById('message'); | |
| const messageContent = document.getElementById('message-content'); | |
| const noteStyleButtons = document.querySelectorAll('.note-style-btn'); | |
| const highlightButtons = document.querySelectorAll('.highlight-btn'); | |
| const undoButton = document.getElementById('undo-btn'); | |
| const redoButton = document.getElementById('redo-btn'); | |
| const statsElements = { | |
| gamesPlayed: document.getElementById('games-played'), | |
| winRate: document.getElementById('win-rate'), | |
| averageTime: document.getElementById('average-time'), | |
| bestTime: document.getElementById('best-time') | |
| }; | |
| const statBars = { | |
| gamesPlayed: document.getElementById('games-played-bar'), | |
| winRate: document.getElementById('win-rate-bar'), | |
| averageTime: document.getElementById('average-time-bar'), | |
| bestTime: document.getElementById('best-time-bar'), | |
| mistakes: document.getElementById('mistakes-bar'), | |
| hints: document.getElementById('hints-bar') | |
| }; | |
| const helpButton = document.getElementById('help-btn'); | |
| const helpModal = document.getElementById('help-modal'); | |
| const closeHelpButton = document.getElementById('close-help'); | |
| const okHelpButton = document.getElementById('ok-help'); | |
| const themeButtons = { | |
| light: document.getElementById('theme-light'), | |
| dark: document.getElementById('theme-dark'), | |
| blue: document.getElementById('theme-blue') | |
| }; | |
| const fullscreenButton = document.getElementById('fullscreen-btn'); | |
| // Initialize the game | |
| initGame(); | |
| // Load statistics from localStorage | |
| loadStatistics(); | |
| // Update statistics display | |
| updateStatisticsDisplay(); | |
| // Event listeners | |
| numberButtons.forEach(button => { | |
| button.addEventListener('click', () => { | |
| if (gameState.selectedCell && !gameState.gameOver) { | |
| const number = parseInt(button.dataset.number); | |
| if (gameState.notesMode) { | |
| toggleNote(gameState.selectedCell.row, gameState.selectedCell.col, number); | |
| } else { | |
| fillCell(gameState.selectedCell.row, gameState.selectedCell.col, number); | |
| } | |
| } | |
| }); | |
| }); | |
| eraseButton.addEventListener('click', () => { | |
| if (gameState.selectedCell && !gameState.givenCells[gameState.selectedCell.row][gameState.selectedCell.col] && !gameState.gameOver) { | |
| eraseCell(gameState.selectedCell.row, gameState.selectedCell.col); | |
| } | |
| }); | |
| notesButton.addEventListener('click', () => { | |
| if (!gameState.gameOver) { | |
| gameState.notesMode = !gameState.notesMode; | |
| notesButton.classList.toggle('bg-yellow-300', gameState.notesMode); | |
| updateSelectedCell(); | |
| } | |
| }); | |
| hintButton.addEventListener('click', () => { | |
| if (gameState.selectedCell && gameState.hints > 0 && !gameState.gameOver) { | |
| giveHint(gameState.selectedCell.row, gameState.selectedCell.col); | |
| } | |
| }); | |
| newGameButton.addEventListener('click', initGame); | |
| noteStyleButtons.forEach(button => { | |
| button.addEventListener('click', () => { | |
| gameState.noteStyle = button.dataset.noteStyle; | |
| noteStyleButtons.forEach(btn => | |
| btn.classList.toggle('bg-indigo-100', btn.dataset.noteStyle === gameState.noteStyle) | |
| ); | |
| renderBoard(); | |
| if (gameState.selectedCell) { | |
| selectCell(gameState.selectedCell.row, gameState.selectedCell.col); | |
| } | |
| }); | |
| }); | |
| highlightButtons.forEach(button => { | |
| button.addEventListener('click', () => { | |
| gameState.highlightMode = button.dataset.highlight; | |
| highlightButtons.forEach(btn => | |
| btn.classList.toggle('bg-indigo-100', btn.dataset.highlight === gameState.highlightMode) | |
| ); | |
| if (gameState.selectedCell && gameState.board[gameState.selectedCell.row][gameState.selectedCell.col] !== 0) { | |
| highlightNumbers(gameState.board[gameState.selectedCell.row][gameState.selectedCell.col]); | |
| } | |
| }); | |
| }); | |
| undoButton.addEventListener('click', undoMove); | |
| redoButton.addEventListener('click', redoMove); | |
| helpButton.addEventListener('click', () => { | |
| helpModal.classList.remove('hidden'); | |
| }); | |
| closeHelpButton.addEventListener('click', () => { | |
| helpModal.classList.add('hidden'); | |
| }); | |
| okHelpButton.addEventListener('click', () => { | |
| helpModal.classList.add('hidden'); | |
| }); | |
| themeButtons.light.addEventListener('click', () => setTheme('light')); | |
| themeButtons.dark.addEventListener('click', () => setTheme('dark')); | |
| themeButtons.blue.addEventListener('click', () => setTheme('blue')); | |
| fullscreenButton.addEventListener('click', toggleFullscreen); | |
| // Keyboard support | |
| document.addEventListener('keydown', (e) => { | |
| if (gameState.gameOver) return; | |
| if (gameState.selectedCell) { | |
| const { row, col } = gameState.selectedCell; | |
| // Number keys 1-9 | |
| if (e.key >= '1' && e.key <= '9') { | |
| const number = parseInt(e.key); | |
| if (gameState.notesMode) { | |
| toggleNote(row, col, number); | |
| } else { | |
| fillCell(row, col, number); | |
| } | |
| } | |
| // Delete or Backspace | |
| else if ((e.key === 'Delete' || e.key === 'Backspace') && !gameState.givenCells[row][col]) { | |
| eraseCell(row, col); | |
| } | |
| // Arrow keys for navigation | |
| else if (e.key === 'ArrowUp' && row > 0) { | |
| selectCell(row - 1, col); | |
| } else if (e.key === 'ArrowDown' && row < 8) { | |
| selectCell(row + 1, col); | |
| } else if (e.key === 'ArrowLeft' && col > 0) { | |
| selectCell(row, col - 1); | |
| } else if (e.key === 'ArrowRight' && col < 8) { | |
| selectCell(row, col + 1); | |
| } | |
| // Space for notes mode | |
| else if (e.key === ' ') { | |
| gameState.notesMode = !gameState.notesMode; | |
| notesButton.classList.toggle('bg-yellow-300', gameState.notesMode); | |
| updateSelectedCell(); | |
| } | |
| // Z for undo (with Ctrl for Windows or Cmd for Mac) | |
| else if ((e.ctrlKey || e.metaKey) && e.key === 'z') { | |
| undoMove(); | |
| } | |
| // Y for redo (with Ctrl for Windows or Cmd for Mac) | |
| else if ((e.ctrlKey || e.metaKey) && e.key === 'y') { | |
| redoMove(); | |
| } | |
| } | |
| }); | |
| // Initialize the game | |
| function initGame() { | |
| // Reset game state | |
| gameState.board = Array(9).fill().map(() => Array(9).fill(0)); | |
| gameState.solution = Array(9).fill().map(() => Array(9).fill(0)); | |
| gameState.givenCells = Array(9).fill().map(() => Array(9).fill(false)); | |
| gameState.notes = Array(9).fill().map(() => Array(9).fill().map(() => new Set())); | |
| gameState.selectedCell = null; | |
| gameState.notesMode = false; | |
| gameState.mistakes = 0; | |
| gameState.hints = 3; | |
| gameState.timer = 0; | |
| gameState.gameOver = false; | |
| gameState.movesHistory = []; | |
| gameState.historyPointer = -1; | |
| // Reset UI | |
| notesButton.classList.remove('bg-yellow-300'); | |
| messageElement.classList.add('hidden'); | |
| updateTimer(); | |
| mistakesElement.textContent = '0/3'; | |
| hintsElement.textContent = '3'; | |
| statBars.mistakes.style.width = '0%'; | |
| statBars.hints.style.width = '100%'; | |
| // Stop any existing timer | |
| if (gameState.timerInterval) { | |
| clearInterval(gameState.timerInterval); | |
| } | |
| // Generate a new puzzle | |
| generatePuzzle(difficultySelect.value); | |
| // Render the board | |
| renderBoard(); | |
| // Start the timer | |
| gameState.timerInterval = setInterval(() => { | |
| gameState.timer++; | |
| updateTimer(); | |
| }, 1000); | |
| } | |
| // Generate a Sudoku puzzle | |
| function generatePuzzle(difficulty) { | |
| // First generate a complete solution | |
| generateSolution(0, 0); | |
| // Then remove numbers based on difficulty | |
| let cellsToRemove; | |
| switch (difficulty) { | |
| case 'easy': | |
| cellsToRemove = 40 + Math.floor(Math.random() * 5); // 40-44 | |
| break; | |
| case 'medium': | |
| cellsToRemove = 45 + Math.floor(Math.random() * 5); // 45-49 | |
| break; | |
| case 'hard': | |
| cellsToRemove = 50 + Math.floor(Math.random() * 5); // 50-54 | |
| break; | |
| case 'expert': | |
| cellsToRemove = 55 + Math.floor(Math.random() * 5); // 55-59 | |
| break; | |
| case 'insane': | |
| cellsToRemove = 60 + Math.floor(Math.random() * 4); // 60-63 | |
| break; | |
| default: | |
| cellsToRemove = 45; | |
| } | |
| // Copy solution to board | |
| for (let i = 0; i < 9; i++) { | |
| for (let j = 0; j < 9; j++) { | |
| gameState.board[i][j] = gameState.solution[i][j]; | |
| } | |
| } | |
| // Remove cells in a way that maintains puzzle uniqueness | |
| let removed = 0; | |
| const positions = []; | |
| for (let i = 0; i < 9; i++) { | |
| for (let j = 0; j < 9; j++) { | |
| positions.push({row: i, col: j}); | |
| } | |
| } | |
| shuffleArray(positions); | |
| // Use a more sophisticated removal algorithm that checks for uniqueness | |
| for (const pos of positions) { | |
| if (removed >= cellsToRemove) break; | |
| const {row, col} = pos; | |
| if (gameState.board[row][col] !== 0) { | |
| const temp = gameState.board[row][col]; | |
| gameState.board[row][col] = 0; | |
| // Check if the puzzle still has a unique solution | |
| if (!hasUniqueSolution()) { | |
| gameState.board[row][col] = temp; | |
| } else { | |
| removed++; | |
| gameState.givenCells[row][col] = false; | |
| } | |
| } | |
| } | |
| // Mark the remaining cells as given | |
| for (let i = 0; i < 9; i++) { | |
| for (let j = 0; j < 9; j++) { | |
| if (gameState.board[i][j] !== 0) { | |
| gameState.givenCells[i][j] = true; | |
| } | |
| } | |
| } | |
| } | |
| // Check if the current puzzle has a unique solution | |
| function hasUniqueSolution() { | |
| const tempBoard = JSON.parse(JSON.stringify(gameState.board)); | |
| let solutions = 0; | |
| function countSolutions(r, c) { | |
| if (r === 9) { | |
| solutions++; | |
| return; | |
| } | |
| if (c === 9) { | |
| countSolutions(r + 1, 0); | |
| return; | |
| } | |
| if (tempBoard[r][c] !== 0) { | |
| countSolutions(r, c + 1); | |
| return; | |
| } | |
| for (let num = 1; num <= 9 && solutions < 2; num++) { | |
| if (isValid(tempBoard, r, c, num)) { | |
| tempBoard[r][c] = num; | |
| countSolutions(r, c + 1); | |
| if (solutions < 2) tempBoard[r][c] = 0; | |
| } | |
| } | |
| } | |
| countSolutions(0, 0); | |
| return solutions === 1; | |
| } | |
| // Generate a complete Sudoku solution | |
| function generateSolution(row, col) { | |
| if (row === 9) { | |
| return true; | |
| } | |
| if (col === 9) { | |
| return generateSolution(row + 1, 0); | |
| } | |
| if (gameState.solution[row][col] !== 0) { | |
| return generateSolution(row, col + 1); | |
| } | |
| // Try numbers 1-9 in random order | |
| const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]; | |
| shuffleArray(numbers); | |
| for (const num of numbers) { | |
| if (isValid(gameState.solution, row, col, num)) { | |
| gameState.solution[row][col] = num; | |
| if (generateSolution(row, col + 1)) { | |
| return true; | |
| } | |
| gameState.solution[row][col] = 0; | |
| } | |
| } | |
| return false; | |
| } | |
| // Check if a number can be placed in a cell | |
| function isValid(grid, row, col, num) { | |
| // Check row | |
| for (let i = 0; i < 9; i++) { | |
| if (grid[row][i] === num) return false; | |
| } | |
| // Check column | |
| for (let i = 0; i < 9; i++) { | |
| if (grid[i][col] === num) return false; | |
| } | |
| // Check 3x3 box | |
| const boxRow = Math.floor(row / 3) * 3; | |
| const boxCol = Math.floor(col / 3) * 3; | |
| for (let i = 0; i < 3; i++) { | |
| for (let j = 0; j < 3; j++) { | |
| if (grid[boxRow + i][boxCol + j] === num) return false; | |
| } | |
| } | |
| return true; | |
| } | |
| // Shuffle an array | |
| function shuffleArray(array) { | |
| for (let i = array.length - 1; i > 0; i--) { | |
| const j = Math.floor(Math.random() * (i + 1)); | |
| [array[i], array[j]] = [array[j], array[i]]; | |
| } | |
| } | |
| // Render the Sudoku board | |
| function renderBoard() { | |
| boardElement.innerHTML = ''; | |
| for (let i = 0; i < 9; i++) { | |
| const rowElement = document.createElement('div'); | |
| rowElement.className = 'row flex'; | |
| for (let j = 0; j < 9; j++) { | |
| const cell = document.createElement('input'); | |
| cell.className = 'cell'; | |
| cell.type = 'text'; | |
| cell.maxLength = 1; | |
| cell.dataset.row = i; | |
| cell.dataset.col = j; | |
| if (gameState.board[i][j] !== 0) { | |
| const cellValue = document.createElement('div'); | |
| cellValue.className = 'cell-value w-full h-full flex items-center justify-center'; | |
| cellValue.textContent = gameState.board[i][j]; | |
| cell.appendChild(cellValue); | |
| if (gameState.givenCells[i][j]) { | |
| cell.classList.add('given'); | |
| cell.readOnly = true; | |
| } | |
| } else if (gameState.notes[i][j].size > 0 && gameState.noteStyle !== 'hidden') { | |
| if (gameState.noteStyle === 'grid') { | |
| const notesContainer = document.createElement('div'); | |
| notesContainer.className = 'notes-grid'; | |
| for (let n = 1; n <= 9; n++) { | |
| const note = document.createElement('div'); | |
| note.className = 'flex items-center justify-center'; | |
| note.textContent = gameState.notes[i][j].has(n) ? n : ''; | |
| notesContainer.appendChild(note); | |
| } | |
| cell.appendChild(notesContainer); | |
| } else if (gameState.noteStyle === 'corner') { | |
| const notesContainer = document.createElement('div'); | |
| notesContainer.className = 'notes-corner w-full h-full'; | |
| const positions = { | |
| 1: { top: '2px', left: '2px' }, | |
| 2: { top: '2px', left: '50%', transform: 'translateX(-50%)' }, | |
| 3: { top: '2px', right: '2px' }, | |
| 4: { top: '50%', left: '2px', transform: 'translateY(-50%)' }, | |
| 5: { top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }, | |
| 6: { top: '50%', right: '2px', transform: 'translateY(-50%)' }, | |
| 7: { bottom: '2px', left: '2px' }, | |
| 8: { bottom: '2px', left: '50%', transform: 'translateX(-50%)' }, | |
| 9: { bottom: '2px', right: '2px' } | |
| }; | |
| for (let n = 1; n <= 9; n++) { | |
| if (gameState.notes[i][j].has(n)) { | |
| const note = document.createElement('div'); | |
| note.className = 'notes-corner absolute text-xs'; | |
| note.textContent = n; | |
| Object.assign(note.style, positions[n]); | |
| notesContainer.appendChild(note); | |
| } | |
| } | |
| cell.appendChild(notesContainer); | |
| } | |
| } | |
| cell.addEventListener('click', () => selectCell(i, j)); | |
| cell.addEventListener('focus', () => selectCell(i, j)); | |
| rowElement.appendChild(cell); | |
| } | |
| boardElement.appendChild(rowElement); | |
| } | |
| } | |
| // Select a cell | |
| function selectCell(row, col) { | |
| // Deselect previous cell | |
| if (gameState.selectedCell) { | |
| const prevCell = document.querySelector(`.cell[data-row="${gameState.selectedCell.row}"][data-col="${gameState.selectedCell.col}"]`); | |
| if (prevCell) { | |
| prevCell.classList.remove('selected', 'highlight-mode'); | |
| // Remove highlights | |
| removeHighlights(); | |
| } | |
| } | |
| // Select new cell | |
| gameState.selectedCell = { row, col }; | |
| const cell = document.querySelector(`.cell[data-row="${row}"][data-col="${col}"]`); | |
| if (cell) { | |
| cell.classList.add('selected', 'highlight-mode'); | |
| cell.focus(); | |
| // Highlight based on current mode | |
| if (gameState.board[row][col] !== 0) { | |
| highlightNumbers(gameState.board[row][col]); | |
| } else if (gameState.highlightMode === 'peers') { | |
| highlightPeers(row, col); | |
| } | |
| } | |
| } | |
| // Update the selected cell appearance | |
| function updateSelectedCell() { | |
| if (gameState.selectedCell) { | |
| const cell = document.querySelector(`.cell[data-row="${gameState.selectedCell.row}"][data-col="${gameState.selectedCell.col}"]`); | |
| if (cell) { | |
| if (gameState.notesMode) { | |
| cell.classList.add('border-yellow-400', 'border-2'); | |
| } else { | |
| cell.classList.remove('border-yellow-400', 'border-2'); | |
| } | |
| } | |
| } | |
| } | |
| // Fill a cell with a number | |
| function fillCell(row, col, number) { | |
| if (gameState.givenCells[row][col] || gameState.gameOver) return; | |
| // Save current state for undo/redo | |
| saveMove(); | |
| // Check if the number is valid | |
| const isValidMove = number === gameState.solution[row][col]; | |
| // Check for conflicts with current board (not just solution) | |
| const isConflict = hasConflict(row, col, number); | |
| // Update the board | |
| gameState.board[row][col] = isConflict ? 0 : number; | |
| // Clear any notes for this cell | |
| gameState.notes[row][col].clear(); | |
| // Render the board | |
| renderBoard(); | |
| selectCell(row, col); | |
| // Highlight the number if not in conflicts mode | |
| if (gameState.highlightMode !== 'conflicts') { | |
| highlightNumbers(number); | |
| } | |
| // Check for mistakes | |
| if (!isValidMove) { | |
| gameState.mistakes++; | |
| mistakesElement.textContent = `${gameState.mistakes}/3`; | |
| statBars.mistakes.style.width = `${(gameState.mistakes / 3) * 100}%`; | |
| // Show error and highlight conflicts | |
| const cell = document.querySelector(`.cell[data-row="${row}"][data-col="${col}"]`); | |
| if (cell) { | |
| cell.classList.add('error'); | |
| setTimeout(() => { | |
| cell.classList.remove('error'); | |
| }, 1000); | |
| } | |
| // Check if game over | |
| if (gameState.mistakes >= 3) { | |
| gameState.gameOver = true; | |
| showMessage('Game Over! Too many mistakes.', 'bg-red-500'); | |
| clearInterval(gameState.timerInterval); | |
| // Update statistics | |
| gameState.stats.gamesPlayed++; | |
| gameState.stats.totalTime += gameState.timer; | |
| updateDifficultyStats(false); | |
| saveStatistics(); | |
| updateStatisticsDisplay(); | |
| } | |
| } else { | |
| // Check if puzzle is complete | |
| if (isPuzzleComplete()) { | |
| gameState.gameOver = true; | |
| showMessage('Congratulations! You solved the puzzle!', 'bg-green-500'); | |
| clearInterval(gameState.timerInterval); | |
| // Update statistics | |
| gameState.stats.gamesPlayed++; | |
| gameState.stats.gamesWon++; | |
| gameState.stats.totalTime += gameState.timer; | |
| // Update best time if applicable | |
| const currentDifficulty = difficultySelect.value; | |
| if (!gameState.stats.bestTime || gameState.timer < gameState.stats.bestTime) { | |
| gameState.stats.bestTime = gameState.timer; | |
| } | |
| if (!gameState.stats.difficultyStats[currentDifficulty].bestTime || | |
| gameState.timer < gameState.stats.difficultyStats[currentDifficulty].bestTime) { | |
| gameState.stats.difficultyStats[currentDifficulty].bestTime = gameState.timer; | |
| } | |
| updateDifficultyStats(true); | |
| saveStatistics(); | |
| updateStatisticsDisplay(); | |
| } | |
| } | |
| } | |
| // Check if a number would conflict with current board | |
| function hasConflict(row, col, number) { | |
| if (number === 0) return false; | |
| // Check row | |
| for (let i = 0; i < 9; i++) { | |
| if (i !== col && gameState.board[row][i] === number) return true; | |
| } | |
| // Check column | |
| for (let i = 0; i < 9; i++) { | |
| if (i !== row && gameState.board[i][col] === number) return true; | |
| } | |
| // Check 3x3 box | |
| const boxRow = Math.floor(row / 3) * 3; | |
| const boxCol = Math.floor(col / 3) * 3; | |
| for (let i = 0; i < 3; i++) { | |
| for (let j = 0; j < 3; j++) { | |
| const r = boxRow + i; | |
| const c = boxCol + j; | |
| if (r !== row && c !== col && gameState.board[r][c] === number) return true; | |
| } | |
| } | |
| return false; | |
| } | |
| // Save current move to history | |
| function saveMove() { | |
| // If we're not at the end of history, discard future moves | |
| if (gameState.historyPointer < gameState.movesHistory.length - 1) { | |
| gameState.movesHistory = gameState.movesHistory.slice(0, gameState.historyPointer + 1); | |
| } | |
| // Save current state | |
| gameState.movesHistory.push({ | |
| board: JSON.parse(JSON.stringify(gameState.board)), | |
| notes: JSON.parse(JSON.stringify(gameState.notes.map(row => row.map(col => [...col])))), | |
| selectedCell: gameState.selectedCell ? {...gameState.selectedCell} : null | |
| }); | |
| gameState.historyPointer = gameState.movesHistory.length - 1; | |
| // Update undo/redo button states | |
| updateUndoRedoButtons(); | |
| } | |
| // Undo the last move | |
| function undoMove() { | |
| if (gameState.historyPointer < 0) return; | |
| gameState.historyPointer--; | |
| if (gameState.historyPointer >= 0) { | |
| const state = gameState.movesHistory[gameState.historyPointer]; | |
| gameState.board = JSON.parse(JSON.stringify(state.board)); | |
| gameState.notes = state.notes.map(row => row.map(col => new Set(col))); | |
| if (state.selectedCell) { | |
| gameState.selectedCell = {...state.selectedCell}; | |
| } | |
| } else { | |
| // If we're undoing the very first move, reset the board | |
| gameState.board = Array(9).fill().map(() => Array(9).fill(0)); | |
| gameState.notes = Array(9).fill().map(() => Array(9).fill().map(() => new Set())); | |
| gameState.selectedCell = null; | |
| } | |
| renderBoard(); | |
| if (gameState.selectedCell) { | |
| selectCell(gameState.selectedCell.row, gameState.selectedCell.col); | |
| } | |
| updateUndoRedoButtons(); | |
| } | |
| // Redo the last undone move | |
| function redoMove() { | |
| if (gameState.historyPointer >= gameState.movesHistory.length - 1) return; | |
| gameState.historyPointer++; | |
| const state = gameState.movesHistory[gameState.historyPointer]; | |
| gameState.board = JSON.parse(JSON.stringify(state.board)); | |
| gameState.notes = state.notes.map(row => row.map(col => new Set(col))); | |
| if (state.selectedCell) { | |
| gameState.selectedCell = {...state.selectedCell}; | |
| } | |
| renderBoard(); | |
| if (gameState.selectedCell) { | |
| selectCell(gameState.selectedCell.row, gameState.selectedCell.col); | |
| } | |
| updateUndoRedoButtons(); | |
| } | |
| // Update undo/redo button states | |
| function updateUndoRedoButtons() { | |
| undoButton.disabled = gameState.historyPointer < 0; | |
| undoButton.classList.toggle('text-gray-400', gameState.historyPointer < 0); | |
| undoButton.classList.toggle('text-gray-700', gameState.historyPointer >= 0); | |
| redoButton.disabled = gameState.historyPointer >= gameState.movesHistory.length - 1; | |
| redoButton.classList.toggle('text-gray-400', gameState.historyPointer >= gameState.movesHistory.length - 1); | |
| redoButton.classList.toggle('text-gray-700', gameState.historyPointer < gameState.movesHistory.length - 1); | |
| } | |
| // Erase a cell | |
| function eraseCell(row, col) { | |
| if (gameState.givenCells[row][col] || gameState.gameOver) return; | |
| // Save current state for undo/redo | |
| saveMove(); | |
| gameState.board[row][col] = 0; | |
| renderBoard(); | |
| selectCell(row, col); | |
| } | |
| // Toggle a note for a cell | |
| function toggleNote(row, col, number) { | |
| if (gameState.givenCells[row][col] || gameState.gameOver) return; | |
| // Save current state for undo/redo | |
| saveMove(); | |
| if (gameState.notes[row][col].has(number)) { | |
| gameState.notes[row][col].delete(number); | |
| } else { | |
| gameState.notes[row][col].add(number); | |
| } | |
| renderBoard(); | |
| selectCell(row, col); | |
| } | |
| // Give a hint | |
| function giveHint(row, col) { | |
| if (gameState.givenCells[row][col] || gameState.board[row][col] !== 0 || gameState.hints <= 0 || gameState.gameOver) return; | |
| gameState.hints--; | |
| hintsElement.textContent = gameState.hints; | |
| // Calculate width percentage for hints bar | |
| const percentage = (gameState.hints / 3) * 100; | |
| statBars.hints.style.width = `${percentage}%`; | |
| // Flash the cell | |
| const cell = document.querySelector(`.cell[data-row="${row}"][data-col="${col}"]`); | |
| if (cell) { | |
| cell.classList.add('highlight'); | |
| setTimeout(() => { | |
| cell.classList.remove('highlight'); | |
| }, 1000); | |
| } | |
| // Fill the cell after a delay to show the highlight | |
| setTimeout(() => { | |
| fillCell(row, col, gameState.solution[row][col]); | |
| }, 300); | |
| } | |
| // Highlight all instances of a number and its peers | |
| function highlightNumbers(number) { | |
| removeHighlights(); | |
| if (number === 0) return; | |
| const cells = document.querySelectorAll('.cell'); | |
| cells.forEach(cell => { | |
| const row = parseInt(cell.dataset.row); | |
| const col = parseInt(cell.dataset.col); | |
| if (gameState.board[row][col] === number) { | |
| cell.classList.add('same-number'); | |
| } else if (gameState.highlightMode === 'peers' && isPeer(row, col, gameState.selectedCell.row, gameState.selectedCell.col)) { | |
| cell.classList.add('peers'); | |
| } else if (gameState.highlightMode === 'conflicts' && gameState.board[row][col] !== 0 && hasConflict(row, col, gameState.board[row][col])) { | |
| cell.classList.add('conflict'); | |
| } | |
| }); | |
| } | |
| // Highlight peers of a cell | |
| function highlightPeers(row, col) { | |
| removeHighlights(); | |
| const cells = document.querySelectorAll('.cell'); | |
| cells.forEach(cell => { | |
| const cellRow = parseInt(cell.dataset.row); | |
| const cellCol = parseInt(cell.dataset.col); | |
| if (isPeer(cellRow, cellCol, row, col)) { | |
| cell.classList.add('peers'); | |
| } | |
| }); | |
| } | |
| // Check if two cells are peers (same row, column, or box) | |
| function isPeer(row1, col1, row2, col2) { | |
| // Same row or column | |
| if (row1 === row2 || col1 === col2) return true; | |
| // Same 3x3 box | |
| const boxRow1 = Math.floor(row1 / 3); | |
| const boxCol1 = Math.floor(col1 / 3); | |
| const boxRow2 = Math.floor(row2 / 3); | |
| const boxCol2 = Math.floor(col2 / 3); | |
| return boxRow1 === boxRow2 && boxCol1 === boxCol2; | |
| } | |
| // Remove all number highlights | |
| function removeHighlights() { | |
| const cells = document.querySelectorAll('.cell'); | |
| cells.forEach(cell => { | |
| cell.classList.remove('same-number', 'peers', 'conflict'); | |
| }); | |
| } | |
| // Check if the puzzle is complete | |
| function isPuzzleComplete() { | |
| for (let i = 0; i < 9; i++) { | |
| for (let j = 0; j < 9; j++) { | |
| if (gameState.board[i][j] !== gameState.solution[i][j]) { | |
| return false; | |
| } | |
| } | |
| } | |
| return true; | |
| } | |
| // Update the timer display | |
| function updateTimer() { | |
| const minutes = Math.floor(gameState.timer / 60).toString().padStart(2, '0'); | |
| const seconds = (gameState.timer % 60).toString().padStart(2, '0'); | |
| timerElement.textContent = `${minutes}:${seconds}`; | |
| } | |
| // Show a message | |
| function showMessage(text, className) { | |
| messageContent.textContent = text; | |
| messageContent.className = `inline-block px-8 py-4 rounded-xl font-bold text-white shadow-lg ${className}`; | |
| messageElement.classList.remove('hidden'); | |
| } | |
| // Update difficulty-specific statistics | |
| function updateDifficultyStats(isWin) { | |
| const difficulty = difficultySelect.value; | |
| gameState.stats.difficultyStats[difficulty].played++; | |
| if (isWin) { | |
| gameState.stats.difficultyStats[difficulty].won++; | |
| } | |
| } | |
| // Save statistics to localStorage | |
| function saveStatistics() { | |
| localStorage.setItem('sudokuStats', JSON.stringify(gameState.stats)); | |
| } | |
| // Load statistics from localStorage | |
| function loadStatistics() { | |
| const savedStats = localStorage.getItem('sudokuStats'); | |
| if (savedStats) { | |
| gameState.stats = JSON.parse(savedStats); | |
| } | |
| } | |
| // Update statistics display | |
| function updateStatisticsDisplay() { | |
| statsElements.gamesPlayed.textContent = gameState.stats.gamesPlayed; | |
| statBars.gamesPlayed.style.width = `${Math.min(gameState.stats.gamesPlayed * 10, 100)}%`; | |
| const winRate = gameState.stats.gamesPlayed > 0 ? | |
| Math.round((gameState.stats.gamesWon / gameState.stats.gamesPlayed) * 100) : 0; | |
| statsElements.winRate.textContent = `${winRate}%`; | |
| statBars.winRate.style.width = `${winRate}%`; | |
| const avgTime = gameState.stats.gamesPlayed > 0 ? | |
| Math.floor(gameState.stats.totalTime / gameState.stats.gamesPlayed) : 0; | |
| const avgMinutes = Math.floor(avgTime / 60).toString().padStart(2, '0'); | |
| const avgSeconds = (avgTime % 60).toString().padStart(2, '0'); | |
| statsElements.averageTime.textContent = `${avgMinutes}:${avgSeconds}`; | |
| statBars.averageTime.style.width = `${Math.min(avgTime * 100 / 1800, 100)}%`; // Cap at 30 minutes | |
| if (gameState.stats.bestTime) { | |
| const bestMinutes = Math.floor(gameState.stats.bestTime / 60).toString().padStart(2, '0'); | |
| const bestSeconds = (gameState.stats.bestTime % 60).toString().padStart(2, '0'); | |
| statsElements.bestTime.textContent = `${bestMinutes}:${bestSeconds}`; | |
| statBars.bestTime.style.width = `${Math.max(100 - (gameState.stats.bestTime * 100 / 900), 5)}%`; // Invert scale (5% min) | |
| } else { | |
| statsElements.bestTime.textContent = "--:--"; | |
| statBars.bestTime.style.width = "0%"; | |
| } | |
| } | |
| // Set theme for the game | |
| function setTheme(theme) { | |
| gameState.currentTheme = theme; | |
| document.body.classList.remove('theme-light', 'theme-dark', 'theme-blue'); | |
| document.body.classList.add(`theme-${theme}`); | |
| // Update theme button highlights | |
| Object.keys(themeButtons).forEach(key => { | |
| themeButtons[key].classList.toggle('ring-2', key === theme); | |
| themeButtons[key].classList.toggle('ring-indigo-500', key === theme); | |
| }); | |
| // Save theme preference | |
| localStorage.setItem('sudokuTheme', theme); | |
| } | |
| // Toggle fullscreen mode | |
| function toggleFullscreen() { | |
| if (!document.fullscreenElement) { | |
| document.documentElement.requestFullscreen().catch(err => { | |
| console.error(`Error attempting to enable fullscreen: ${err.message}`); | |
| }); | |
| } else { | |
| if (document.exitFullscreen) { | |
| document.exitFullscreen(); | |
| } | |
| } | |
| } | |
| // Load saved theme | |
| const savedTheme = localStorage.getItem('sudokuTheme') || 'light'; | |
| setTheme(savedTheme); | |
| // Set initial note style and highlight mode buttons | |
| noteStyleButtons.forEach(btn => | |
| btn.classList.toggle('bg-indigo-100', btn.dataset.noteStyle === gameState.noteStyle) | |
| ); | |
| highlightButtons.forEach(btn => | |
| btn.classList.toggle('bg-indigo-100', btn.dataset.highlight === gameState.highlightMode) | |
| ); | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=sekopst/serkanspace" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> | |
| </html> |