Crear Generador Sudoku con JavaScript
Tutorial completo de programación para crear tus propios algoritmos de generación de sudoku
¿Por Qué Crear un Generador JavaScript de Sudoku?
Crear un generador de sudoku en JavaScript enseña conceptos fundamentales de programación mientras resuelves un problema complejo de satisfacción de restricciones. Este tutorial proporciona experiencia práctica con algoritmos, estructuras de datos y técnicas de resolución de problemas esenciales para el desarrollo de software. Tu generador creará puzzles como nuestros desafíos de dificultad media y potencialmente puzzles difíciles con la implementación adecuada del algoritmo.
Lo que Aprenderás
- Algoritmos de satisfacción de restricciones
- Implementación de búsqueda con backtracking
- Técnicas de validación de cuadrícula
- Generación aleatoria de puzzles
- Control de niveles de dificultad
- Optimización de rendimiento
- Integración de interfaz de usuario
- Pruebas y validación
Requisitos Previos y Configuración
Conocimiento Requerido
Antes de comenzar, deberías tener:
- JavaScript Básico: Variables, funciones, arrays y objetos
- Comprensión de Sudoku: Familiaridad con las reglas del sudoku
- Conceptos de Algoritmos: Recursión y búsqueda básicas
- Herramientas de Desarrollo: Editor de código y navegador web
Configuración del Entorno
Configura tu entorno de desarrollo:
// Estructura básica del archivo HTML
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Generador Sudoku</title>
<style>
.sudoku-grid {
display: grid;
grid-template-columns: repeat(9, 40px);
grid-template-rows: repeat(9, 40px);
gap: 1px;
border: 3px solid #333;
margin: 20px auto;
width: fit-content;
}
.cell {
width: 40px;
height: 40px;
border: 1px solid #ccc;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: bold;
}
</style>
</head>
<body>
<div id="app">
<h2>Generador Sudoku JavaScript</h2>
<button onclick="generateNewPuzzle()">Generar Nuevo Puzzle</button>
<div id="sudoku-container"></div>
</div>
<script src="sudoku-generator.js"></script>
</body>
</html>Paso 1: Estructura Base del Generador
Clase SudokuGenerator
Comencemos con la estructura principal de nuestro generador:
class SudokuGenerator {
constructor() {
this.grid = Array(9).fill().map(() => Array(9).fill(0));
this.solution = Array(9).fill().map(() => Array(9).fill(0));
}
// Método principal para generar un puzzle completo
generatePuzzle(difficulty = 'medium') {
// 1. Crear una solución completa válida
this.generateCompleteSolution();
// 2. Guardar la solución
this.solution = this.grid.map(row => [...row]);
// 3. Remover números para crear el puzzle
this.removeCellsForDifficulty(difficulty);
return {
puzzle: this.grid,
solution: this.solution
};
}
// Crear una cuadrícula sudoku completa válida
generateCompleteSolution() {
// Limpiar la cuadrícula
this.grid = Array(9).fill().map(() => Array(9).fill(0));
// Llenar la cuadrícula usando backtracking
this.fillGrid();
}
// Algoritmo de backtracking para llenar la cuadrícula
fillGrid() {
// Encontrar la siguiente celda vacía
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
if (this.grid[row][col] === 0) {
// Probar números 1-9 en orden aleatorio
const numbers = this.shuffleArray([1,2,3,4,5,6,7,8,9]);
for (let num of numbers) {
if (this.isValidPlacement(row, col, num)) {
this.grid[row][col] = num;
// Recursivamente llenar el resto
if (this.fillGrid()) {
return true;
}
// Backtrack si no funciona
this.grid[row][col] = 0;
}
}
// No se pudo colocar ningún número válido
return false;
}
}
}
// Todas las celdas están llenas
return true;
}Paso 2: Validación de Sudoku
Funciones de Validación
Implementa las reglas de validación del sudoku:
// Verificar si es válido colocar un número en una posición
isValidPlacement(row, col, num) {
return this.isValidInRow(row, num) &&
this.isValidInColumn(col, num) &&
this.isValidInBox(row, col, num);
}
// Verificar validez en la fila
isValidInRow(row, num) {
for (let col = 0; col < 9; col++) {
if (this.grid[row][col] === num) {
return false;
}
}
return true;
}
// Verificar validez en la columna
isValidInColumn(col, num) {
for (let row = 0; row < 9; row++) {
if (this.grid[row][col] === num) {
return false;
}
}
return true;
}
// Verificar validez en la caja 3x3
isValidInBox(row, col, num) {
const boxStartRow = Math.floor(row / 3) * 3;
const boxStartCol = Math.floor(col / 3) * 3;
for (let r = boxStartRow; r < boxStartRow + 3; r++) {
for (let c = boxStartCol; c < boxStartCol + 3; c++) {
if (this.grid[r][c] === num) {
return false;
}
}
}
return true;
}
// Verificar si el puzzle completo es válido
isValidSudoku() {
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
if (this.grid[row][col] !== 0) {
const num = this.grid[row][col];
this.grid[row][col] = 0; // Temporalmente remover
if (!this.isValidPlacement(row, col, num)) {
this.grid[row][col] = num; // Restaurar
return false;
}
this.grid[row][col] = num; // Restaurar
}
}
}
return true;
}Paso 3: Control de Dificultad
Algoritmo de Remoción de Celdas
Controla la dificultad removiendo números estratégicamente:
// Remover celdas según la dificultad deseada
removeCellsForDifficulty(difficulty) {
const difficultySettings = {
'easy': { cellsToRemove: 35, maxAttempts: 50 },
'medium': { cellsToRemove: 45, maxAttempts: 100 },
'hard': { cellsToRemove: 55, maxAttempts: 200 },
'expert': { cellsToRemove: 65, maxAttempts: 500 }
};
const settings = difficultySettings[difficulty] || difficultySettings['medium'];
let cellsRemoved = 0;
let attempts = 0;
while (cellsRemoved < settings.cellsToRemove && attempts < settings.maxAttempts) {
const row = Math.floor(Math.random() * 9);
const col = Math.floor(Math.random() * 9);
// Solo intentar remover si la celda no está vacía
if (this.grid[row][col] !== 0) {
const backup = this.grid[row][col];
this.grid[row][col] = 0;
// Verificar que el puzzle aún tenga solución única
if (this.hasUniqueSolution()) {
cellsRemoved++;
} else {
// Restaurar si la remoción causa múltiples soluciones
this.grid[row][col] = backup;
}
}
attempts++;
}
}
// Verificar si el puzzle tiene exactamente una solución
hasUniqueSolution() {
const tempGrid = this.grid.map(row => [...row]);
const solutionCount = this.countSolutions(0);
this.grid = tempGrid; // Restaurar cuadrícula original
return solutionCount === 1;
}
// Contar el número total de soluciones (parar en 2 para eficiencia)
countSolutions(solutionsFound) {
if (solutionsFound >= 2) return solutionsFound; // Parar si encontramos múltiples
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
if (this.grid[row][col] === 0) {
for (let num = 1; num <= 9; num++) {
if (this.isValidPlacement(row, col, num)) {
this.grid[row][col] = num;
solutionsFound = this.countSolutions(solutionsFound);
this.grid[row][col] = 0;
if (solutionsFound >= 2) return solutionsFound;
}
}
return solutionsFound; // Celda vacía encontrada, regresa
}
}
}
return solutionsFound + 1; // Cuadrícula completa, solución encontrada
}Paso 4: Funciones Utilitarias
Funciones de Soporte
Implementa funciones auxiliares para aleatoriedad y utilidades:
// Mezclar array usando algoritmo Fisher-Yates
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;
}
// Imprimir cuadrícula en la consola (para debugging)
printGrid() {
console.log('Cuadrícula Sudoku:');
for (let row = 0; row < 9; row++) {
let rowStr = '';
for (let col = 0; col < 9; col++) {
rowStr += (this.grid[row][col] || '.') + ' ';
if (col === 2 || col === 5) rowStr += '| ';
}
console.log(rowStr);
if (row === 2 || row === 5) {
console.log('------+-------+------');
}
}
}
// Obtener cuadrícula como string para almacenamiento/compartir
gridToString() {
return this.grid.flat().join('');
}
// Cargar cuadrícula desde string
loadFromString(gridString) {
if (gridString.length !== 81) {
throw new Error('String de cuadrícula debe tener exactamente 81 caracteres');
}
for (let i = 0; i < 81; i++) {
const row = Math.floor(i / 9);
const col = i % 9;
this.grid[row][col] = parseInt(gridString[i]) || 0;
}
}
// Clonar el estado actual de la cuadrícula
cloneGrid() {
return this.grid.map(row => [...row]);
}
// Restaurar cuadrícula desde un clon
restoreGrid(clonedGrid) {
this.grid = clonedGrid.map(row => [...row]);
}Paso 5: Interfaz de Usuario
Renderizado de la Cuadrícula
Crea funciones para mostrar el sudoku en la página web:
// Función global para generar nuevo puzzle
function generateNewPuzzle() {
const generator = new SudokuGenerator();
const result = generator.generatePuzzle('medium');
renderSudokuGrid(result.puzzle);
// Almacenar solución globalmente para verificación
window.currentSolution = result.solution;
}
// Renderizar la cuadrícula en el DOM
function renderSudokuGrid(grid) {
const container = document.getElementById('sudoku-container');
container.innerHTML = ''; // Limpiar contenido anterior
const gridElement = document.createElement('div');
gridElement.className = 'sudoku-grid';
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
const cell = document.createElement('div');
cell.className = 'cell';
// Añadir bordes para separar cajas 3x3
if (col === 2 || col === 5) {
cell.style.borderRight = '3px solid #333';
}
if (row === 2 || row === 5) {
cell.style.borderBottom = '3px solid #333';
}
const value = grid[row][col];
if (value !== 0) {
cell.textContent = value;
cell.style.backgroundColor = '#f0f0f0';
cell.style.fontWeight = 'bold';
} else {
// Hacer celdas editables
cell.contentEditable = true;
cell.style.backgroundColor = 'white';
cell.addEventListener('input', function(e) {
handleCellInput(e, row, col);
});
}
gridElement.appendChild(cell);
}
}
container.appendChild(gridElement);
}
// Manejar entrada de usuario en celdas
function handleCellInput(event, row, col) {
const value = event.target.textContent;
// Validar entrada (solo números 1-9 o vacío)
if (value !== '' && (!/^[1-9]$/.test(value))) {
event.target.textContent = '';
return;
}
// Opcional: validar si el movimiento es válido
if (value !== '') {
const generator = new SudokuGenerator();
generator.grid = getCurrentGridState();
if (!generator.isValidPlacement(row, col, parseInt(value))) {
event.target.style.color = 'red';
} else {
event.target.style.color = 'blue';
}
}
}
// Obtener estado actual de la cuadrícula desde el DOM
function getCurrentGridState() {
const cells = document.querySelectorAll('.cell');
const grid = Array(9).fill().map(() => Array(9).fill(0));
cells.forEach((cell, index) => {
const row = Math.floor(index / 9);
const col = index % 9;
const value = parseInt(cell.textContent) || 0;
grid[row][col] = value;
});
return grid;
}
// Verificar si el puzzle está resuelto correctamente
function checkSolution() {
const currentGrid = getCurrentGridState();
const solution = window.currentSolution;
if (!solution) {
alert('No hay solución para comparar. Genera un nuevo puzzle primero.');
return;
}
let isCorrect = true;
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
if (currentGrid[row][col] !== solution[row][col]) {
isCorrect = false;
break;
}
}
if (!isCorrect) break;
}
if (isCorrect) {
alert('¡Felicidades! Has resuelto el puzzle correctamente.');
} else {
alert('La solución no es correcta. Sigue intentando.');
}
}Paso 6: Optimizaciones y Mejoras
Optimización de Rendimiento
Mejora la eficiencia de tu generador:
Técnicas de Optimización
- Heurística de Selección de Celdas: Priorizar celdas con menos opciones válidas
- Caché de Validación: Almacenar resultados de validación para evitar recálculos
- Generación Progresiva: Usar web workers para evitar bloquear la UI
- Patrones Pre-calculados: Comenzar con patrones válidos conocidos
// Versión optimizada del algoritmo de llenado
fillGridOptimized() {
const emptyCells = this.getEmptyCells();
return this.fillCellsRecursively(emptyCells, 0);
}
// Obtener todas las celdas vacías ordenadas por número de opciones
getEmptyCells() {
const emptyCells = [];
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
if (this.grid[row][col] === 0) {
const possibleValues = this.getPossibleValues(row, col);
emptyCells.push({
row: row,
col: col,
possibilities: possibleValues,
count: possibleValues.length
});
}
}
}
// Ordenar por número de posibilidades (heurística MRV - Most Restricting Value)
emptyCells.sort((a, b) => a.count - b.count);
return emptyCells;
}
// Obtener valores posibles para una celda específica
getPossibleValues(row, col) {
const possible = [];
for (let num = 1; num <= 9; num++) {
if (this.isValidPlacement(row, col, num)) {
possible.push(num);
}
}
return this.shuffleArray(possible);
}
// Llenar celdas recursivamente con optimizaciones
fillCellsRecursively(emptyCells, cellIndex) {
if (cellIndex >= emptyCells.length) {
return true; // Todas las celdas llenas exitosamente
}
const cell = emptyCells[cellIndex];
const possibilities = this.getPossibleValues(cell.row, cell.col);
for (let num of possibilities) {
this.grid[cell.row][cell.col] = num;
if (this.fillCellsRecursively(emptyCells, cellIndex + 1)) {
return true;
}
this.grid[cell.row][cell.col] = 0; // Backtrack
}
return false; // No se pudo llenar esta configuración
}Paso 7: Funcionalidades Adicionales
Análisis de Dificultad
Implementa análisis más sofisticado de la dificultad:
// Analizar la dificultad real del puzzle generado
analyzeDifficulty() {
const techniques = {
nakedSingles: 0,
hiddenSingles: 0,
nakedPairs: 0,
pointingPairs: 0,
boxLineReduction: 0,
nakedTriples: 0,
// Añadir más técnicas según sea necesario
};
const tempGrid = this.grid.map(row => [...row]);
const solver = new SudokuSolver(tempGrid);
while (!solver.isComplete()) {
const progress = solver.solveOneStep();
if (progress.technique) {
techniques[progress.technique]++;
}
if (!progress.madeProgress) {
// El puzzle requiere técnicas avanzadas no implementadas
break;
}
}
return this.calculateDifficultyScore(techniques);
}
// Calcular puntuación de dificultad basada en técnicas requeridas
calculateDifficultyScore(techniques) {
const weights = {
nakedSingles: 1,
hiddenSingles: 2,
nakedPairs: 5,
pointingPairs: 7,
boxLineReduction: 8,
nakedTriples: 12
};
let score = 0;
for (let [technique, count] of Object.entries(techniques)) {
if (weights[technique]) {
score += weights[technique] * count;
}
}
// Clasificar por rangos de puntuación
if (score < 50) return 'easy';
if (score < 100) return 'medium';
if (score < 200) return 'hard';
return 'expert';
}Exportar e Importar Puzzles
Añade funcionalidad para guardar y cargar puzzles:
// Exportar puzzle en formato JSON
exportPuzzle() {
return {
puzzle: this.grid,
solution: this.solution,
difficulty: this.analyzeDifficulty(),
timestamp: new Date().toISOString(),
generator: 'JavaScript Sudoku Generator v1.0'
};
}
// Guardar puzzle en localStorage
savePuzzle(name) {
const puzzleData = this.exportPuzzle();
const savedPuzzles = JSON.parse(localStorage.getItem('sudokuPuzzles') || '{}');
savedPuzzles[name] = puzzleData;
localStorage.setItem('sudokuPuzzles', JSON.stringify(savedPuzzles));
}
// Cargar puzzle desde localStorage
loadPuzzle(name) {
const savedPuzzles = JSON.parse(localStorage.getItem('sudokuPuzzles') || '{}');
if (savedPuzzles[name]) {
this.grid = savedPuzzles[name].puzzle;
this.solution = savedPuzzles[name].solution;
return true;
}
return false;
}
// Generar URL compartible
generateShareableURL() {
const gridString = this.gridToString();
const baseURL = window.location.origin + window.location.pathname;
return `${baseURL}?puzzle=${gridString}`;
}
// Cargar puzzle desde URL
loadFromURL() {
const urlParams = new URLSearchParams(window.location.search);
const puzzleString = urlParams.get('puzzle');
if (puzzleString && puzzleString.length === 81) {
this.loadFromString(puzzleString);
return true;
}
return false;
}Ejemplo de Uso Completo
// HTML adicional para la interfaz completa
<div id="controls">
<button onclick="generateNewPuzzle()">Generar Nuevo</button>
<button onclick="checkSolution()">Verificar Solución</button>
<button onclick="showHint()">Mostrar Pista</button>
<button onclick="solvePuzzle()">Resolver</button>
<select id="difficulty" onchange="changeDifficulty()">
<option value="easy">Fácil</option>
<option value="medium" selected>Medio</option>
<option value="hard">Difícil</option>
<option value="expert">Experto</option>
</select>
<button onclick="sharePuzzle()">Compartir</button>
<input type="text" id="puzzleName" placeholder="Nombre del puzzle">
<button onclick="savePuzzle()">Guardar</button>
</div>
// JavaScript para las funciones de control
function changeDifficulty() {
const difficulty = document.getElementById('difficulty').value;
generateNewPuzzle(difficulty);
}
function showHint() {
const currentGrid = getCurrentGridState();
const solution = window.currentSolution;
if (!solution) {
alert('Genera un puzzle primero.');
return;
}
// Encontrar primera celda vacía y mostrar la respuesta
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
if (currentGrid[row][col] === 0) {
const cellIndex = row * 9 + col;
const cells = document.querySelectorAll('.cell');
cells[cellIndex].textContent = solution[row][col];
cells[cellIndex].style.color = 'green';
cells[cellIndex].contentEditable = false;
return;
}
}
}
alert('¡El puzzle ya está completo!');
}
function solvePuzzle() {
const solution = window.currentSolution;
if (!solution) {
alert('Genera un puzzle primero.');
return;
}
renderSudokuGrid(solution);
alert('Puzzle resuelto automáticamente.');
}
function sharePuzzle() {
const generator = new SudokuGenerator();
generator.grid = getCurrentGridState();
const shareURL = generator.generateShareableURL();
navigator.clipboard.writeText(shareURL).then(() => {
alert('URL copiada al portapapeles!');
});
}
function savePuzzle() {
const name = document.getElementById('puzzleName').value;
if (!name) {
alert('Ingresa un nombre para el puzzle.');
return;
}
const generator = new SudokuGenerator();
generator.grid = getCurrentGridState();
generator.solution = window.currentSolution;
generator.savePuzzle(name);
alert(`Puzzle '${name}' guardado exitosamente!`);
}
// Inicialización al cargar la página
window.addEventListener('load', function() {
const generator = new SudokuGenerator();
// Intentar cargar puzzle desde URL
if (!generator.loadFromURL()) {
// Si no hay puzzle en URL, generar uno nuevo
generateNewPuzzle();
} else {
renderSudokuGrid(generator.grid);
}
});Mejoras Futuras y Extensiones
Funcionalidades Avanzadas
Considera estas mejoras para tu generador:
- Variantes de Sudoku: Implementa variantes como Killer, X-Sudoku, o Thermo
- Solver Integrado: Añade un solucionador automático con explicaciones paso a paso
- Estadísticas: Rastrea tiempo de resolución, movimientos, y progreso del usuario
- Múltiples Tamaños: Soporte para sudokus 4x4, 6x6, y 16x16
- Temas Visuales: Diferentes estilos y colores para la cuadrícula
Optimizaciones de Performance
- Implementar Web Workers para generación en segundo plano
- Añadir caché de puzzles pre-generados
- Optimizar algoritmos de validación
- Implementar lazy loading para la interfaz
Conclusión: Dominando la Generación de Sudoku
Has creado un generador de sudoku en JavaScript completamente funcional que demuestra conceptos avanzados de programación y resolución de problemas. Este proyecto combina algoritmos de backtracking, validación de restricciones y optimización de rendimiento en una aplicación práctica y atractiva.
Tu generador puede crear puzzles de calidad comparable a los que encuentras en nuestros juegos de dificultad media y puede expandirse para generar desafíos más complejos. El código es modular, extensible, y proporciona una base sólida para futuras mejoras.
La experiencia de construir este generador te ha enseñado principios valiosos aplicables a muchos otros proyectos de programación: gestión de estado, validación de datos, algoritmos de búsqueda, y diseño de interfaces de usuario. Estos conceptos fundamentales te servirán bien en el desarrollo de software más amplio.
Continúa experimentando con tu generador, añadiendo nuevas funcionalidades y optimizaciones. Considera explorar nuestras otras guías de construcción de sudoku para aprender sobre técnicas manuales y crear puzzles de variantes especializa das que desafíen incluso a resolutores expertos.
🎯 Descubre el Universo Completo del Sudoku
📊 Progresión Completa de Dificultad
Explora nuestra progresión científicamente diseñada para encontrar tu desafío perfecto:
🟢 Sudoku Fácil Classic Sudoku
Perfecto para construir habilidades fundamentales de razonamiento lógico.
🟡 Sudoku Medio Classic Sudoku ✓
El siguiente paso con desafío equilibrado y solucionabilidad.
🔴 Sudoku Difícil Classic Sudoku
Requiere técnicas avanzadas y pensamiento estratégico.
🟣 Sudoku Experto Classic Sudoku
Desafía incluso a solucionadores experimentados con patrones complejos.
🔵 Sudoku Maestro Classic Sudoku
Puzzles de élite que requieren dominio de todas las técnicas.
⚫ Sudoku Diabólico Classic Sudoku
El desafío definitivo para grandes maestros del sudoku.
🎮 Tipos Alternativos de Sudoku
Expande tus horizontes de resolución de puzzles con estas emocionantes variaciones que ofrecen desafíos únicos:
⚔️ Killer Sudoku
Combina la lógica del sudoku con celdas matemáticas que requieren sumas específicas.
🔷 Sudoku Gigante 16x16
Puzzles masivos de 256 celdas con números hexadecimales para máxima resistencia.
👶 Sudoku para Niños
Introducción perfecta para mentes jóvenes con cuadrículas simplificadas de 4x4 y 6x6.
📚 Recursos de Aprendizaje
Guías completas y estrategias para mejorar las habilidades de resolución.