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.