Sudoku / script.js
pitangent's picture
Update numpad counter badges to display remaining cell count
29c627e verified
Raw
History Blame Contribute Delete
13.4 kB
const BOARD_SIZE = 9;
const BOX_SIZE = 3;
// True RNG Utilities
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]];
}
}
// Game State
let solutionBoard = [];
let initialBoard = [];
let currentBoard = [];
let notesBoard = Array.from({ length: BOARD_SIZE }, () => Array.from({ length: BOARD_SIZE }, () => []));
let history = []; // For undo
let selectedCell = null;
let notesMode = false;
let difficulty = 'easy'; // easy, medium, hard
let timerInterval = null;
let secondsElapsed = 0;
// Sudoku Core Logic
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; // Fast exit if more than 1 solution
}
}
function generatePuzzle(difficulty) {
let board = getEmptyBoard();
fillBoard(board);
solutionBoard = board.map(row => [...row]);
let cellsToRemove = 40; // Default easy
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; // Put it back
} else {
removedCount++;
}
}
return puzzle;
}
// Timer Logic
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);
}
// UI Initialization
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];
// Check if it's an error relative to solution board
if (currentBoard[r][c] !== solutionBoard[r][c]) {
cell.classList.add('error');
}
} else {
// Render Notes
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();
}
// Interaction
function handleInput(num) {
if (!selectedCell) return;
const { r, c } = selectedCell;
if (initialBoard[r][c] !== 0) return; // Cannot edit fixed cells
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] = []; // Clear notes when filled
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);
}
// History & Undo
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;
}
// Win Check
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);
}
}
// Event Listeners
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);
});
});
// Difficulty Dropdown Logic
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');
}
});
// Theme Switcher Logic
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);
}
});
// Start Game
initGame();