let currentGameId = null; let currentBoard = []; let originalBoard = []; let selectedCell = null; // Initialize the game when page loads document.addEventListener('DOMContentLoaded', () => { newGame(); }); async function newGame() { const difficulty = document.getElementById('difficulty').value; try { const response = await fetch(`/api/sudoku/new/${difficulty}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); currentGameId = data.gameId; // Ensure puzzle is properly formatted currentBoard = []; for (let i = 0; i < 9; i++) { const row = []; for (let j = 0; j < 9; j++) { row.push(data.puzzle[i][j] || '.'); } currentBoard.push(row); } originalBoard = JSON.parse(JSON.stringify(currentBoard)); renderBoard(); showMessage('New game started! Good luck!', 'info'); // Focus on first empty cell focusFirstEmptyCell(); } catch (error) { showMessage('Error starting new game: ' + error.message, 'error'); console.error('Error:', error); } } function renderBoard() { const boardElement = document.getElementById('sudoku-board'); boardElement.innerHTML = ''; for (let i = 0; i < 9; i++) { for (let j = 0; j < 9; j++) { const cell = document.createElement('div'); cell.className = 'cell'; cell.dataset.row = i; cell.dataset.col = j; // Create input element for each cell const input = document.createElement('input'); input.type = 'text'; input.className = 'cell-input'; input.maxLength = 1; input.dataset.row = i; input.dataset.col = j; const value = currentBoard[i][j]; const isFixed = originalBoard[i][j] !== '.'; if (value !== '.') { input.value = value; } if (isFixed) { cell.classList.add('fixed'); input.readOnly = true; input.tabIndex = -1; // Skip fixed cells when tabbing } else { // Add input event listeners for editable cells input.addEventListener('input', handleCellInput); input.addEventListener('focus', handleCellFocus); input.addEventListener('keydown', handleCellKeydown); } cell.appendChild(input); boardElement.appendChild(cell); } } } function handleCellInput(event) { const input = event.target; const row = parseInt(input.dataset.row); const col = parseInt(input.dataset.col); const value = input.value; // Only allow numbers 1-9 if (value && !/^[1-9]$/.test(value)) { input.value = ''; return; } // Update the board if (value === '') { currentBoard[row][col] = '.'; } else { currentBoard[row][col] = value; // Automatically move to next empty cell after entering a number setTimeout(() => moveToNextEmptyCell(row, col), 100); } // Remove error styling if present input.parentElement.classList.remove('error'); } function handleCellFocus(event) { const input = event.target; const row = parseInt(input.dataset.row); const col = parseInt(input.dataset.col); selectedCell = { row, col }; // Select all text when focusing input.select(); } function handleCellKeydown(event) { const input = event.target; const row = parseInt(input.dataset.row); const col = parseInt(input.dataset.col); const key = event.key; // Handle arrow key navigation if (key === 'ArrowUp' || key === 'ArrowDown' || key === 'ArrowLeft' || key === 'ArrowRight') { event.preventDefault(); navigateWithArrows(row, col, key); } // Handle Tab navigation (move to next empty cell) else if (key === 'Tab') { event.preventDefault(); if (event.shiftKey) { moveToPreviousEmptyCell(row, col); } else { moveToNextEmptyCell(row, col); } } // Handle Delete/Backspace else if (key === 'Delete' || key === 'Backspace') { event.preventDefault(); input.value = ''; currentBoard[row][col] = '.'; input.parentElement.classList.remove('error'); } // Handle Enter key - validate current board else if (key === 'Enter') { event.preventDefault(); validateBoard(); } // Handle number keys from numpad else if (key >= '1' && key <= '9') { event.preventDefault(); input.value = key; currentBoard[row][col] = key; setTimeout(() => moveToNextEmptyCell(row, col), 100); } } function navigateWithArrows(row, col, key) { let newRow = row; let newCol = col; switch(key) { case 'ArrowUp': newRow = Math.max(0, row - 1); break; case 'ArrowDown': newRow = Math.min(8, row + 1); break; case 'ArrowLeft': newCol = Math.max(0, col - 1); break; case 'ArrowRight': newCol = Math.min(8, col + 1); break; } const nextInput = document.querySelector(`input[data-row="${newRow}"][data-col="${newCol}"]`); if (nextInput && !nextInput.readOnly) { nextInput.focus(); } } function moveToNextEmptyCell(currentRow, currentCol) { // Start from the next cell let startIndex = currentRow * 9 + currentCol + 1; for (let i = 0; i < 81; i++) { let index = (startIndex + i) % 81; let row = Math.floor(index / 9); let col = index % 9; if (originalBoard[row][col] === '.' && currentBoard[row][col] === '.') { const input = document.querySelector(`input[data-row="${row}"][data-col="${col}"]`); if (input) { input.focus(); return; } } } } function moveToPreviousEmptyCell(currentRow, currentCol) { // Start from the previous cell let startIndex = currentRow * 9 + currentCol - 1; for (let i = 0; i < 81; i++) { let index = (startIndex - i + 81) % 81; let row = Math.floor(index / 9); let col = index % 9; if (originalBoard[row][col] === '.' && currentBoard[row][col] === '.') { const input = document.querySelector(`input[data-row="${row}"][data-col="${col}"]`); if (input) { input.focus(); return; } } } } function focusFirstEmptyCell() { for (let i = 0; i < 9; i++) { for (let j = 0; j < 9; j++) { if (originalBoard[i][j] === '.' && currentBoard[i][j] === '.') { const input = document.querySelector(`input[data-row="${i}"][data-col="${j}"]`); if (input) { input.focus(); return; } } } } } // Clear all non-fixed cells function clearBoard() { if (!confirm('Are you sure you want to clear all your entries?')) { return; } for (let i = 0; i < 9; i++) { for (let j = 0; j < 9; j++) { if (originalBoard[i][j] === '.') { currentBoard[i][j] = '.'; const input = document.querySelector(`input[data-row="${i}"][data-col="${j}"]`); if (input) { input.value = ''; input.parentElement.classList.remove('error', 'hint'); } } } } showMessage('Board cleared!', 'info'); focusFirstEmptyCell(); } async function validateBoard() { try { // Ensure board is properly formatted as 2D array const boardToSend = []; for (let i = 0; i < 9; i++) { const row = []; for (let j = 0; j < 9; j++) { row.push(currentBoard[i][j] || '.'); } boardToSend.push(row); } const response = await fetch('/api/sudoku/validate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ board: boardToSend, gameId: currentGameId }) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); // Clear all error styling first document.querySelectorAll('.cell').forEach(cell => { cell.classList.remove('error'); }); if (data.valid) { if (data.complete) { showMessage(data.message, 'success'); // Optionally disable all inputs when puzzle is solved if (data.message.includes('Congratulations')) { document.querySelectorAll('.cell-input').forEach(input => { input.readOnly = true; }); } } else { showMessage(data.message, 'info'); } } else { showMessage(data.message, 'error'); // Highlight cells with errors highlightErrors(); } } catch (error) { showMessage('Error validating board: ' + error.message, 'error'); console.error('Error:', error); } } function highlightErrors() { // Check for duplicates in rows, columns, and boxes for (let i = 0; i < 9; i++) { for (let j = 0; j < 9; j++) { if (currentBoard[i][j] !== '.') { if (hasDuplicate(i, j, currentBoard[i][j])) { const cell = document.querySelector(`input[data-row="${i}"][data-col="${j}"]`).parentElement; cell.classList.add('error'); } } } } } function hasDuplicate(row, col, value) { // Check row for (let j = 0; j < 9; j++) { if (j !== col && currentBoard[row][j] === value) { return true; } } // Check column for (let i = 0; i < 9; i++) { if (i !== row && currentBoard[i][col] === value) { return true; } } // Check 3x3 box const boxRow = Math.floor(row / 3) * 3; const boxCol = Math.floor(col / 3) * 3; for (let i = boxRow; i < boxRow + 3; i++) { for (let j = boxCol; j < boxCol + 3; j++) { if (i !== row && j !== col && currentBoard[i][j] === value) { return true; } } } return false; } async function getHint() { try { // Ensure board is properly formatted as 2D array const boardToSend = []; for (let i = 0; i < 9; i++) { const row = []; for (let j = 0; j < 9; j++) { row.push(currentBoard[i][j] || '.'); } boardToSend.push(row); } const response = await fetch('/api/sudoku/hint', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ board: boardToSend, gameId: currentGameId }) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const newBoard = await response.json(); // Find and highlight the hint cell for (let i = 0; i < 9; i++) { for (let j = 0; j < 9; j++) { if (currentBoard[i][j] === '.' && newBoard[i][j] !== '.') { currentBoard = newBoard; const input = document.querySelector(`input[data-row="${i}"][data-col="${j}"]`); if (input) { input.value = newBoard[i][j]; input.parentElement.classList.add('hint'); input.focus(); // Remove hint highlighting after animation setTimeout(() => { input.parentElement.classList.remove('hint'); }, 2000); } showMessage('Hint provided!', 'info'); return; } } } showMessage('No more hints available', 'info'); } catch (error) { showMessage('Error getting hint: ' + error.message, 'error'); console.error('Error:', error); } } async function showSolution() { if (!confirm('Are you sure you want to see the solution? This will end the game.')) { return; } try { const response = await fetch(`/api/sudoku/solution/${currentGameId}`); const solution = await response.json(); currentBoard = solution; // Update all cells with solution for (let i = 0; i < 9; i++) { for (let j = 0; j < 9; j++) { const input = document.querySelector(`input[data-row="${i}"][data-col="${j}"]`); if (input) { input.value = solution[i][j]; if (originalBoard[i][j] === '.') { input.parentElement.classList.add('hint'); } input.readOnly = true; } } } showMessage('Solution revealed!', 'info'); } catch (error) { showMessage('Error getting solution', 'error'); console.error('Error:', error); } } function showMessage(text, type) { const messageElement = document.getElementById('message'); messageElement.textContent = text; messageElement.className = `message ${type}`; // Clear message after 5 seconds setTimeout(() => { messageElement.textContent = ''; messageElement.className = 'message'; }, 5000); }