Juggling between coding languages? Let our Code Converter help. Your one-stop solution for language conversion. Start 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.
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.
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.answerable_table = None
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
self.answerable_table = copy.deepcopy(self.table)
# 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
if (self.answerable_table[row][col] != 0):
count -= 1
self.answerable_table[row][col] = 0
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.answerable_table
def puzzle_answers(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(self.answerable_table[row][col], end=" ")
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.
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.answers = self.puzzle.puzzle_answers()
self.answerable_table = self.puzzle.puzzle_table()
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):
cell_value = self.answerable_table[y][x]
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):
# adding delete button details
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)))
# adding guess button details
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
if self.clicked_num_below == self.answers[self.clicked_cell.col][self.clicked_cell.row]:
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:
if cell.value == self.answers[cell.col][cell.row]:
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:
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!
Liked what you read? You'll love what you can learn from our AI-powered Code Explainer. Check it out!
View Full Code Convert My Code
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!