| const BOARD_SIZE = 9; |
| const BOX_SIZE = 3; |
|
|
| |
| function getTrueRandomInt(max) { |
| const randomBuffer = new Uint32Array(1); |
| window.crypto.getRandomValues(randomBuffer); |
| return randomBuffer[0] % max; |
| } |
|
|
| function shuffleArray(array) { |
| for (let i = array.length - 1; i > 0; i--) { |
| const j = getTrueRandomInt(i + 1); |
| [array[i], array[j]] = [array[j], array[i]]; |
| } |
| } |
|
|
| |
| let solutionBoard = []; |
| let initialBoard = []; |
| let currentBoard = []; |
| let notesBoard = Array.from({ length: BOARD_SIZE }, () => Array.from({ length: BOARD_SIZE }, () => [])); |
| let history = []; |
| let selectedCell = null; |
| let notesMode = false; |
| let difficulty = 'easy'; |
| let timerInterval = null; |
| let secondsElapsed = 0; |
|
|
| |
| function getEmptyBoard() { |
| return Array.from({ length: BOARD_SIZE }, () => Array(BOARD_SIZE).fill(0)); |
| } |
|
|
| function isValid(board, row, col, num) { |
| for (let x = 0; x < BOARD_SIZE; x++) { |
| if (board[row][x] === num) return false; |
| } |
| for (let x = 0; x < BOARD_SIZE; x++) { |
| if (board[x][col] === num) return false; |
| } |
| let startRow = row - row % BOX_SIZE, startCol = col - col % BOX_SIZE; |
| for (let i = 0; i < BOX_SIZE; i++) { |
| for (let j = 0; j < BOX_SIZE; j++) { |
| if (board[i + startRow][j + startCol] === num) return false; |
| } |
| } |
| return true; |
| } |
|
|
| function fillBoard(board) { |
| for (let row = 0; row < BOARD_SIZE; row++) { |
| for (let col = 0; col < BOARD_SIZE; col++) { |
| if (board[row][col] === 0) { |
| let nums = [1, 2, 3, 4, 5, 6, 7, 8, 9]; |
| shuffleArray(nums); |
| for (let num of nums) { |
| if (isValid(board, row, col, num)) { |
| board[row][col] = num; |
| if (fillBoard(board)) { |
| return true; |
| } |
| board[row][col] = 0; |
| } |
| } |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
|
|
| function solveBoardCount(board, count = { value: 0 }) { |
| let row = -1; |
| let col = -1; |
| let isEmpty = false; |
| for (let i = 0; i < BOARD_SIZE; i++) { |
| for (let j = 0; j < BOARD_SIZE; j++) { |
| if (board[i][j] === 0) { |
| row = i; |
| col = j; |
| isEmpty = true; |
| break; |
| } |
| } |
| if (isEmpty) break; |
| } |
|
|
| if (!isEmpty) { |
| count.value++; |
| return; |
| } |
|
|
| for (let num = 1; num <= 9; num++) { |
| if (isValid(board, row, col, num)) { |
| board[row][col] = num; |
| solveBoardCount(board, count); |
| board[row][col] = 0; |
| } |
| if (count.value > 1) return; |
| } |
| } |
|
|
| function generatePuzzle(difficulty) { |
| let board = getEmptyBoard(); |
| fillBoard(board); |
| solutionBoard = board.map(row => [...row]); |
|
|
| let cellsToRemove = 40; |
| if (difficulty === 'medium') cellsToRemove = 50; |
| if (difficulty === 'hard') cellsToRemove = 60; |
|
|
| let cells = []; |
| for (let r = 0; r < BOARD_SIZE; r++) { |
| for (let c = 0; c < BOARD_SIZE; c++) { |
| cells.push({ r, c }); |
| } |
| } |
| shuffleArray(cells); |
|
|
| let puzzle = solutionBoard.map(row => [...row]); |
| let removedCount = 0; |
|
|
| for (let cell of cells) { |
| if (removedCount >= cellsToRemove) break; |
| let temp = puzzle[cell.r][cell.c]; |
| puzzle[cell.r][cell.c] = 0; |
|
|
| let count = { value: 0 }; |
| let copy = puzzle.map(row => [...row]); |
| solveBoardCount(copy, count); |
|
|
| if (count.value !== 1) { |
| puzzle[cell.r][cell.c] = temp; |
| } else { |
| removedCount++; |
| } |
| } |
|
|
| return puzzle; |
| } |
|
|
| |
| function startTimer() { |
| clearInterval(timerInterval); |
| secondsElapsed = 0; |
| updateTimerDisplay(); |
| timerInterval = setInterval(() => { |
| secondsElapsed++; |
| updateTimerDisplay(); |
| }, 1000); |
| } |
|
|
| function updateTimerDisplay() { |
| let m = Math.floor(secondsElapsed / 60).toString().padStart(2, '0'); |
| let s = (secondsElapsed % 60).toString().padStart(2, '0'); |
| document.getElementById('timerDisplay').innerText = `${m}:${s}`; |
| } |
|
|
| function stopTimer() { |
| clearInterval(timerInterval); |
| } |
|
|
| |
| function initGame() { |
| history = []; |
| selectedCell = null; |
| notesBoard = Array.from({ length: BOARD_SIZE }, () => Array.from({ length: BOARD_SIZE }, () => [])); |
| updateUndoBtn(); |
|
|
| currentBoard = generatePuzzle(difficulty); |
| initialBoard = currentBoard.map(row => [...row]); |
| |
| renderBoard(); |
| startTimer(); |
| } |
|
|
| function renderBoard() { |
| const boardEl = document.getElementById('board'); |
| boardEl.innerHTML = ''; |
| |
| for (let r = 0; r < BOARD_SIZE; r++) { |
| for (let c = 0; c < BOARD_SIZE; c++) { |
| const cell = document.createElement('div'); |
| cell.classList.add('cell'); |
| cell.dataset.r = r; |
| cell.dataset.c = c; |
| |
| if (initialBoard[r][c] !== 0) { |
| cell.classList.add('fixed'); |
| cell.innerText = initialBoard[r][c]; |
| } else if (currentBoard[r][c] !== 0) { |
| cell.classList.add('user-input'); |
| cell.innerText = currentBoard[r][c]; |
| |
| if (currentBoard[r][c] !== solutionBoard[r][c]) { |
| cell.classList.add('error'); |
| } |
| } else { |
| |
| if (notesBoard[r][c].length > 0) { |
| const notesGrid = document.createElement('div'); |
| notesGrid.classList.add('notes-grid'); |
| for (let i = 1; i <= 9; i++) { |
| const noteNum = document.createElement('div'); |
| noteNum.classList.add('note-num'); |
| if (notesBoard[r][c].includes(i)) { |
| noteNum.innerText = i; |
| } |
| notesGrid.appendChild(noteNum); |
| } |
| cell.appendChild(notesGrid); |
| } |
| } |
|
|
| if (selectedCell && selectedCell.r === r && selectedCell.c === c) { |
| cell.classList.add('selected'); |
| } else if (selectedCell && currentBoard[selectedCell.r][selectedCell.c] !== 0 && currentBoard[r][c] === currentBoard[selectedCell.r][selectedCell.c]) { |
| cell.classList.add('highlight'); |
| } else if (selectedCell && initialBoard[selectedCell.r][selectedCell.c] !== 0 && initialBoard[r][c] === initialBoard[selectedCell.r][selectedCell.c]) { |
| cell.classList.add('highlight'); |
| } |
|
|
| cell.addEventListener('click', () => selectCell(r, c)); |
| boardEl.appendChild(cell); |
| } |
| } |
| updateNumpadState(); |
| } |
|
|
| function updateNumpadState() { |
| const totalCounts = {}; |
| for (let i = 1; i <= 9; i++) { |
| totalCounts[i] = 0; |
| } |
| |
| for (let r = 0; r < BOARD_SIZE; r++) { |
| for (let c = 0; c < BOARD_SIZE; c++) { |
| const val = currentBoard[r][c]; |
| if (val >= 1 && val <= 9) { |
| totalCounts[val]++; |
| } |
| } |
| } |
|
|
| document.querySelectorAll('.num-btn').forEach(btn => { |
| const num = parseInt(btn.dataset.num); |
| if (num >= 1 && num <= 9) { |
| btn.disabled = totalCounts[num] >= 9; |
| const counterEl = btn.querySelector('.num-counter'); |
| if (counterEl) { |
| const remaining = 9 - totalCounts[num]; |
| counterEl.innerText = remaining; |
| if (remaining > 0) { |
| counterEl.classList.remove('hidden'); |
| } else { |
| counterEl.classList.add('hidden'); |
| } |
| } |
| } |
| }); |
| } |
|
|
| function selectCell(r, c) { |
| if (selectedCell && selectedCell.r === r && selectedCell.c === c) { |
| selectedCell = null; |
| } else { |
| selectedCell = { r, c }; |
| } |
| renderBoard(); |
| } |
|
|
| |
| function handleInput(num) { |
| if (!selectedCell) return; |
| const { r, c } = selectedCell; |
| |
| if (initialBoard[r][c] !== 0) return; |
|
|
| saveHistory(); |
|
|
| if (num === 0) { |
| currentBoard[r][c] = 0; |
| } else if (notesMode) { |
| if (currentBoard[r][c] === 0) { |
| const idx = notesBoard[r][c].indexOf(num); |
| if (idx > -1) { |
| notesBoard[r][c].splice(idx, 1); |
| } else { |
| notesBoard[r][c].push(num); |
| notesBoard[r][c].sort((a, b) => a - b); |
| } |
| } |
| } else { |
| currentBoard[r][c] = num; |
| notesBoard[r][c] = []; |
| clearRelatedNotes(r, c, num); |
| } |
|
|
| renderBoard(); |
| checkWin(); |
| } |
|
|
| function clearRelatedNotes(r, c, num) { |
| for (let i = 0; i < BOARD_SIZE; i++) { |
| removeNote(r, i, num); |
| removeNote(i, c, num); |
| } |
| let startR = r - r % BOX_SIZE; |
| let startC = c - c % BOX_SIZE; |
| for (let i = 0; i < BOX_SIZE; i++) { |
| for (let j = 0; j < BOX_SIZE; j++) { |
| removeNote(startR + i, startC + j, num); |
| } |
| } |
| } |
|
|
| function removeNote(r, c, num) { |
| const idx = notesBoard[r][c].indexOf(num); |
| if (idx > -1) notesBoard[r][c].splice(idx, 1); |
| } |
|
|
| |
| function saveHistory() { |
| history.push({ |
| board: currentBoard.map(row => [...row]), |
| notes: notesBoard.map(row => row.map(cellNotes => [...cellNotes])) |
| }); |
| updateUndoBtn(); |
| } |
|
|
| function undo() { |
| if (history.length === 0) return; |
| const lastState = history.pop(); |
| currentBoard = lastState.board; |
| notesBoard = lastState.notes; |
| updateUndoBtn(); |
| renderBoard(); |
| } |
|
|
| function updateUndoBtn() { |
| document.getElementById('btnUndo').disabled = history.length === 0; |
| } |
|
|
| |
| function checkWin() { |
| let complete = true; |
| for (let r = 0; r < BOARD_SIZE; r++) { |
| for (let c = 0; c < BOARD_SIZE; c++) { |
| if (currentBoard[r][c] === 0 || currentBoard[r][c] !== solutionBoard[r][c]) { |
| complete = false; |
| break; |
| } |
| } |
| } |
| if (complete) { |
| stopTimer(); |
| setTimeout(() => alert('Congratulations! You solved the Sudoku!'), 100); |
| } |
| } |
|
|
| |
| document.getElementById('btnNewGame').addEventListener('click', initGame); |
|
|
| document.getElementById('btnUndo').addEventListener('click', undo); |
|
|
| document.getElementById('btnNotes').addEventListener('click', () => { |
| notesMode = !notesMode; |
| document.getElementById('btnNotes').classList.toggle('active', notesMode); |
| }); |
|
|
| document.getElementById('btnClear').addEventListener('click', () => { |
| handleInput(0); |
| }); |
|
|
| document.querySelectorAll('.num-btn').forEach(btn => { |
| btn.addEventListener('click', (e) => { |
| const num = parseInt(e.currentTarget.dataset.num); |
| handleInput(num); |
| }); |
| }); |
|
|
| |
| const btnDifficulty = document.getElementById('btnDifficulty'); |
| const difficultyDropdown = document.getElementById('difficultyDropdown'); |
|
|
| btnDifficulty.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| difficultyDropdown.classList.toggle('show'); |
| }); |
|
|
| document.querySelectorAll('.dropdown-item').forEach(item => { |
| item.addEventListener('click', (e) => { |
| document.querySelectorAll('.dropdown-item').forEach(el => el.classList.remove('active')); |
| e.currentTarget.classList.add('active'); |
| difficulty = e.currentTarget.dataset.diff; |
| difficultyDropdown.classList.remove('show'); |
| initGame(); |
| }); |
| }); |
|
|
| document.addEventListener('click', (e) => { |
| if (!difficultyDropdown.contains(e.target) && e.target !== btnDifficulty) { |
| difficultyDropdown.classList.remove('show'); |
| } |
| }); |
|
|
| |
| const btnThemeToggle = document.getElementById('btnThemeToggle'); |
| btnThemeToggle.addEventListener('click', () => { |
| const isDarkMode = document.body.classList.toggle('dark-mode'); |
| const icon = btnThemeToggle.querySelector('i'); |
| if (isDarkMode) { |
| icon.className = 'fa-solid fa-sun'; |
| } else { |
| icon.className = 'fa-solid fa-moon'; |
| } |
| }); |
|
|
| document.addEventListener('keydown', (e) => { |
| if (!selectedCell) return; |
| if (e.key >= '1' && e.key <= '9') { |
| handleInput(parseInt(e.key)); |
| } else if (e.key === 'Backspace' || e.key === 'Delete') { |
| handleInput(0); |
| } else if (e.key === 'n' || e.key === 'N') { |
| document.getElementById('btnNotes').click(); |
| } else if (e.key === 'ArrowUp') { |
| if (selectedCell.r > 0) selectCell(selectedCell.r - 1, selectedCell.c); |
| } else if (e.key === 'ArrowDown') { |
| if (selectedCell.r < BOARD_SIZE - 1) selectCell(selectedCell.r + 1, selectedCell.c); |
| } else if (e.key === 'ArrowLeft') { |
| if (selectedCell.c > 0) selectCell(selectedCell.r, selectedCell.c - 1); |
| } else if (e.key === 'ArrowRight') { |
| if (selectedCell.c < BOARD_SIZE - 1) selectCell(selectedCell.r, selectedCell.c + 1); |
| } |
| }); |
|
|
| |
| initGame(); |
|
|