Spaces:
Sleeping
Sleeping
| 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); | |
| } |