Create a Sudoku Puzzle Game with HTML, CSS, and JavaScript

Sudoku is a logic-based number puzzle. The goal is to fill a 9×9 grid so that each row, column, and 3×3 subgrid contains every number from 1 to 9 without repetition. It’s a fun and engaging way to test your problem-solving skills. In this guide, we will learn how to create our own Sudoku puzzle using basic web development skills. Let’s get started!

Game Rules and Approach

Before we start to code for the project, it is important that we first understand the rules of the sudoku puzzle, which will help us to design our logic accordingly.

Sudoku Rules:

  • Each number (1-9) must appear only once in every row.
  • Each number must appear only once in every column.
  • Each number must appear only once in every 3×3 subgrid.

To create a Sudoku puzzle in a web project, we will use HTML to create the grid, CSS to style it, and JavaScript to handle the game logic, including checking for valid inputs and solving the puzzle.

Steps to Create a Sudoku Puzzle

Step 1: Creating Basic HTML Structure

The first step is to create a 9×9 grid using an HTML table. Each cell in the grid will represent a Sudoku box where the player can input numbers.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Sudoku Puzzle</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <h1>It's Soduku Time</h1>
    <div id="sudoku-container">
        <table id="sudoku-board"></table>
        <div class="controls">
            <button id="solve-button">Solve</button>
            <button id="clear-button">Clear</button>
            <button id="check-button">Check Solution</button>
        </div>
    </div>
    <script src="index.js"></script>
</body>
</html>

Explanation:

  • The page title is set as “Sudoku Puzzle”.
  • It links to an external CSS file (style.css) and a JavaScript file (index.js).
  • In <body>, it displays a heading (“It’s Sudoku Time”) and creates an empty table (<table id=”sudoku-board”>) for the Sudoku grid.
  • We have also added three control buttons: ‘Solve‘ to display the solution, ‘Clear‘ to reset the board, and ‘Check Solution‘ to verify if the puzzle has been solved correctly

Output: The H2 with 3 buttons will appear.

HTML Output

Note: The grid will appear once we add the JavaScript logic for creating it, as it’s very tiring to write HTML code for a 9×9 grid.

Step 2: Implementing JavaScript Logic

We will cover JavaScript before CSS, as this is where we will write the logic to convert the empty table into a 9×9 grid and define the functions for the ‘Solve,’ ‘Clear,’ and ‘Check Solution’ buttons.

Firstly, we will create a 9×9 grid:

