Unlock the secrets of your code with ourAI-powered Code Explainer. Take a look!
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 installpygame module usingpip.
$ pip install pygameAfter 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, andclock.py.

Let's define our game variables and useful external functions insettings.py:
# setting.pyfrom itertools import isliceWIDTH, HEIGHT = 450, 450N_CELLS = 9CELL_SIZE = (WIDTH // N_CELLS, HEIGHT // N_CELLS)# Convert 1D list to 2D listdef 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.pyimport pygame, sysfrom settings import WIDTH, HEIGHT, CELL_SIZEfrom table import Tablepygame.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, theMain class will be the main class of our game. It takes an argument ofscreen which will serve as the game window, for animating the game.
Themain() function will run and update our game. It will initialize theTable first (serves as our puzzle table). To keep the game running without intentionally exiting, we put awhile loop inside it. Inside our loop, we place another loop (thefor 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.
Themain() is also responsible for displaying players' "lives left" and game-over messages, whether the player wins or loses. To update the game, we call thetable.update() to update the changes in our game table. Then, to render the changes, thepygame.display.flip() does the job done. Theself.FPS.tick(30) controls the framerate update speed.
TheSudoku() class will be responsible for generating a random Sudoku puzzle for us. Insudoku.py, create a class and name itSudoku. Let's begin by importing the necessary modules:random,math, andcopy:
# sudoku.pyimport randomimport mathimport copyclass 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 parametersN andE, representing the size of the Sudoku grid and the number of cells to be removed to create a puzzle. The class attributes includeN (grid size),E (number of cells to remove),SRN (square root of N),table (Sudoku grid), andanswerable_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] = numThefill_diagonal() method fills subgroups diagonally by calling thefill_cell() method for each subgroup. Thefill_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 FalseSeveral helper methods (random_generator(),safe_position(),not_in_row(),not_in_col(), andnot_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] = 0Theremove_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. Thepuzzle_table() returns the answerable table (puzzle with some cells removed). Thepuzzle_answers() returns the complete Sudoku table. Theprint_sudoku() prints both the complete Sudoku grid and the answerable grid.
Before making the game grid, let's create our table cells. Incell.py, make the functionCell():
# cell.pyimport pygamefrom settings import convert_listpygame.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)TheCell() class has attributes such asrow andcol (cell position in the table),cell_size,width andheight,abs_x andabs_y (absolute x and y coordinates of the cell on the screen), value (numericalvalue, 0 for an empty cell),is_correct_guess (indicating whether the current value is a correct guess), andguesses (list representing possible guesses for an empty cell orNone if the cell is filled)
Theupdate() method is responsible for updating the graphical representation of the cell on the screen. It draws a rectangle with the specified color usingpygame.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 theconvert_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 itTable intable.py. It uses the Pygame library to create the Sudoku grid, handle user inputs, and display the puzzle, number choices, buttons, and timer.
import pygameimport mathfrom cell import Cellfrom sudoku import Sudokufrom clock import Clockfrom settings import WIDTH, HEIGHT, N_CELLS, CELL_SIZEpygame.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))TheTable 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 cellThe_get_cell_from_pos() method returns theCell 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] = 0The 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 = NoneThehandle_mouse_click() method processes mouse clicks based on the position on the screen. It updates game variables likeclicked_cell,clicked_num_below, andcell_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 checkThe_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. CreateClock class inclock.py:
import pygame, timefrom settings import CELL_SIZEpygame.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 = NoneThestart_timer() method sets thestart_time attribute to the current time usingtime.time() when called. This marks the beginning of the timer.
Theupdate_timer() method calculates the elapsed time since the timer started. If thestart_time is notNone, it updates theelapsed_time by subtracting the current time from thestart_time.
Thedisplay_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.
Thestop_timer() method resets thestart_time toNone, effectively stopping the timer.
And now, we are done coding!! To try our game, simply runpython main.py orpython3 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 asSudoku,Cell,Table, andClock, 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 somegame dev tutorials:
Happy Coding!
Liked what you read? You'll love what you can learn from ourAI-powered Code Explainer. Check it out!
View Full Code Transform My CodeGot a coding query or need some guidance before you comment? Check out thisPython Code Assistant for expert advice and handy tips. It's like having a coding tutor right in your fingertips!
