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!
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
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
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")
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 = ''
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 = []
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
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
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 = ""
Now, let’s define the heart of our game:
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)
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)
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)
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
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 = ""
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
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
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))
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]
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()
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_1Example
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