document.addEventListener('DOMContentLoaded', function() {
    const sudokuBoard = document.getElementById('sudoku-board');
    const solveButton = document.getElementById('solve-button');
    const clearButton = document.getElementById('clear-button');
    const checkButton = document.getElementById('check-button');

    function createBoard() {
        for (let row = 0; row < 9; row++) {
            const tr = document.createElement('tr');
            for (let col = 0; col < 9; col++) {
                const td = document.createElement('td');
                const input = document.createElement('input');
                input.type = 'number';
                input.min = '1';
                input.max = '9';
                td.appendChild(input);
                tr.appendChild(td);
            }
            sudokuBoard.appendChild(tr);
        }
    }

    createBoard();

Now, when the user adds inputs to the grid, we need to convert them into a 2-D array for simplification. This function collects all the numbers from the Sudoku board and returns them as a grid (2D array), using 0 for any empty cells:

function getBoard() {
    return Array.from(sudokuBoard.querySelectorAll('tr')).map(row =>
        Array.from(row.querySelectorAll('td input')).map(cell =>
            cell.value ? parseInt(cell.value) : 0
        )
    );
}

Next, we would need a function to update the board with solved values. This function fills the Sudoku board with numbers from a given 2D array, leaving any 0 values as empty cells:

function setBoard(board) {
    const cells = sudokuBoard.querySelectorAll('td input');
    cells.forEach((cell, index) => {
        cell.value = board[Math.floor(index / 9)][index % 9] || '';
    });
}

Now, a function to check if a number can be placed in the given position. This function checks if a number is allowed in a Sudoku cell by making sure it’s not already in the same row, column, or 3×3 box:

function isValid(board, row, col, num) {
    for (let i = 0; i < 9; i++) {
        if (board[row][i] === num || board[i][col] === num) return false;
    }

    const boxRowStart = Math.floor(row / 3) * 3;
    const boxColStart = Math.floor(col / 3) * 3;

    for (let i = 0; i < 3; i++) {
        for (let j = 0; j < 3; j++) {
            if (board[boxRowStart + i][boxColStart + j] === num) return false;
        }
    }
    return true;
}

Next is the logic to solve the puzzle. This function solves the Sudoku puzzle by trying numbers in empty cells and using backtracking to find a valid solution:

function solveSudoku(board) {
    for (let row = 0; row < 9; row++) {
        for (let col = 0; col < 9; col++) {
            if (board[row][col] === 0) {
                for (let num = 1; num <= 9; num++) {
                    if (isValid(board, row, col, num)) {
                        board[row][col] = num;
                        if (solveSudoku(board)) return true;
                        board[row][col] = 0; // Backtrack
                    }
                }
                return false; // No valid number found
            }
        }
    }
    return true; // Puzzle solved
}

Now to check if the current board is a valid solution, we write a function that checks if the current Sudoku board is valid by ensuring that each number appears only once in its row, column, and 3×3 box:

function isValidSudoku(board) {
    const rows = Array.from({ length: 9 }, () => new Set());
    const cols = Array.from({ length: 9 }, () => new Set());
    const boxes = Array.from({ length: 9 }, () => new Set());

    for (let r = 0; r < 9; r++) {
        for (let c = 0; c < 9; c++) {
            const num = board[r][c];
            if (num === 0) continue; // Ignore empty cells
            if (rows[r].has(num) || cols[c].has(num) || boxes[Math.floor(r / 3) * 3 + Math.floor(c / 3)].has(num)) {
                return false; // Invalid if already present
            }
            rows[r].add(num);
            cols[c].add(num);
            boxes[Math.floor(r / 3) * 3 + Math.floor(c / 3)].add(num);
        }
    }
    return true; // If all checks are passed
}

Next, we write a function to solve the Sudoku when the Solve button is clicked:

solveButton.addEventListener('click', function() {
    const board = getBoard();
    if (solveSudoku(board)) {
        setBoard(board);
        alert('Solution found! Check the board.');
    } else {
        alert('Sudoku cannot be solved.');
    }
});

Now we will add prompts to interact with the user and check if the current board is solved correctly:

checkButton.addEventListener('click', function() {
    const board = getBoard();
    if (isBoardFull(board)) {
        if (isValidSudoku(board)) {
            alert('Congratulations! You solved the Sudoku correctly!');
        } else {
            alert('The solution is incorrect. click clear and then solve to see the correct solution:');
            const solvedBoard = JSON.parse(JSON.stringify(board)); // Clone the board
            if (solveSudoku(solvedBoard)) {
                setBoard(solvedBoard);
            }
        }
    } else {
        alert('Please fill all the cells before checking the solution.');
    }
});

Then, a function to check if the board is full:

function isBoardFull(board) {
    return board.every(row => row.every(cell => cell !== 0));
}

And lastly, a function to clear the board:

clearButton.addEventListener('click', function () {
    document.querySelectorAll('input').forEach(input => input.value = '');
});

Here is the Complete Code:

document.addEventListener('DOMContentLoaded', function() {
    const sudokuBoard = document.getElementById('sudoku-board');
    const solveButton = document.getElementById('solve-button');
    const clearButton = document.getElementById('clear-button');
    const checkButton = document.getElementById('check-button');

    function createBoard() {
        for (let row = 0; row < 9; row++) {
            const tr = document.createElement('tr');
            for (let col = 0; col < 9; col++) {
                const td = document.createElement('td');
                const input = document.createElement('input');
                input.type = 'number';
                input.min = '1';
                input.max = '9';
                td.appendChild(input);
                tr.appendChild(td);
            }
            sudokuBoard.appendChild(tr);
        }
    }

    createBoard();

    function getBoard() {
        return Array.from(sudokuBoard.querySelectorAll('tr')).map(row => 
            Array.from(row.querySelectorAll('td input')).map(cell => 
                cell.value ? parseInt(cell.value) : 0
            )
        );
    }

    function setBoard(board) {
        const cells = sudokuBoard.querySelectorAll('td input');
        cells.forEach((cell, index) => {
            cell.value = board[Math.floor(index / 9)][index % 9] || '';
        });
    }

    function isValid(board, row, col, num) {
        for (let i = 0; i < 9; i++) {
            if (board[row][i] === num || board[i][col] === num) return false;
        }

        const boxRowStart = Math.floor(row / 3) * 3;
        const boxColStart = Math.floor(col / 3) * 3;

        for (let i = 0; i < 3; i++) {
            for (let j = 0; j < 3; j++) {
                if (board[boxRowStart + i][boxColStart + j] === num) return false;
            }
        }
        return true;
    }

    function solveSudoku(board) {
        for (let row = 0; row < 9; row++) {
            for (let col = 0; col < 9; col++) {
                if (board[row][col] === 0) {
                    for (let num = 1; num <= 9; num++) {
                        if (isValid(board, row, col, num)) {
                            board[row][col] = num;
                            if (solveSudoku(board)) return true;
                            board[row][col] = 0;
                        }
                    }
                    return false;
                }
            }
        }
        return true;
    }

    function isValidSudoku(board) {
        const rows = Array.from({ length: 9 }, () => new Set());
        const cols = Array.from({ length: 9 }, () => new Set());
        const boxes = Array.from({ length: 9 }, () => new Set());

        for (let r = 0; r < 9; r++) {
            for (let c = 0; c < 9; c++) {
                const num = board[r][c];
                if (num === 0) continue;
                if (rows[r].has(num) || cols[c].has(num) || boxes[Math.floor(r / 3) * 3 + Math.floor(c / 3)].has(num)) {
                    return false;
                }
                rows[r].add(num);
                cols[c].add(num);
                boxes[Math.floor(r / 3) * 3 + Math.floor(c / 3)].add(num);
            }
        }
        return true;
    }

    solveButton.addEventListener('click', function() {
        const board = getBoard();
        if (solveSudoku(board)) {
            setBoard(board);
            alert('Solution found! Check the board.');
        } else {
            alert('Sudoku cannot be solved.');
        }
    });

    checkButton.addEventListener('click', function() {
        const board = getBoard();
        if (isBoardFull(board)) {
            if (isValidSudoku(board)) {
                alert('Congratulations! You solved the Sudoku correctly!');
            } else {
                alert('The solution is incorrect. click clear and then solve to see the correct solution:');
                const solvedBoard = JSON.parse(JSON.stringify(board));
                if (solveSudoku(solvedBoard)) {
                    setBoard(solvedBoard);
                }
            }
        } else {
            alert('Please fill all the cells before checking the solution.');
        }
    });

    function isBoardFull(board) {
        return board.every(row => row.every(cell => cell !== 0));
    }

    clearButton.addEventListener('click', function() {
        document.querySelectorAll('input').forEach(input => input.value = '');
    });
});

Output: Our Sudoku puzzle is now functional. We have a 9×9 grid where we can input numbers. The ‘Solve’ button will provide the solution, the ‘Clear’ button will reset the board, and the ‘Check Solution’ button will verify our inputs

But wait, this page looks quite simple and boring. Let’s make it more attractive with some styling.

Step 3: Add Styling with CSS

body {
    font-family: Arial, sans-serif;
    text-align: center;
    margin: 0;
    padding: 0;
    background-color: #00246B;
}

h1 {
    font-family: Lucida Bright; 
    color: #CADCFC;                      
    text-align: center;                
    margin: 10px;                   
}

#sudoku-container {
    margin: 50px auto;
    display: inline-block;
    padding: 20px;
    background-color: #CADCFC;
    border-radius: 10px;
    box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.2);
}

