Home » Tutorials » How to Build a Sudoku Game in Python

How to Build a Sudoku Game in Python

If you’ve ever killed time in a coffee shop or needed a break during a long day, you’ve probably come across Sudoku. It’s that puzzle where you have a 9×9 grid that you need to fill up with numbers from 1 to 9. The twist? No number can repeat in any row, any column, or within any of the nine 3×3 squares that make up the grid. It’s these simple yet strict rules that make Sudoku both addictive and a fantastic brain teaser.

Today, you’re going to learn how to create your own Sudoku game with Python. We’ll show you how to whip up random puzzles that stick to the classic rules of Sudoku. By the end of this guide, you’ll have everything you need to challenge both newbies and pros alike. So, let’s get started and have some fun programming this beloved puzzle game!

Table of Contents

Getting Started

Let’s get everything set up before we dive into the code part, so make sure to install the pygame library via the terminal or your command prompt by running this command:

$ pip install pygame

Imports

We start by importing pygame, which creates graphical user interfaces and is better suited for game development. Then, we import the random module, which generates random numbers.

import pygame
import random

Sudoku Game Setup

Initialization

We begin by enabling text to be displayed on the screen, then we create the main window and set its geometry and title.

# Initialize Pygame and set up the display
pygame.init()
pygame.font.init()
screen = pygame.display.set_mode((500, 600))
pygame.display.set_caption("SUDOKU - The Pycodes")

Variables Initialization

Here, we have set four variables and given them initial values. We have x_coordinate and y_coordinate, which represent the position of the cell selected by the user. Therefore, they will update whenever the user selects a cell. We set their initial values to 0 to indicate that the user hasn’t selected a cell yet.

x_coordinate = 0
y_coordinate = 0

Next, we have cell_width with an initial value of 500 (screen width) divided by 9 (number of cells in a row) to ensure equal width for all cells.

cell_width = 500 / 9

Lastly, we have user_input_value which stores the value (1 to 9) that the user wants to input and updates the Sudoku grid so it can be displayed later. We set its initial value to be empty, meaning the user hasn’t selected a value yet.

user_input_value = ''

Empty Sudoku Grid

For this step, we will initialize an empty 9×9 grid where our Sudoku game will take place. In other words, this grid will hold the input numbers that the player enters, as well as the randomly generated numbers, which is why we initially set all the cells to be empty.

# Initialize an empty Sudoku grid and save its initial state
# Each cell now holds a tuple: (value, is_initial)
sudoku_grid = [[(0, False) for _ in range(9)] for _ in range(9)]
initial_sudoku_grid = []

Font Initialization

Next, we initialized two fonts: one for the input numbers and another for both the randomly generated numbers and the congratulatory messages.

# Fonts for displaying text
font_user_input = pygame.font.SysFont("arial", 25)
font_instructions = pygame.font.SysFont("arial", 20)
font_congratulations = pygame.font.SysFont("arial", 40)  # Larger font for congratulations

Color Constants

Then, we defined four different colors using RGB values for use later in the script.

# Color definitions
COLOR_WHITE = (255, 255, 255)
COLOR_RED = (255, 0, 0)
COLOR_BLUE = (0, 0, 255)
COLOR_BLACK = (0, 0, 0)
COLOR_GREEN = (0, 255, 0)  # Green color for congratulations message

Error and Congratulations Messages

In this part, we create two variables to store error and congratulatory messages and set them as empty initially. This approach ensures they do not display at the start of the game, but only when their display conditions are met. Additionally, the display_error_until variable controls how long the error message displays.

# Error message display variables
error_message = ""
display_error_until = 0
congratulations_message = ""

Defining Sudoku Game Functions

Now, let’s define the heart of our game:

get_coordinate Function

The first one calculates the x_coordinate and y_coordinate by taking the mouse position (pos), and using integer division of the mouse’s x and y positions by cell_width. It updates the global variables x_coordinate and y_coordinate with these calculated values.

