Novo Jogo
Pausar
Fácil
Médio
Difícil
Extremo
Dica (3)
Desfazer
Lápis
Tempo: 00:00
Erros: 0/3
Pontuação: 100%
📌 Como jogar e melhorar sua performance no Sudoku?
Agora que você começou, veja algumas dicas para dominar o Sudoku Master :
🧠 Use a lógica: Evite palpites e analise cada jogada.
✏️ Modo Lápis: Utilize esta função para testar possibilidades antes de confirmar um número.
🔍 Olhe padrões: Algumas combinações de números se repetem frequentemente.
🚫 Evite erros: Você tem um limite de erros. Jogue com estratégia!
🔄 Quer tentar outro nível? Selecione uma nova dificuldade e continue melhorando! 🎯
/**
* Sudoku Game - Refatorado e Otimizado (2026)
* Melhorias: Performance, legibilidade, modularidade, eliminação de bugs
*/
class SudokuGame {
constructor() {
// Cache de elementos DOM
this.DOM = {
board: document.getElementById('sudoku-board'),
modal: document.getElementById('modal'),
modalTitle: document.getElementById('modal-title'),
modalMessage: document.getElementById('modal-message'),
timerDisplay: document.getElementById('timer'),
errorsDisplay: document.getElementById('errors'),
scoreDisplay: document.getElementById('score'),
hintBtn: document.getElementById('hint'),
undoBtn: document.getElementById('undo'),
pauseBtn: document.getElementById('pause'),
togglePencilBtn: document.getElementById('toggle-pencil'),
difficultySelect: document.getElementById('difficulty'),
newGameBtn: document.getElementById('new-game'),
modalClose: document.getElementById('modal-close')
};
// Constantes do jogo
this.CONSTANTS = {
BOARD_SIZE: 9,
BOX_SIZE: 3,
MAX_ERRORS: 3,
MAX_HINTS: 3,
DIFFICULTIES: {
easy: 35,
medium: 45,
hard: 52,
extreme: 58
},
SCORE_PENALTIES: {
TIME_PER_MINUTE: 1.5,
HINT: 5,
ERROR: 10
}
};
// Estado do jogo
this.state = {
sudoku: [],
solution: [],
timer: null,
seconds: 0,
errors: 0,
hintsUsed: 0,
pencilMode: false,
gameOver: false,
paused: false,
moveHistory: [],
selectedNumber: null
};
this.init();
}
// ============= INICIALIZAÇÃO =============
init() {
this.bindEvents();
this.newGame();
}
bindEvents() {
// Botões principais
this.DOM.newGameBtn.addEventListener('click', () => this.handleNewGame());
this.DOM.pauseBtn.addEventListener('click', () => this.togglePause());
this.DOM.hintBtn.addEventListener('click', () => this.useHint());
this.DOM.undoBtn.addEventListener('click', () => this.undoMove());
this.DOM.togglePencilBtn.addEventListener('click', () => this.togglePencilMode());
this.DOM.modalClose.addEventListener('click', () => this.closeModal());
this.DOM.difficultySelect.addEventListener('change', (e) => this.handleDifficultyChange(e));
// Atalhos de teclado
window.addEventListener('keydown', (e) => this.handleGlobalKeyboard(e));
}
// ============= GERAÇÃO DO SUDOKU =============
generateSudoku(difficulty) {
const board = this.createEmptyBoard();
this.fillBoard(board);
// Clonar solução
this.state.solution = board.map(row => [...row]);
// Remover células baseado na dificuldade
const cellsToRemove = this.CONSTANTS.DIFFICULTIES[difficulty] || 35;
this.removeNumbers(board, cellsToRemove);
return board;
}
createEmptyBoard() {
return Array.from({ length: this.CONSTANTS.BOARD_SIZE },
() => Array(this.CONSTANTS.BOARD_SIZE).fill(0)
);
}
fillBoard(board) {
return this.solveSudoku(board, true);
}
solveSudoku(board, randomize = false) {
const emptyCell = this.findEmptyCell(board);
if (!emptyCell) return true;
const [row, col] = emptyCell;
const numbers = randomize
? this.shuffleArray([1, 2, 3, 4, 5, 6, 7, 8, 9])
: [1, 2, 3, 4, 5, 6, 7, 8, 9];
for (const num of numbers) {
if (this.isValidPlacement(board, row, col, num)) {
board[row][col] = num;
if (this.solveSudoku(board, randomize)) {
return true;
}
board[row][col] = 0; // Backtrack
}
}
return false;
}
findEmptyCell(board) {
for (let i = 0; i < this.CONSTANTS.BOARD_SIZE; i++) {
for (let j = 0; j < this.CONSTANTS.BOARD_SIZE; j++) {
if (board[i][j] === 0) return [i, j];
}
}
return null;
}
isValidPlacement(board, row, col, num) {
// Verificar linha e coluna
for (let x = 0; x < this.CONSTANTS.BOARD_SIZE; x++) {
if (board[row][x] === num || board[x][col] === num) {
return false;
}
}
// Verificar quadrante 3x3
const boxRow = Math.floor(row / this.CONSTANTS.BOX_SIZE) * this.CONSTANTS.BOX_SIZE;
const boxCol = Math.floor(col / this.CONSTANTS.BOX_SIZE) * this.CONSTANTS.BOX_SIZE;
for (let i = 0; i < this.CONSTANTS.BOX_SIZE; i++) {
for (let j = 0; j < this.CONSTANTS.BOX_SIZE; j++) {
if (board[boxRow + i][boxCol + j] === num) {
return false;
}
}
}
return true;
}
removeNumbers(board, count) {
const positions = [];
// Criar lista de todas as posições
for (let i = 0; i < this.CONSTANTS.BOARD_SIZE; i++) {
for (let j = 0; j < this.CONSTANTS.BOARD_SIZE; j++) {
positions.push([i, j]);
}
}
// Embaralhar e remover
this.shuffleArray(positions);
for (let i = 0; i < Math.min(count, positions.length); i++) {
const [row, col] = positions[i];
board[row][col] = 0;
}
}
shuffleArray(array) {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
// ============= RENDERIZAÇÃO =============
renderBoard() {
// Usar DocumentFragment para melhor performance
const fragment = document.createDocumentFragment();
for (let i = 0; i < this.CONSTANTS.BOARD_SIZE; i++) {
for (let j = 0; j < this.CONSTANTS.BOARD_SIZE; j++) {
const cell = this.createCell(i, j);
fragment.appendChild(cell);
}
}
this.DOM.board.innerHTML = '';
this.DOM.board.appendChild(fragment);
// Aplicar highlight inicial se houver número selecionado
if (this.state.selectedNumber) {
this.highlightRelatedNumbers(this.state.selectedNumber);
}
}
createCell(row, col) {
const cell = document.createElement('div');
cell.className = 'cell';
cell.dataset.row = row;
cell.dataset.col = col;
// Adicionar bordas de quadrante
if (col % this.CONSTANTS.BOX_SIZE === 0 && col !== 0) {
cell.classList.add('quadrant-border-left');
}
if (row % this.CONSTANTS.BOX_SIZE === 0 && row !== 0) {
cell.classList.add('quadrant-border-top');
}
const input = this.createInput(row, col);
cell.appendChild(input);
return cell;
}
createInput(row, col) {
const input = document.createElement('input');
input.type = 'tel';
input.maxLength = 1;
input.dataset.row = row;
input.dataset.col = col;
input.setAttribute('inputmode', 'numeric');
input.setAttribute('pattern', '[1-9]');
const value = this.state.sudoku[row][col];
if (value !== 0) {
input.value = value;
input.parentElement?.classList.add('fixed');
input.disabled = true;
input.setAttribute('aria-label', `Célula fixa com valor ${value}`);
} else {
input.setAttribute('aria-label', `Célula vazia linha ${row + 1} coluna ${col + 1}`);
}
// Event listeners
input.addEventListener('input', (e) => this.handleInput(e));
input.addEventListener('focus', (e) => this.handleFocus(e));
input.addEventListener('keydown', (e) => this.handleKeyDown(e));
input.addEventListener('blur', () => {
// Delay para permitir cliques em outras células
setTimeout(() => this.clearHighlights(), 100);
});
return input;
}
// ============= MANIPULAÇÃO DE ENTRADA =============
handleInput(e) {
if (this.state.gameOver || this.state.paused) return;
const input = e.target;
const row = parseInt(input.dataset.row, 10);
const col = parseInt(input.dataset.col, 10);
const cell = input.parentElement;
// Validar entrada
let value = input.value.replace(/[^1-9]/g, '');
if (value.length > 1) {
value = value.slice(-1);
}
input.value = value;
// Verificar se é célula fixa
if (cell.classList.contains('fixed')) {
input.value = this.state.sudoku[row][col] || '';
return;
}
// Salvar histórico antes de modificar
const oldValue = this.state.sudoku[row][col];
const oldPencilNotes = cell.querySelector('.pencil-mode')?.innerHTML || '';
if (this.state.pencilMode) {
this.handlePencilInput(cell, value);
this.state.selectedNumber = value || null;
} else if (value) {
this.handleRegularInput(input, row, col, parseInt(value, 10), cell);
this.state.selectedNumber = value;
this.highlightRelatedNumbers(value);
} else {
// Limpar célula
this.state.sudoku[row][col] = 0;
cell.classList.remove('wrong');
this.state.selectedNumber = null;
}
// Adicionar ao histórico
this.state.moveHistory.push({
row,
col,
oldValue,
newValue: value ? parseInt(value, 10) : 0,
isPencil: this.state.pencilMode,
oldPencilNotes
});
this.updateUndoButton();
}
handlePencilInput(cell, value) {
let pencilDiv = cell.querySelector('.pencil-mode');
if (!pencilDiv) {
pencilDiv = document.createElement('div');
pencilDiv.className = 'pencil-mode';
pencilDiv.setAttribute('aria-label', 'Notas a lápis');
// Criar 9 spans para os números 1-9
for (let i = 0; i < this.CONSTANTS.BOARD_SIZE; i++) {
const span = document.createElement('span');
span.dataset.number = i + 1;
pencilDiv.appendChild(span);
}
cell.appendChild(pencilDiv);
}
const input = cell.querySelector('input');
input.value = '';
if (value) {
const index = parseInt(value, 10) - 1;
const spans = pencilDiv.querySelectorAll('span');
const targetSpan = spans[index];
// Toggle nota
if (targetSpan.textContent) {
targetSpan.textContent = '';
} else {
targetSpan.textContent = value;
}
}
}
handleRegularInput(input, row, col, value, cell) {
// Remover notas a lápis
cell.querySelector('.pencil-mode')?.remove();
if (value === this.state.solution[row][col]) {
// Resposta correta
this.state.sudoku[row][col] = value;
cell.classList.remove('wrong');
// Verificar vitória
requestAnimationFrame(() => this.checkWin());
} else {
// Resposta incorreta
this.state.errors++;
this.updateErrorsDisplay();
cell.classList.add('wrong');
// Feedback sonoro (se disponível)
this.playErrorFeedback();
// Limpar após delay
setTimeout(() => {
input.value = '';
cell.classList.remove('wrong');
this.state.sudoku[row][col] = 0;
}, 800);
if (this.state.errors >= this.CONSTANTS.MAX_ERRORS) {
this.endGame(false);
}
}
}
handleKeyDown(e) {
const input = e.target;
const row = parseInt(input.dataset.row, 10);
const col = parseInt(input.dataset.col, 10);
let nextRow = row;
let nextCol = col;
switch (e.key) {
case 'ArrowLeft':
nextCol = Math.max(0, col - 1);
break;
case 'ArrowRight':
nextCol = Math.min(this.CONSTANTS.BOARD_SIZE - 1, col + 1);
break;
case 'ArrowUp':
nextRow = Math.max(0, row - 1);
break;
case 'ArrowDown':
nextRow = Math.min(this.CONSTANTS.BOARD_SIZE - 1, row + 1);
break;
case 'Backspace':
case 'Delete':
if (!input.parentElement.classList.contains('fixed')) {
input.value = '';
this.state.sudoku[row][col] = 0;
input.parentElement.querySelector('.pencil-mode')?.remove();
}
return;
default:
return;
}
e.preventDefault();
const nextInput = this.DOM.board.querySelector(
`input[data-row="${nextRow}"][data-col="${nextCol}"]`
);
nextInput?.focus();
}
handleFocus(e) {
if (this.state.gameOver || this.state.paused) return;
const input = e.target;
const row = parseInt(input.dataset.row, 10);
const col = parseInt(input.dataset.col, 10);
const value = input.value;
this.clearHighlights();
this.highlightRelatedCells(row, col);
if (value) {
this.highlightRelatedNumbers(value);
}
}
handleGlobalKeyboard(e) {
// Prevenir ações se modal estiver aberto
if (this.DOM.modal.style.display === 'flex') return;
const key = e.key.toLowerCase();
switch (key) {
case 'p':
e.preventDefault();
this.togglePause();
break;
case 'z':
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
this.undoMove();
}
break;
case 'l':
e.preventDefault();
this.togglePencilMode();
break;
case 'h':
if (!this.state.gameOver && !this.state.paused) {
e.preventDefault();
this.useHint();
}
break;
}
}
// ============= HIGHLIGHTS =============
highlightRelatedCells(row, col) {
const cells = this.DOM.board.querySelectorAll('.cell');
cells.forEach(cell => {
const cellRow = parseInt(cell.dataset.row, 10);
const cellCol = parseInt(cell.dataset.col, 10);
const sameRow = cellRow === row;
const sameCol = cellCol === col;
const sameBox =
Math.floor(cellRow / this.CONSTANTS.BOX_SIZE) === Math.floor(row / this.CONSTANTS.BOX_SIZE) &&
Math.floor(cellCol / this.CONSTANTS.BOX_SIZE) === Math.floor(col / this.CONSTANTS.BOX_SIZE);
if (sameRow || sameCol || sameBox) {
cell.classList.add('highlight');
}
});
}
highlightRelatedNumbers(value) {
if (!value) return;
const cells = this.DOM.board.querySelectorAll('.cell');
cells.forEach(cell => {
const input = cell.querySelector('input');
const pencilNotes = cell.querySelector('.pencil-mode');
const hasValue = input.value === value;
const hasPencilNote = pencilNotes &&
Array.from(pencilNotes.querySelectorAll('span')).some(
span => span.textContent === value
);
if (hasValue || hasPencilNote) {
cell.classList.add('highlight-number');
}
});
}
clearHighlights() {
const cells = this.DOM.board.querySelectorAll('.cell');
cells.forEach(cell => {
cell.classList.remove('highlight', 'highlight-number');
});
}
// ============= FUNCIONALIDADES =============
useHint() {
if (this.state.hintsUsed >= this.CONSTANTS.MAX_HINTS) {
this.showTemporaryMessage('Você já usou todas as dicas disponíveis!');
return;
}
if (this.state.gameOver || this.state.paused) return;
// Encontrar células vazias
const emptyCells = [];
for (let i = 0; i < this.CONSTANTS.BOARD_SIZE; i++) {
for (let j = 0; j < this.CONSTANTS.BOARD_SIZE; j++) {
if (this.state.sudoku[i][j] === 0) {
emptyCells.push({ row: i, col: j });
}
}
}
if (emptyCells.length === 0) return;
// Escolher célula aleatória
const randomIndex = Math.floor(Math.random() * emptyCells.length);
const { row, col } = emptyCells[randomIndex];
const value = this.state.solution[row][col];
// Aplicar dica
this.state.sudoku[row][col] = value;
const cell = this.DOM.board.querySelector(
`.cell[data-row="${row}"][data-col="${col}"]`
);
const input = cell.querySelector('input');
input.value = value;
cell.classList.add('hint-used');
// Remover highlight após animação
setTimeout(() => cell.classList.remove('hint-used'), 1000);
this.state.hintsUsed++;
this.updateHintButton();
// Verificar vitória
requestAnimationFrame(() => this.checkWin());
}
undoMove() {
if (this.state.moveHistory.length === 0 || this.state.gameOver || this.state.paused) {
return;
}
const lastMove = this.state.moveHistory.pop();
const cell = this.DOM.board.querySelector(
`.cell[data-row="${lastMove.row}"][data-col="${lastMove.col}"]`
);
const input = cell.querySelector('input');
// Restaurar valor anterior
this.state.sudoku[lastMove.row][lastMove.col] = lastMove.oldValue;
if (lastMove.isPencil) {
// Restaurar notas a lápis
let pencilDiv = cell.querySelector('.pencil-mode');
if (lastMove.oldPencilNotes && !pencilDiv) {
pencilDiv = document.createElement('div');
pencilDiv.className = 'pencil-mode';
cell.appendChild(pencilDiv);
}
if (pencilDiv) {
pencilDiv.innerHTML = lastMove.oldPencilNotes;
}
input.value = '';
} else {
// Restaurar valor regular
input.value = lastMove.oldValue || '';
cell.querySelector('.pencil-mode')?.remove();
}
cell.classList.remove('wrong');
this.state.selectedNumber = lastMove.oldValue?.toString() || null;
this.updateUndoButton();
if (this.state.selectedNumber) {
this.highlightRelatedNumbers(this.state.selectedNumber);
}
}
togglePencilMode() {
this.state.pencilMode = !this.state.pencilMode;
const iconClass = this.state.pencilMode ? 'pencil-alt' : 'pencil-alt';
const text = this.state.pencilMode ? 'Normal' : 'Lápis';
this.DOM.togglePencilBtn.innerHTML = `
${text}`;
this.DOM.board.classList.toggle('pencil-active', this.state.pencilMode);
}
togglePause() {
this.state.paused = !this.state.paused;
const icon = this.state.paused ? 'play' : 'pause';
const text = this.state.paused ? 'Continuar' : 'Pausar';
this.DOM.pauseBtn.innerHTML = `
${text}`;
this.DOM.board.classList.toggle('paused', this.state.paused);
// Desabilitar inputs durante pausa
const inputs = this.DOM.board.querySelectorAll('.cell input');
inputs.forEach(input => {
if (!input.parentElement.classList.contains('fixed')) {
input.disabled = this.state.paused;
}
});
}
// ============= TIMER E PONTUAÇÃO =============
startTimer() {
this.stopTimer();
this.state.timer = setInterval(() => {
if (!this.state.paused && !this.state.gameOver) {
this.state.seconds++;
this.updateDisplay();
}
}, 1000);
}
stopTimer() {
if (this.state.timer) {
clearInterval(this.state.timer);
this.state.timer = null;
}
}
calculateScore() {
const { TIME_PER_MINUTE, HINT, ERROR } = this.CONSTANTS.SCORE_PENALTIES;
const timePenalty = Math.floor(this.state.seconds / 60) * TIME_PER_MINUTE;
const hintPenalty = this.state.hintsUsed * HINT;
const errorPenalty = this.state.errors * ERROR;
return Math.max(0, Math.floor(100 - timePenalty - hintPenalty - errorPenalty));
}
updateDisplay() {
// Atualizar timer
const minutes = Math.floor(this.state.seconds / 60);
const seconds = this.state.seconds % 60;
this.DOM.timerDisplay.textContent =
`Tempo: ${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
// Atualizar pontuação
const score = this.calculateScore();
this.DOM.scoreDisplay.textContent = `Pontuação: ${score}%`;
}
updateErrorsDisplay() {
this.DOM.errorsDisplay.textContent =
`Erros: ${this.state.errors}/${this.CONSTANTS.MAX_ERRORS}`;
}
updateHintButton() {
const remaining = this.CONSTANTS.MAX_HINTS - this.state.hintsUsed;
this.DOM.hintBtn.innerHTML = `
Dica (${remaining})`;
this.DOM.hintBtn.disabled = this.state.hintsUsed >= this.CONSTANTS.MAX_HINTS;
}
updateUndoButton() {
this.DOM.undoBtn.disabled = this.state.moveHistory.length === 0;
}
// ============= VERIFICAÇÃO E FIM DE JOGO =============
checkWin() {
for (let i = 0; i < this.CONSTANTS.BOARD_SIZE; i++) {
for (let j = 0; j < this.CONSTANTS.BOARD_SIZE; j++) {
if (this.state.sudoku[i][j] !== this.state.solution[i][j]) {
return false;
}
}
}
this.endGame(true);
return true;
}
endGame(win) {
this.stopTimer();
this.state.gameOver = true;
const finalScore = this.calculateScore();
const minutes = Math.floor(this.state.seconds / 60);
const seconds = this.state.seconds % 60;
this.DOM.modalTitle.textContent = win ? '🎉 Parabéns!' : '😔 Fim de Jogo';
if (win) {
this.DOM.modalMessage.innerHTML = `
Você completou o Sudoku com sucesso!
Estatísticas:
Pontuação Final:
${finalScore}%
Tempo: ${minutes}m ${seconds}s
Erros: ${this.state.errors}
Dicas Usadas: ${this.state.hintsUsed}/${this.CONSTANTS.MAX_HINTS}
`;
} else {
this.DOM.modalMessage.innerHTML = `
Você atingiu o limite de ${this.CONSTANTS.MAX_ERRORS} erros.
Não desanime! Tente novamente com um novo jogo.
`;
}
this.showModal();
}
// ============= CONTROLE DE JOGO =============
handleNewGame() {
if (this.state.gameOver) {
this.newGame();
return;
}
if (confirm('Iniciar novo jogo? O progresso atual será perdido.')) {
this.newGame();
}
}
handleDifficultyChange(e) {
const previousValue = this.DOM.difficultySelect.dataset.lastValue;
if (this.state.gameOver) {
this.newGame();
return;
}
if (confirm('Mudar a dificuldade iniciará um novo jogo. Continuar?')) {
this.newGame();
} else {
this.DOM.difficultySelect.value = previousValue;
}
}
newGame() {
// Parar timer anterior
this.stopTimer();
this.clearHighlights();
// Gerar novo sudoku
const difficulty = this.DOM.difficultySelect.value;
this.state.sudoku = this.generateSudoku(difficulty);
this.DOM.difficultySelect.dataset.lastValue = difficulty;
// Resetar estado
this.state.seconds = 0;
this.state.errors = 0;
this.state.hintsUsed = 0;
this.state.gameOver = false;
this.state.paused = false;
this.state.pencilMode = false;
this.state.moveHistory = [];
this.state.selectedNumber = null;
// Renderizar tabuleiro
this.renderBoard();
// Resetar UI
this.updateDisplay();
this.updateErrorsDisplay();
this.updateHintButton();
this.updateUndoButton();
this.DOM.pauseBtn.innerHTML = '
Pausar';
this.DOM.togglePencilBtn.innerHTML = '
Lápis';
this.DOM.board.classList.remove('paused', 'pencil-active');
// Iniciar timer
this.startTimer();
// Focar primeira célula vazia
requestAnimationFrame(() => {
const firstEmpty = this.DOM.board.querySelector('input:not([disabled])');
firstEmpty?.focus();
});
}
// ============= MODAL =============
showModal() {
this.DOM.modal.style.display = 'flex';
this.DOM.modal.setAttribute('aria-hidden', 'false');
// Focar botão de fechar para acessibilidade
requestAnimationFrame(() => {
this.DOM.modalClose.focus();
});
}
closeModal() {
this.DOM.modal.style.display = 'none';
this.DOM.modal.setAttribute('aria-hidden', 'true');
}
// ============= FEEDBACK =============
playErrorFeedback() {
// Vibração (se disponível)
if ('vibrate' in navigator) {
navigator.vibrate(200);
}
}
showTemporaryMessage(message) {
// Implementar tooltip ou notificação temporária
alert(message);
}
}
// Inicializar jogo quando DOM estiver pronto
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => new SudokuGame());
} else {
new SudokuGame();
}
0 Comentários