table {
    border-collapse: separate; 
    margin: 0 auto;
    border: 3px solid #333; 
    border-spacing: 0px; 
}

td {
    border: 1px solid #999;
    width: 60px;
    height: 60px;
    padding: 0;
}

input {
    width: 100%;
    height: 100%;
    text-align: center;
    font-size: 24px;
    font-weight: bold;
    border:  2px solid #999;
    outline: none;
}

td:nth-child(3n) {
    border-right: 3px solid #333;
}

td:nth-child(3n+1) {
    border-left: 3px solid #333;
}

tr:nth-child(3n) td {
    border-bottom: 3px solid #333;
}

tr:nth-child(3n+1) td {
    border-top: 3px solid #333;
}

.controls {
    margin-top: 20px;
}

button {
    padding: 10px 20px;
    font-size: 16px;
    margin: 5px;
    cursor: pointer;
    background-color: #00246B;
    color: white;
    border: none;
    border-radius: 5px;
    transition: background-color 0.3s;
}

button:hover {
    background-color: #c71878;
}

Explanation:

  • body: Sets a basic font (Arial), centers the content, removes margins/padding, and gives a dark blue background.
  • h1: Uses a different font (Lucida Bright), sets the text color to light blue, centers the title, and adds a small margin.
  • #sudoku-container: Centers the Sudoku board, adds padding, light blue background, rounded corners, and a shadow effect.
  • table: Ensures the grid borders don’t overlap, centers the table, and adds a thick outer border.
  • td: Sets cell size (60×60), adds a border, and removes padding.
  • input: Makes inputs fill the cell, centers the text, and sets larger, bold numbers.
  • td:nth-child(3n) and tr:nth-child(3n): Add thicker borders every third row/column to visually divide the grid.
  • .controls: Adds space above the control buttons.
  • button: Styles buttons with padding, dark blue background, white text, rounded corners, and a hover effect that changes the background color to pink.

Output:

Conclusion

With just a few steps, you can create a fully functional Sudoku puzzle using HTML, CSS, and JavaScript. You can expand this project by adding features like timer functionality, difficulty levels, or even hints for players.

If you like puzzle and gaming projects like this one, then don’t forget to check out these:

Reference

https://stackoverflow.com/questions/23497444/how-to-make-a-sudoku-grid-using-html-and-css

Snigdha Keshariya
Snigdha Keshariya
Articles: 101