def get_coordinate(pos):
   global x_coordinate, y_coordinate
   x_coordinate = int(pos[0] // cell_width)
   y_coordinate = int(pos[1] // cell_width)

draw_selection_box Function

Then we create this function that draws a box around a cell that the player selects. It determines the box’s position using the x_coordinate and y_coordinate.

def draw_selection_box():
   for i in range(2):
       pygame.draw.line(screen, COLOR_RED, (x_coordinate * cell_width - 3, (y_coordinate + i) * cell_width),
                        (x_coordinate * cell_width + cell_width + 3, (y_coordinate + i) * cell_width), 7)
       pygame.draw.line(screen, COLOR_RED, ((x_coordinate + i) * cell_width, y_coordinate * cell_width),
                        ((x_coordinate + i) * cell_width, y_coordinate * cell_width + cell_width), 7)

draw_sudoku_grid Function

The draw_sudoku_grid() function draws the grid with normal black lines, except for the 3×3 subgrids, which have thicker boundary lines. It colors the cells white and assigns appropriate colors to the numbers: input numbers are red, while randomly generated numbers are blue.

def draw_sudoku_grid():
   for i in range(9):
       for j in range(9):
           num, is_initial = sudoku_grid[j][i]
           pygame.draw.rect(screen, COLOR_WHITE, (i * cell_width, j * cell_width, cell_width, cell_width))
           if num != 0:
               text_color = COLOR_BLUE if is_initial else COLOR_RED
               text_value = font_user_input.render(str(num), True, text_color)
               screen.blit(text_value, (i * cell_width + 15, j * cell_width + 15))
   for i in range(10):
       thick = 7 if i % 3 == 0 else 1
       pygame.draw.line(screen, COLOR_BLACK, (0, i * cell_width), (500, i * cell_width), thick)
       pygame.draw.line(screen, COLOR_BLACK, (i * cell_width, 0), (i * cell_width, 500), thick)

display_error_message(message) Function

To display our error message we define this one which ensures that once the error_message variable stores a message, it is automatically displayed for 2 seconds, thanks to the display_error_until variable.

def display_error_message(message):
   global error_message, display_error_until
   error_message = message
   display_error_until = pygame.time.get_ticks() + 2000  # Display the message for 2 seconds

check_error_message Function

This function resets the error message back to empty after confirming that the time stored in the display_error_until variable has been exceeded.

def check_error_message():
   global error_message
   if pygame.time.get_ticks() > display_error_until:
       error_message = ""

is_valid_move Function

It checks whether the number entered by the player already exists in the same row, column, or 3×3 subgrid.

If it does exist, the function returns False; if not, meaning it is a valid move, it returns True. In other words, the number is unique and not repeated, therefore it does not violate the Sudoku game rules.

def is_valid_move(grid, i, j, val):
   val = int(val)
   for it in range(9):
       if grid[i][it][0] == val or grid[it][j][0] == val:
           return False
   subgrid_x, subgrid_y = 3 * (i // 3), 3 * (j // 3)
   for i in range(subgrid_x, subgrid_x + 3):
       for j in range(subgrid_y, subgrid_y + 3):
           if grid[i][j][0] == val:
               return False
   return True

check_puzzle_solved Function

This one goes through each row, column, and subgrid to ensure that the cells contain the numbers from 1 to 9 without repetition. If this condition is met throughout, indicating that the puzzle is solved, it returns True; otherwise, it returns False.

def check_puzzle_solved(grid):
   for row in grid:
       if sorted(num for num, _ in row) != list(range(1, 10)):
           return False
   for col in range(9):
       if sorted(grid[row][col][0] for row in range(9)) != list(range(1, 10)):
           return False
   for x in range(0, 9, 3):
       for y in range(0, 9, 3):
           subgrid = [grid[y + i][x + j][0] for i in range(3) for j in range(3)]
           if sorted(subgrid) != list(range(1, 10)):
               return False
   return True

display_instructions Function

This function informs the player how to start a new game or reset it by displaying instructions on the screen.

def display_instructions():
   instruction_text = font_instructions.render("PRESS R TO RESET / N FOR NEW GAME", True, COLOR_BLACK)
   screen.blit(instruction_text, (20, 520))

generate_sudoku_puzzle Function

The generate_sudoku_puzzle() function generates a partial Sudoku puzzle by randomly placing numbers from 1 to 9 into an initially empty grid, using the is_valid_move() function to ensure each number adheres to Sudoku rules. It attempts to place numbers 30 times, then stores the resulting grid in the initial_sudoku_grid variable.

def generate_sudoku_puzzle():
   global sudoku_grid, initial_sudoku_grid
   sudoku_grid = [[(0, False) for _ in range(9)] for _ in range(9)]
   for _ in range(30):
       i, j = random.randint(0, 8), random.randint(0, 8)
       num = random.randint(1, 9)
       if is_valid_move(sudoku_grid, i, j, num):
           sudoku_grid[i][j] = (num, True)
   initial_sudoku_grid = [row[:] for row in sudoku_grid]

reset_sudoku_puzzle Function

The last one resets the Sudoku grid to its initial state by copying the contents stored in the initial_sudoku_grid variable.

def reset_sudoku_puzzle():
   global sudoku_grid, initial_sudoku_grid
   sudoku_grid = [row[:] for row in initial_sudoku_grid]


generate_sudoku_puzzle()

Main Game Loop

Lastly, this part ensures that the main window remains active by maintaining while run as True. It manages events such as mouse clicks, detected through pygame.MOUSEBUTTONDOWN, and key presses, detected through pygame.KEYDOWN.

The game is continuously updated with pygame.display.update(). If the player chooses to quit, while run is set to False, and pygame.quit() is called. Additionally, pressing “N” triggers the generate_sudoku_puzzle() function to create a new Sudoku game.

# Main game loop
run = True
while run:
   screen.fill(COLOR_WHITE)
   for event in pygame.event.get():
       if event.type == pygame.QUIT:
           run = False
       if event.type == pygame.MOUSEBUTTONDOWN:
           pos = pygame.mouse.get_pos()
           get_coordinate(pos)
       if event.type == pygame.KEYDOWN:
           if pygame.K_1 <= event.key <= pygame.K_9:
               user_input_value = str(event.key - 48)  # Convert pygame key to string representation of integer
           elif event.key == pygame.K_r:
               reset_sudoku_puzzle()
           elif event.key == pygame.K_n:
               generate_sudoku_puzzle()
           elif event.key == pygame.K_UP:
               y_coordinate = (y_coordinate - 1) % 9
           elif event.key == pygame.K_DOWN:
               y_coordinate = (y_coordinate + 1) % 9
           elif event.key == pygame.K_LEFT:
               x_coordinate = (x_coordinate - 1) % 9
           elif event.key == pygame.K_RIGHT:
               x_coordinate = (x_coordinate + 1) % 9


   if user_input_value:
       num, is_initial = sudoku_grid[y_coordinate][x_coordinate]
       if not is_initial:
           if num == int(user_input_value):
               sudoku_grid[y_coordinate][x_coordinate] = (0, False)  # Clear the cell
           elif is_valid_move(sudoku_grid, y_coordinate, x_coordinate, user_input_value):
               sudoku_grid[y_coordinate][x_coordinate] = (int(user_input_value), False)  # Update with new input
               if check_puzzle_solved(sudoku_grid):
                   congratulations_message = "Congratulations! Puzzle Solved!"
           else:
               display_error_message("Invalid move!")
       user_input_value = ''


   draw_sudoku_grid()
   draw_selection_box()
   display_instructions()


   check_error_message()
   if error_message:
       error_text = font_instructions.render(error_message, True, COLOR_RED)
       screen.blit(error_text, (20, 550))
   if congratulations_message:
       congrats_text = font_congratulations.render(congratulations_message, True, COLOR_GREEN)
       text_rect = congrats_text.get_rect(center=(250, 300))
       screen.blit(congrats_text, text_rect)


   pygame.display.update()


pygame.quit()

Example

We’ve run this code on a Linux system

And also on a Windows system

Full Code

import pygame
import random


# Initialize Pygame and set up the display
pygame.init()
pygame.font.init()
screen = pygame.display.set_mode((500, 600))
pygame.display.set_caption("SUDOKU - The Pycodes")


# Grid coordinates, dimensions, and initial user input value
x_coordinate = 0
y_coordinate = 0
cell_width = 500 / 9
user_input_value = ''


# Initialize an empty Sudoku grid and save its initial state
# Each cell now holds a tuple: (value, is_initial)
sudoku_grid = [[(0, False) for _ in range(9)] for _ in range(9)]
initial_sudoku_grid = []


# Fonts for displaying text
font_user_input = pygame.font.SysFont("arial", 25)
font_instructions = pygame.font.SysFont("arial", 20)
font_congratulations = pygame.font.SysFont("arial", 40)  # Larger font for congratulations


# Color definitions
COLOR_WHITE = (255, 255, 255)
COLOR_RED = (255, 0, 0)
COLOR_BLUE = (0, 0, 255)
COLOR_BLACK = (0, 0, 0)
COLOR_GREEN = (0, 255, 0)  # Green color for congratulations message


# Error message display variables
error_message = ""
display_error_until = 0
congratulations_message = ""


def get_coordinate(pos):
   global x_coordinate, y_coordinate
   x_coordinate = int(pos[0] // cell_width)
   y_coordinate = int(pos[1] // cell_width)


def draw_selection_box():
   for i in range(2):
       pygame.draw.line(screen, COLOR_RED, (x_coordinate * cell_width - 3, (y_coordinate + i) * cell_width),
                        (x_coordinate * cell_width + cell_width + 3, (y_coordinate + i) * cell_width), 7)
       pygame.draw.line(screen, COLOR_RED, ((x_coordinate + i) * cell_width, y_coordinate * cell_width),
                        ((x_coordinate + i) * cell_width, y_coordinate * cell_width + cell_width), 7)


def draw_sudoku_grid():
   for i in range(9):
       for j in range(9):
           num, is_initial = sudoku_grid[j][i]
           pygame.draw.rect(screen, COLOR_WHITE, (i * cell_width, j * cell_width, cell_width, cell_width))
           if num != 0:
               text_color = COLOR_BLUE if is_initial else COLOR_RED
               text_value = font_user_input.render(str(num), True, text_color)
               screen.blit(text_value, (i * cell_width + 15, j * cell_width + 15))
   for i in range(10):
       thick = 7 if i % 3 == 0 else 1
       pygame.draw.line(screen, COLOR_BLACK, (0, i * cell_width), (500, i * cell_width), thick)
       pygame.draw.line(screen, COLOR_BLACK, (i * cell_width, 0), (i * cell_width, 500), thick)


def display_error_message(message):
   global error_message, display_error_until
   error_message = message
   display_error_until = pygame.time.get_ticks() + 2000  # Display the message for 2 seconds


def check_error_message():
   global error_message
   if pygame.time.get_ticks() > display_error_until:
       error_message = ""


def is_valid_move(grid, i, j, val):
   val = int(val)
   for it in range(9):
       if grid[i][it][0] == val or grid[it][j][0] == val:
           return False
   subgrid_x, subgrid_y = 3 * (i // 3), 3 * (j // 3)
   for i in range(subgrid_x, subgrid_x + 3):
       for j in range(subgrid_y, subgrid_y + 3):
           if grid[i][j][0] == val:
               return False
   return True


def check_puzzle_solved(grid):
   for row in grid:
       if sorted(num for num, _ in row) != list(range(1, 10)):
           return False
   for col in range(9):
       if sorted(grid[row][col][0] for row in range(9)) != list(range(1, 10)):
           return False
   for x in range(0, 9, 3):
       for y in range(0, 9, 3):
           subgrid = [grid[y + i][x + j][0] for i in range(3) for j in range(3)]
           if sorted(subgrid) != list(range(1, 10)):
               return False
   return True


def display_instructions():
   instruction_text = font_instructions.render("PRESS R TO RESET / N FOR NEW GAME", True, COLOR_BLACK)
   screen.blit(instruction_text, (20, 520))


def generate_sudoku_puzzle():
   global sudoku_grid, initial_sudoku_grid
   sudoku_grid = [[(0, False) for _ in range(9)] for _ in range(9)]
   for _ in range(30):
       i, j = random.randint(0, 8), random.randint(0, 8)
       num = random.randint(1, 9)
       if is_valid_move(sudoku_grid, i, j, num):
           sudoku_grid[i][j] = (num, True)
   initial_sudoku_grid = [row[:] for row in sudoku_grid]


def reset_sudoku_puzzle():
   global sudoku_grid, initial_sudoku_grid
   sudoku_grid = [row[:] for row in initial_sudoku_grid]


generate_sudoku_puzzle()


# Main game loop
run = True
while run:
   screen.fill(COLOR_WHITE)
   for event in pygame.event.get():
       if event.type == pygame.QUIT:
           run = False
       if event.type == pygame.MOUSEBUTTONDOWN:
           pos = pygame.mouse.get_pos()
           get_coordinate(pos)
       if event.type == pygame.KEYDOWN:
           if pygame.K_1 <= event.key <= pygame.K_9:
               user_input_value = str(event.key - 48)  # Convert pygame key to string representation of integer
           elif event.key == pygame.K_r:
               reset_sudoku_puzzle()
           elif event.key == pygame.K_n:
               generate_sudoku_puzzle()
           elif event.key == pygame.K_UP:
               y_coordinate = (y_coordinate - 1) % 9
           elif event.key == pygame.K_DOWN:
               y_coordinate = (y_coordinate + 1) % 9
           elif event.key == pygame.K_LEFT:
               x_coordinate = (x_coordinate - 1) % 9
           elif event.key == pygame.K_RIGHT:
               x_coordinate = (x_coordinate + 1) % 9


   if user_input_value:
       num, is_initial = sudoku_grid[y_coordinate][x_coordinate]
       if not is_initial:
           if num == int(user_input_value):
               sudoku_grid[y_coordinate][x_coordinate] = (0, False)  # Clear the cell
           elif is_valid_move(sudoku_grid, y_coordinate, x_coordinate, user_input_value):
               sudoku_grid[y_coordinate][x_coordinate] = (int(user_input_value), False)  # Update with new input
               if check_puzzle_solved(sudoku_grid):
                   congratulations_message = "Congratulations! Puzzle Solved!"
           else:
               display_error_message("Invalid move!")
       user_input_value = ''


   draw_sudoku_grid()
   draw_selection_box()
   display_instructions()


   check_error_message()
   if error_message:
       error_text = font_instructions.render(error_message, True, COLOR_RED)
       screen.blit(error_text, (20, 550))
   if congratulations_message:
       congrats_text = font_congratulations.render(congratulations_message, True, COLOR_GREEN)
       text_rect = congrats_text.get_rect(center=(250, 300))
       screen.blit(congrats_text, text_rect)


   pygame.display.update()


pygame.quit()

Happy Coding!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top