(function () { "use strict"; const SIZE = 9; const BOX = 3; const DIFFICULTY_CLUES = { easy: 42, medium: 34, hard: 28, expert: 24 }; const DECORATIVE = [ [5,3,4,6,7,8,9,1,2], [6,7,2,1,9,5,3,4,8], [1,9,8,3,4,2,5,6,7], [8,5,9,7,6,1,4,2,3], [4,2,6,8,5,3,7,9,1], [7,1,3,9,2,4,8,5,6], [9,6,1,5,3,7,2,8,4], [2,8,7,4,1,9,6,3,5], [3,4,5,2,8,6,1,7,9], ]; const STATE = { IDLE: 'idle', RUNNING: 'running', PAUSED: 'paused', FINISHED: 'finished' }; let dom = {}; let callbacks = { onWin: null, onAbandon: null }; let state = STATE.IDLE; let solution = [], puzzle = [], given = [], board = [], notes = []; let selected = null; let notesMode = false; let history = []; let mistakes = 0; let timerSeconds = 0; let timerHandle = null; let currentDifficulty = 'medium'; // ---------- Sudoku generation ---------- function emptyGrid(){ return Array.from({length:SIZE}, () => Array(SIZE).fill(0)); } function shuffled(arr){ const a = arr.slice(); for(let i=a.length-1;i>0;i--){ const j = Math.floor(Math.random()*(i+1)); [a[i],a[j]]=[a[j],a[i]]; } return a; } function isValidPlacement(grid, row, col, val){ for(let i=0;i=limit) return; let row=-1, col=-1; outer: for(let r=0;r=limit) return; } } } backtrack(); return count; } function generatePuzzle(clueCount){ const full = emptyGrid(); fillGrid(full); const puz = full.map(r=>r.slice()); const cells = shuffled(Array.from({length:81}, (_,i)=>i)); let removed = 0; const toRemove = 81 - clueCount; for(const idx of cells){ if(removed>=toRemove) break; const row = Math.floor(idx/SIZE), col = idx%SIZE; if(puz[row][col]===0) continue; const backup = puz[row][col]; puz[row][col] = 0; const testGrid = puz.map(r=>r.slice()); const solutions = countSolutions(testGrid, 2); if(solutions !== 1){ puz[row][col] = backup; } else { removed++; } } return { full, puzzle: puz }; } // ---------- Rendering ---------- function renderDecorative(){ dom.board.innerHTML = ''; dom.board.classList.remove('blurred'); for(let r=0;r 0){ const notesGrid = document.createElement('div'); notesGrid.className = 'notes'; for(let n=1;n<=9;n++){ const span = document.createElement('span'); span.textContent = notes[r][c].has(n) ? n : ''; notesGrid.appendChild(span); } cell.appendChild(notesGrid); } cell.addEventListener('click', () => selectCell(r,c)); dom.board.appendChild(cell); } } applySelectionStyles(); } function applySelectionStyles(){ const cellEls = dom.board.querySelectorAll('.cell'); cellEls.forEach(el => el.classList.remove('peer','selected','same-value')); if(!selected) return; const [sr, sc] = selected; const selVal = board[sr][sc]; cellEls.forEach(el => { const r = +el.dataset.row, c = +el.dataset.col; const sameRow = r===sr, sameCol = c===sc; const sameBox = Math.floor(r/BOX)===Math.floor(sr/BOX) && Math.floor(c/BOX)===Math.floor(sc/BOX); if(sameRow || sameCol || sameBox) el.classList.add('peer'); if(r===sr && c===sc) el.classList.add('selected'); if(selVal!==0 && board[r][c]===selVal) el.classList.add('same-value'); }); } function renderKeypad(){ dom.keypad.innerHTML = ''; const counts = Array(10).fill(0); for(let r=0;r=9) btn.classList.add('count-done'); btn.addEventListener('click', () => inputNumber(n)); dom.keypad.appendChild(btn); } } function updateNotesBtn(){ dom.notesBtn.classList.toggle('active', notesMode); } function formatTime(s){ const m = Math.floor(s/60).toString().padStart(2,'0'); const sec = (s%60).toString().padStart(2,'0'); return `${m}:${sec}`; } function updateUiForState(){ dom.difficulty.disabled = (state === STATE.RUNNING || state === STATE.PAUSED); dom.startOverlay.hidden = state !== STATE.IDLE; dom.pauseOverlay.hidden = state !== STATE.PAUSED; dom.gameActions.hidden = !(state === STATE.RUNNING || state === STATE.PAUSED); dom.keypad.style.visibility = (state === STATE.RUNNING) ? 'visible' : 'hidden'; dom.pauseBtn.hidden = state !== STATE.RUNNING; dom.board.classList.toggle('blurred', state === STATE.PAUSED); } // ---------- Timer ---------- function startTimerTick(){ clearInterval(timerHandle); timerHandle = setInterval(() => { timerSeconds++; dom.timer.textContent = formatTime(timerSeconds); }, 1000); } function stopTimerTick(){ clearInterval(timerHandle); timerHandle = null; } // ---------- History (undo) ---------- function pushHistory(){ history.push({ board: board.map(r=>r.slice()), notes: notes.map(row=>row.map(s=>new Set(s))), mistakes }); if(history.length>200) history.shift(); } function undo(){ if(state !== STATE.RUNNING || history.length===0) return; const prev = history.pop(); board = prev.board; notes = prev.notes; mistakes = prev.mistakes; dom.mistakes.textContent = mistakes; renderBoard(); } // ---------- Interaction ---------- function selectCell(r,c){ if(state !== STATE.RUNNING) return; selected = [r,c]; applySelectionStyles(); } function inputNumber(n){ if(state !== STATE.RUNNING || !selected) return; const [r,c] = selected; if(given[r][c]) return; pushHistory(); if(notesMode){ if(board[r][c]!==0) { history.pop(); return; } if(notes[r][c].has(n)) notes[r][c].delete(n); else notes[r][c].add(n); renderBoard(); return; } board[r][c] = n; notes[r][c].clear(); clearPeerNotes(r,c,n); if(solution[r][c] !== n){ mistakes++; dom.mistakes.textContent = mistakes; } renderBoard(); renderKeypad(); checkWin(); } function clearPeerNotes(row,col,n){ for(let i=0;iel.classList.remove('hint'), 900); } checkWin(); } function checkWin(){ for(let r=0;r row.map(s => Array.from(s))), mistakes, timerSeconds })); }catch(e){} } function loadInProgress(){ try{ const raw = localStorage.getItem('sudoku-inprogress'); if(!raw) return false; const data = JSON.parse(raw); currentDifficulty = data.difficulty || 'medium'; solution = data.solution; puzzle = data.puzzle; given = data.given; board = data.board; notes = data.notes.map(row => row.map(arr => new Set(arr))); mistakes = data.mistakes || 0; timerSeconds = data.timerSeconds || 0; history = []; selected = null; dom.difficulty.value = currentDifficulty; dom.mistakes.textContent = mistakes; dom.timer.textContent = formatTime(timerSeconds); renderBoard(); renderKeypad(); state = STATE.PAUSED; updateUiForState(); return true; }catch(e){ return false; } } function clearSavedState(){ try{ localStorage.removeItem('sudoku-inprogress'); }catch(e){} } // ---------- Public lifecycle ---------- function start(difficulty){ currentDifficulty = difficulty || dom.difficulty.value; timerSeconds = 0; mistakes = 0; history = []; selected = null; notesMode = false; updateNotesBtn(); dom.mistakes.textContent = '0'; dom.timer.textContent = '00:00'; dom.msg.textContent = ''; const clueCount = DIFFICULTY_CLUES[currentDifficulty] || 34; const { full, puzzle: puz } = generatePuzzle(clueCount); solution = full; puzzle = puz; given = puz.map(r => r.map(v => v!==0)); board = puz.map(r => r.slice()); notes = Array.from({length:SIZE}, ()=>Array.from({length:SIZE}, ()=>new Set())); state = STATE.RUNNING; renderBoard(); renderKeypad(); updateUiForState(); startTimerTick(); saveInProgress(); } function pause(){ if(state !== STATE.RUNNING) return; state = STATE.PAUSED; stopTimerTick(); updateUiForState(); saveInProgress(); } function resume(){ if(state !== STATE.PAUSED) return; state = STATE.RUNNING; startTimerTick(); updateUiForState(); selected = null; applySelectionStyles(); } function stop(){ if(state !== STATE.RUNNING && state !== STATE.PAUSED) return; const ok = window.confirm(window.I18N ? window.I18N.t('confirm.stop') : 'Abandonner la partie en cours ?'); if(!ok) return; stopTimerTick(); clearSavedState(); const snapshot = { difficulty: currentDifficulty, timeSeconds: timerSeconds, mistakes }; state = STATE.IDLE; renderDecorative(); updateUiForState(); if(snapshot.timeSeconds >= 1 && typeof callbacks.onAbandon === 'function'){ callbacks.onAbandon(snapshot); } } function reset(){ stopTimerTick(); clearSavedState(); state = STATE.IDLE; selected = null; renderDecorative(); updateUiForState(); } function getState(){ return state; } function init(cb){ callbacks = cb || {}; dom = { board: document.getElementById('board'), keypad: document.getElementById('keypad'), timer: document.getElementById('timer'), mistakes: document.getElementById('mistakes'), difficulty: document.getElementById('difficulty'), msg: document.getElementById('msg'), boardWrap: document.getElementById('boardWrap'), startOverlay: document.getElementById('startOverlay'), startBtn: document.getElementById('startBtn'), pauseOverlay: document.getElementById('pauseOverlay'), resumeBtn: document.getElementById('resumeBtn'), gameActions: document.getElementById('gameActions'), pauseBtn: document.getElementById('pauseBtn'), stopBtn: document.getElementById('stopBtn'), undoBtn: document.getElementById('undoBtn'), eraseBtn: document.getElementById('eraseBtn'), notesBtn: document.getElementById('notesBtn'), hintBtn: document.getElementById('hintBtn'), }; dom.startBtn.addEventListener('click', () => start(dom.difficulty.value)); dom.resumeBtn.addEventListener('click', resume); dom.pauseBtn.addEventListener('click', pause); dom.stopBtn.addEventListener('click', stop); dom.undoBtn.addEventListener('click', undo); dom.eraseBtn.addEventListener('click', eraseCell); dom.hintBtn.addEventListener('click', giveHint); dom.notesBtn.addEventListener('click', () => { notesMode = !notesMode; updateNotesBtn(); }); document.addEventListener('keydown', (e) => { if(state !== STATE.RUNNING || !selected) return; if(e.key>='1' && e.key<='9'){ inputNumber(parseInt(e.key,10)); return; } if(e.key==='Backspace' || e.key==='Delete' || e.key==='0'){ eraseCell(); return; } let [r,c] = selected; if(e.key==='ArrowUp') r = Math.max(0, r-1); else if(e.key==='ArrowDown') r = Math.min(8, r+1); else if(e.key==='ArrowLeft') c = Math.max(0, c-1); else if(e.key==='ArrowRight') c = Math.min(8, c+1); else return; e.preventDefault(); selectCell(r,c); }); window.addEventListener('beforeunload', saveInProgress); setInterval(saveInProgress, 5000); if(!loadInProgress()){ state = STATE.IDLE; renderDecorative(); } updateUiForState(); } window.SudokuGame = { init, start, pause, resume, stop, reset, getState, STATE }; })();