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!