# How to Build a Sudoku Game with Python

Learn how to build your own Sudoku game in Python using Pygame with this comprehensive tutorial. Covering installation, game logic, user interface, and a timer feature, this guide is perfect for enthusiasts looking to create a functional and extendable Sudoku puzzle game.
16 min read · Updated jan 2024 · Game Development

Want to code faster? Our Python Code Generator lets you create Python scripts with just a few clicks. Try it now!

Sudoku, a classic number puzzle, has captivated the minds of puzzle enthusiasts for years. In this tutorial, we'll walk through the process of creating a Sudoku game using Python. By the end of this guide, you'll have a fully functional Sudoku game that you can play and even extend further.

## Installation and Setup

Let's start by making sure Pygame is installed on your computer; head to your terminal and install `pygame` module using `pip`.

``\$ pip install pygame``

After that, create a directory for the game and create the following `.py` file inside it; `settings.py`, `main.py`, `sudoku.py`, `cell.py`, `table.py`, and `clock.py`.

Let's define our game variables and useful external functions in `settings.py`:

``````# setting.py
from itertools import islice

WIDTH, HEIGHT = 450, 450
N_CELLS = 9
CELL_SIZE = (WIDTH // N_CELLS, HEIGHT // N_CELLS)

# Convert 1D list to 2D list
def convert_list(lst, var_lst):
it = iter(lst)
return [list(islice(it, i)) for i in var_lst]``````

Next, let's create the main class of our game. This class will be responsible for calling the game and running the game loop:

``````# main.py
import pygame, sys
from settings import WIDTH, HEIGHT, CELL_SIZE
from table import Table

pygame.init()

screen = pygame.display.set_mode((WIDTH, HEIGHT + (CELL_SIZE[1] * 3)))
pygame.display.set_caption("Sudoku")

pygame.font.init()

class Main:
def __init__(self, screen):
self.screen = screen
self.FPS = pygame.time.Clock()
self.lives_font = pygame.font.SysFont("monospace", CELL_SIZE[0] // 2)
self.message_font = pygame.font.SysFont('Bauhaus 93', (CELL_SIZE[0]))
self.color = pygame.Color("darkgreen")

def main(self):
table = Table(self.screen)
while True:
self.screen.fill("gray")
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if event.type == pygame.MOUSEBUTTONDOWN:
if not table.game_over:
table.handle_mouse_click(event.pos)
# lower screen display
if not table.game_over:
my_lives = self.lives_font.render(f"Lives Left: {table.lives}", True, pygame.Color("black"))
self.screen.blit(my_lives, ((WIDTH // table.SRN) - (CELL_SIZE[0] // 2), HEIGHT + (CELL_SIZE[1] * 2.2)))
else:
if table.lives <= 0:
message = self.message_font.render("GAME OVER!!", True, pygame.Color("red"))
self.screen.blit(message, (CELL_SIZE[0] + (CELL_SIZE[0] // 2), HEIGHT + (CELL_SIZE[1] * 2)))
elif table.lives > 0:
message = self.message_font.render("You Made It!!!", True, self.color)
self.screen.blit(message, (CELL_SIZE[0] , HEIGHT + (CELL_SIZE[1] * 2)))
table.update()
pygame.display.flip()
self.FPS.tick(30)

if __name__ == "__main__":
play = Main(screen)
play.main()``````

From the name itself, the `Main` class will be the main class of our game. It takes an argument of `screen` which will serve as the game window, for animating the game.

The `main()` function will run and update our game. It will initialize the `Table` first (serves as our puzzle table). To keep the game running without intentionally exiting, we put a `while` loop inside it. Inside our loop, we place another loop (the `for` loop), which will catch all the events going on inside our game window, events such as key click, mouse movement, mouse button click or when the player hits the exit button.

The `main()` is also responsible for displaying players' "lives left" and game-over messages, whether the player wins or loses. To update the game, we call the `table.update()` to update the changes in our game table. Then, to render the changes, the `pygame.display.flip()` does the job done. The `self.FPS.tick(30)` controls the framerate update speed.

## Generating Sudoku Puzzle

The `Sudoku()` class will be responsible for generating a random Sudoku puzzle for us. In `sudoku.py`, create a class and name it `Sudoku`. Let's begin by importing the necessary modules: `random`, `math`, and `copy`:

``````# sudoku.py
import random
import math
import copy

class Sudoku:
def __init__(self, N, E):
self.N = N
self.E = E
# compute square root of N
self.SRN = int(math.sqrt(N))
self.table = [[0 for x in range(N)] for y in range(N)]
self._generate_table()

def _generate_table(self):
# fill the subgroups diagonally table/matrices
self.fill_diagonal()
# fill remaining empty subgroups
self.fill_remaining(0, self.SRN)
# Remove random Key digits to make game
self.remove_digits()``````

The class has an initializer (`__init__()`) method that takes two parameters `N` and `E`, representing the size of the Sudoku grid and the number of cells to be removed to create a puzzle. The class attributes include `N` (grid size), `E` (number of cells to remove), `SRN` (square root of N), `table` (Sudoku grid), and `answerable_table` (a copy of the grid with some cells removed). The `_generate_table()` method is immediately called upon object creation to set up the Sudoku puzzle.

Primary number filling:

``````    def fill_diagonal(self):
for x in range(0, self.N, self.SRN):
self.fill_cell(x, x)

def not_in_subgroup(self, rowstart, colstart, num):
for x in range(self.SRN):
for y in range(self.SRN):
if self.table[rowstart + x][colstart + y] == num:
return False
return True

def fill_cell(self, row, col):
num = 0
for x in range(self.SRN):
for y in range(self.SRN):
while True:
num = self.random_generator(self.N)
if self.not_in_subgroup(row, col, num):
break
self.table[row + x][col + y] = num``````

The `fill_diagonal()` method fills subgroups diagonally by calling the `fill_cell()` method for each subgroup. The `fill_cell()` method generates and places a unique number in each subgroup cell.

``````    def random_generator(self, num):
return math.floor(random.random() * num + 1)

def safe_position(self, row, col, num):
return (self.not_in_row(row, num) and self.not_in_col(col, num) and self.not_in_subgroup(row - row % self.SRN, col - col % self.SRN, num))

def not_in_row(self, row, num):
for col in range(self.N):
if self.table[row][col] == num:
return False
return True

def not_in_col(self, col, num):
for row in range(self.N):
if self.table[row][col] == num:
return False
return True

def fill_remaining(self, row, col):
# check if we have reached the end of the matrix
if row == self.N - 1 and col == self.N:
return True
# move to the next row if we have reached the end of the current row
if col == self.N:
row += 1
col = 0
# skip cells that are already filled
if self.table[row][col] != 0:
return self.fill_remaining(row, col + 1)
# try filling the current cell with a valid value
for num in range(1, self.N + 1):
if self.safe_position(row, col, num):
self.table[row][col] = num
if self.fill_remaining(row, col + 1):
return True
self.table[row][col] = 0
# no valid value was found, so backtrack
return False``````

Several helper methods (`random_generator()`, `safe_position()`, `not_in_row()`, `not_in_col()`, and `not_in_subgroup()`) are defined. These methods assist in generating random numbers, checking if a position is safe to place a number, and ensuring that a number is not already present in a row, column, or subgroup.

``````    def remove_digits(self):
count = self.E
# replicates the table so we can have a filled and pre-filled copy
# removing random numbers to create the puzzle sheet
while (count != 0):
row = self.random_generator(self.N) - 1
col = self.random_generator(self.N) - 1
count -= 1

The `remove_digits()` method removes a specified number of random digits from the filled grid to create the puzzle. It also creates a copy of the grid (`answerable_table`) before removing digits.

``````    def puzzle_table(self):

return self.table

def print_sudoku(self):
for row in range(self.N):
for col in range(self.N):
print(self.table[row][col], end=" ")
print()
print("")
for row in range(self.N):
for col in range(self.N):
print()

if __name__ == "__main__":
N = 9
E = (N * N) // 2
sudoku = Sudoku(N, E)
sudoku.print_sudoku()``````

The last 3 methods are responsible for returning and printing the puzzle and/or answers. The `puzzle_table()` returns the answerable table (puzzle with some cells removed). The `puzzle_answers()` returns the complete Sudoku table. The `print_sudoku()` prints both the complete Sudoku grid and the answerable grid.

## Creating the Game Table

Before making the game grid, let's create our table cells. In `cell.py`, make the function `Cell()`:

``````# cell.py
import pygame
from settings import convert_list

pygame.font.init()

class Cell:
def __init__(self, row, col, cell_size, value, is_correct_guess = None):
self.row = row
self.col = col
self.cell_size = cell_size
self.width = self.cell_size[0]
self.height = self.cell_size[1]
self.abs_x = row * self.width
self.abs_y = col * self.height
self.value = value
self.is_correct_guess = is_correct_guess
self.guesses = None if self.value != 0 else [0 for x in range(9)]
self.color = pygame.Color("white")
self.font = pygame.font.SysFont('monospace', self.cell_size[0])
self.g_font = pygame.font.SysFont('monospace', (cell_size[0] // 3))
self.rect = pygame.Rect(self.abs_x,self.abs_y,self.width,self.height)

def update(self, screen, SRN = None):
pygame.draw.rect(screen, self.color, self.rect)
if self.value != 0:
font_color = pygame.Color("black") if self.is_correct_guess else pygame.Color("red")
num_val = self.font.render(str(self.value), True, font_color)
screen.blit(num_val, (self.abs_x, self.abs_y))
elif self.value == 0 and self.guesses != None:
cv_list = convert_list(self.guesses, [SRN, SRN, SRN])
for y in range(SRN):
for x in range(SRN):
num_txt = " "
if cv_list[y][x] != 0:
num_txt = cv_list[y][x]
num_txt = self.g_font.render(str(num_txt), True, pygame.Color("orange"))
abs_x = (self.abs_x + ((self.width // SRN) * x))
abs_y = (self.abs_y + ((self.height // SRN) * y))
abs_pos = (abs_x, abs_y)
screen.blit(num_txt, abs_pos)``````

The `Cell()` class has attributes such as `row` and `col` (cell position in the table), `cell_size`, `width` and `height`, `abs_x` and `abs_y` (absolute x and y coordinates of the cell on the screen), value (numerical `value`, 0 for an empty cell), `is_correct_guess` (indicating whether the current value is a correct guess), and `guesses` (list representing possible guesses for an empty cell or `None` if the cell is filled)

The `update()` method is responsible for updating the graphical representation of the cell on the screen. It draws a rectangle with the specified color using `pygame.draw.rect`. Depending on whether the cell is filled (value != 0) or empty (value == 0), it either draws the value in a filled cell or the possible guesses in an empty cell.

If the cell is empty and has possible guesses, it converts the guess list into a 2D list using the `convert_list()` function. It then iterates through the converted list and draws each guess in the corresponding position within the cell. It renders each guess as text using the small font (`g_font`). It calculates the absolute position within the cell for each guess based on the position within the 2D list. Then, blits (draws) the text onto the screen at the calculated position.

Now, let's move on to creating the game table. Create a class and name it `Table` in `table.py`. It uses the Pygame library to create the Sudoku grid, handle user inputs, and display the puzzle, number choices, buttons, and timer.

``````import pygame
import math
from cell import Cell
from sudoku import Sudoku
from clock import Clock

from settings import WIDTH, HEIGHT, N_CELLS, CELL_SIZE

pygame.font.init()

class Table:
def __init__(self, screen):
self.screen = screen
self.puzzle = Sudoku(N_CELLS, (N_CELLS * N_CELLS) // 2)
self.clock = Clock()
self.SRN = self.puzzle.SRN
self.table_cells = []
self.num_choices = []
self.clicked_cell = None
self.clicked_num_below = None
self.cell_to_empty = None
self.making_move = False
self.guess_mode = True
self.lives = 3
self.game_over = False
self.delete_button = pygame.Rect(0, (HEIGHT + CELL_SIZE[1]), (CELL_SIZE[0] * 3), (CELL_SIZE[1]))
self.guess_button = pygame.Rect((CELL_SIZE[0] * 6), (HEIGHT + CELL_SIZE[1]), (CELL_SIZE[0] * 3), (CELL_SIZE[1]))
self.font = pygame.font.SysFont('Bauhaus 93', (CELL_SIZE[0] // 2))
self.font_color = pygame.Color("white")
self._generate_game()
self.clock.start_timer()

def _generate_game(self):
# generating sudoku table
for y in range(N_CELLS):
for x in range(N_CELLS):
is_correct_guess = True if cell_value != 0 else False
self.table_cells.append(Cell(x, y, CELL_SIZE, cell_value, is_correct_guess))
# generating number choices
for x in range(N_CELLS):
self.num_choices.append(Cell(x, N_CELLS, CELL_SIZE, x + 1))``````

The `Table` class' `__init__()` method (constructor) initializes various attributes such as the Pygame screen, the Sudoku puzzle, the clock, answers, the answerable table, and other game-related variables.

``````    def _draw_grid(self):
grid_color = (50, 80, 80)
pygame.draw.rect(self.screen, grid_color, (-3, -3, WIDTH + 6, HEIGHT + 6), 6)
i = 1
while (i * CELL_SIZE[0]) < WIDTH:
line_size = 2 if i % 3 > 0 else 4
pygame.draw.line(self.screen, grid_color, ((i * CELL_SIZE[0]) - (line_size // 2), 0), ((i * CELL_SIZE[0]) - (line_size // 2), HEIGHT), line_size)
pygame.draw.line(self.screen, grid_color, (0, (i * CELL_SIZE[0]) - (line_size // 2)), (HEIGHT, (i * CELL_SIZE[0]) - (line_size // 2)), line_size)
i += 1

def _draw_buttons(self):
dl_button_color = pygame.Color("red")
pygame.draw.rect(self.screen, dl_button_color, self.delete_button)
del_msg = self.font.render("Delete", True, self.font_color)
self.screen.blit(del_msg, (self.delete_button.x + (CELL_SIZE[0] // 2), self.delete_button.y + (CELL_SIZE[1] // 4)))
gss_button_color = pygame.Color("blue") if self.guess_mode else pygame.Color("purple")
pygame.draw.rect(self.screen, gss_button_color, self.guess_button)
gss_msg = self.font.render("Guess: On" if self.guess_mode else "Guess: Off", True, self.font_color)
self.screen.blit(gss_msg, (self.guess_button.x + (CELL_SIZE[0] // 3), self.guess_button.y + (CELL_SIZE[1] // 4)))``````

The `_draw_grid()` method is responsible for drawing the Sudoku grid; it uses Pygame functions to draw the grid lines based on the size of the cells. The `_draw_buttons()` method is responsible for drawing the delete and guess buttons; it uses Pygame functions to draw rectangular buttons with appropriate colors and messages.

``````    def _get_cell_from_pos(self, pos):
for cell in self.table_cells:
if (cell.row, cell.col) == (pos[0], pos[1]):
return cell``````

The `_get_cell_from_pos()` method returns the `Cell` object at a given position (row, col) in the Sudoku table.

``````    # checking rows, cols, and subgroups for adding guesses on each cell
def _not_in_row(self, row, num):
for cell in self.table_cells:
if cell.row == row:
if cell.value == num:
return False
return True

def _not_in_col(self, col, num):
for cell in self.table_cells:
if cell.col == col:
if cell.value == num:
return False
return True

def _not_in_subgroup(self, rowstart, colstart, num):
for x in range(self.SRN):
for y in range(self.SRN):
current_cell = self._get_cell_from_pos((rowstart + x, colstart + y))
if current_cell.value == num:
return False
return True

# remove numbers in guess if number already guessed in the same row, col, subgroup correctly
def _remove_guessed_num(self, row, col, rowstart, colstart, num):
for cell in self.table_cells:
if cell.row == row and cell.guesses != None:
for x_idx,guess_row_val in enumerate(cell.guesses):
if guess_row_val == num:
cell.guesses[x_idx] = 0
if cell.col == col and cell.guesses != None:
for y_idx,guess_col_val in enumerate(cell.guesses):
if guess_col_val == num:
cell.guesses[y_idx] = 0
for x in range(self.SRN):
for y in range(self.SRN):
current_cell = self._get_cell_from_pos((rowstart + x, colstart + y))
if current_cell.guesses != None:
for idx,guess_val in enumerate(current_cell.guesses):
if guess_val == num:
current_cell.guesses[idx] = 0``````

The methods `_not_in_row()`, `_not_in_col()`, `_not_in_subgroup()`, and `_remove_guessed_num()` are responsible for checking whether a number is valid in a row, column, or subgroup and removing guessed numbers when correctly placed.

``````    def handle_mouse_click(self, pos):
x, y = pos[0], pos[1]
# getting table cell clicked
if x <= WIDTH and y <= HEIGHT:
x = x // CELL_SIZE[0]
y = y // CELL_SIZE[1]
clicked_cell = self._get_cell_from_pos((x, y))
# if clicked empty cell
if clicked_cell.value == 0:
self.clicked_cell = clicked_cell
self.making_move = True
# clicked unempty cell but with wrong number guess
elif clicked_cell.value != 0 and clicked_cell.value != self.answers[y][x]:
self.cell_to_empty = clicked_cell
# getting number selected
elif x <= WIDTH and y >= HEIGHT and y <= (HEIGHT + CELL_SIZE[1]):
x = x // CELL_SIZE[0]
self.clicked_num_below = self.num_choices[x].value
# deleting numbers
elif x <= (CELL_SIZE[0] * 3) and y >= (HEIGHT + CELL_SIZE[1]) and y <= (HEIGHT + CELL_SIZE[1] * 2):
if self.cell_to_empty:
self.cell_to_empty.value = 0
self.cell_to_empty = None
# selecting modes
elif x >= (CELL_SIZE[0] * 6) and y >= (HEIGHT + CELL_SIZE[1]) and y <= (HEIGHT + CELL_SIZE[1] * 2):
self.guess_mode = True if not self.guess_mode else False
# if making a move
if self.clicked_num_below and self.clicked_cell != None and self.clicked_cell.value == 0:
current_row = self.clicked_cell.row
current_col = self.clicked_cell.col
rowstart = self.clicked_cell.row - self.clicked_cell.row % self.SRN
colstart = self.clicked_cell.col - self.clicked_cell.col % self.SRN
if self.guess_mode:
# checking the vertical group, the horizontal group, and the subgroup
if self._not_in_row(current_row, self.clicked_num_below) and self._not_in_col(current_col, self.clicked_num_below):
if self._not_in_subgroup(rowstart, colstart, self.clicked_num_below):
if self.clicked_cell.guesses != None:
self.clicked_cell.guesses[self.clicked_num_below - 1] = self.clicked_num_below
else:
self.clicked_cell.value = self.clicked_num_below
# if the player guess correctly
self.clicked_cell.is_correct_guess = True
self.clicked_cell.guesses = None
self._remove_guessed_num(current_row, current_col, rowstart, colstart, self.clicked_num_below)
# if guess is wrong
else:
self.clicked_cell.is_correct_guess = False
self.clicked_cell.guesses = [0 for x in range(9)]
self.lives -= 1
self.clicked_num_below = None
self.making_move = False
else:
self.clicked_num_below = None``````

The `handle_mouse_click()` method processes mouse clicks based on the position on the screen. It updates game variables like `clicked_cell`, `clicked_num_below`, and `cell_to_empty` accordingly.

``````    def _puzzle_solved(self):
check = None
for cell in self.table_cells:
check = True
else:
check = False
break
return check``````

The `_puzzle_solved()` method checks if the Sudoku puzzle is solved by comparing the values in each cell with the correct answers.

``````    def update(self):
[cell.update(self.screen, self.SRN) for cell in self.table_cells]
[num.update(self.screen) for num in self.num_choices]
self._draw_grid()
self._draw_buttons()
if self._puzzle_solved() or self.lives == 0:
self.clock.stop_timer()
self.game_over = True
else:
self.clock.update_timer()
self.screen.blit(self.clock.display_timer(), (WIDTH // self.SRN,HEIGHT + CELL_SIZE[1]))``````

The update method is responsible for updating the display. It updates the graphical representation of cells and numbers, draws the grid and buttons, checks if the puzzle is solved or the game is over, and updates the timer.

And for the last part of our code, we're making a class for timer. Create `Clock` class in `clock.py`:

``````import pygame, time
from settings import CELL_SIZE

pygame.font.init()

class Clock:
def __init__(self):
self.start_time = None
self.elapsed_time = 0
self.font = pygame.font.SysFont("monospace", CELL_SIZE[0])
self.message_color = pygame.Color("black")

# Start the timer
def start_timer(self):
self.start_time = time.time()

# Update the timer
def update_timer(self):
if self.start_time is not None:
self.elapsed_time = time.time() - self.start_time

# Display the timer
def display_timer(self):
secs = int(self.elapsed_time % 60)
mins = int(self.elapsed_time / 60)
my_time = self.font.render(f"{mins:02}:{secs:02}", True, self.message_color)
return my_time

# Stop the timer
def stop_timer(self):
self.start_time = None``````

The `start_timer()` method sets the `start_time` attribute to the current time using `time.time()` when called. This marks the beginning of the timer.

The `update_timer()` method calculates the elapsed time since the timer started. If the `start_time` is not `None`, it updates the `elapsed_time` by subtracting the current time from the `start_time`.

The `display_timer()` method converts the elapsed time into minutes and seconds. It then creates a text representation of the time in the format "MM:SS" using the Pygame font. The rendered text is returned.

The `stop_timer()` method resets the `start_time` to `None`, effectively stopping the timer.

And now, we are done coding!! To try our game, simply run `python main.py` or `python3 main.py` on your terminal once you're inside our project directory. Here are some game snapshots:

Or a video of me playing the game:

## Conclusion

In conclusion, the tutorial outlines the development of a Sudoku game in Python using the Pygame library. The implementation covers key aspects, including Sudoku puzzle generation, graphical representation, user interaction, and a timer feature. By breaking down the code into modular classes, such as `Sudoku`, `Cell`, `Table`, and `Clock`, the tutorial emphasizes a structured and organized approach to game development. This tutorial is a valuable resource for those seeking to create their own Sudoku game or enhance their understanding of Python game development with Pygame.

Here are some game dev tutorials:

Happy Coding!

Let our Code Converter simplify your multi-language projects. It's like having a coding translator at your fingertips. Don't miss out!

Sharing is caring!

### Comment panel

Got a coding query or need some guidance before you comment? Check out this Python Code Assistant for expert advice and handy tips. It's like having a coding tutor right in your fingertips!