Movatterモバイル変換


[0]ホーム

URL:


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

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.

Table of Contents

Installation and Setup

Let's start by making sure Pygame is installed on your computer; head to your terminal and installpygame module usingpip.

$ 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, 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.

Generating Sudoku Puzzle

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] = num

Thefill_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 False

Several 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] = 0

Theremove_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.

Creating the Game Table

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 cell

The_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] = 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

Thehandle_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 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.

Adding a Game 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 = None

Thestart_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:

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 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 Code
Sharing is caring!



Read Also


How to Make a Checkers Game with Pygame in Python
How to Make a Chess Game with Pygame in Python
How to Create a Space Invaders Game in Python

Comment panel

    Got 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!





    Ethical Hacking with Python EBook - Topic - Top


    Join 50,000+ Python Programmers & Enthusiasts like you!



    Tags


    New Tutorials

    Popular Tutorials


    Ethical Hacking with Python EBook - Topic - Bottom

    CodingFleet - Topic - Bottom






    Claim your Free Chapter!

    Download a Completely Free Ethical hacking with Python from Scratch Chapter.

    See how the book can help you build awesome hacking tools with Python!



    [8]ページ先頭

    ©2009-2025 Movatter.jp