【Dison夏令营 Day 12】如何用 Python 构建数独游戏

发布于:2024-07-08 ⋅ 阅读:(36) ⋅ 点赞:(0)

通过本综合教程,学习如何使用 Pygame 在 Python 中创建自己的数独游戏。本指南涵盖安装、游戏逻辑、用户界面和计时器功能,是希望创建功能性和可扩展性数独益智游戏的爱好者的理想之选。

数独是一种经典的数字谜题,多年来一直吸引着谜题爱好者。在本教程中,我们将介绍使用 Python 创建数独游戏的过程。本指南结束时,您将拥有一个功能齐全的数独游戏,您可以玩这个游戏,甚至可以进一步扩展。

安装和设置

让我们先确保 Pygame 已安装在电脑上;前往终端,使用 pip 安装 pygame 模块。

$ pip install pygame

然后,为游戏创建一个目录,并在其中创建以下 .py 文件:settings.pymain.pysudoku.pycell.pytable.pyclock.py

在这里插入图片描述

让我们在 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]

接下来,让我们创建游戏的主类。该类将负责调用游戏和运行游戏循环:

# 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()

从名称本身来看,Main 类将是我们游戏的主类。它的参数 screen 将作为游戏窗口,用于制作游戏动画。

main() 函数将运行并更新我们的游戏。它将首先初始化 Table(作为谜题表)。为了保持游戏运行而不故意退出,我们在其中设置了一个 while 循环。在循环内部,我们还将设置另一个循环(for 循环),它将捕捉游戏窗口内发生的所有事件,如按键、鼠标移动、鼠标按键点击或玩家点击退出键等事件。

main() 还负责显示玩家的 "剩余生命 "和游戏结束信息,无论玩家是赢还是输。为了更新游戏,我们调用 table.update() 来更新游戏表中的变化。然后,pygame.display.flip() 会呈现这些变化。self.FPS.tick(30) 控制帧频更新速度。

生成数独谜题

sudoku() 类将负责为我们随机生成数独谜题。在 sudoku.py 中创建一个类并命名为 Sudoku。首先导入必要的模块:random, mathcopy

# 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()

该类有一个初始化方法(__init__()),需要两个参数NE,分别代表数独网格的大小和创建谜题时需要移除的单元格数。类属性包括 N(网格大小)、E(需要删除的单元格数)、SRN(N 的平方根)、table(数独网格)和 answerable_table(删除部分单元格后的网格副本)。在创建对象时,会立即调用 _generate_table() 方法来设置数独谜题。

主要数字填充:

    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

fill_diagonal() 方法通过调用每个子组的 fill_cell() 方法对角填充子组。fill_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

定义了几个辅助方法(random_generator()safe_position()not_in_row()not_in_col()not_in_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

remove_digits() 方法会从填满的网格中移除指定数量的随机数字来创建谜题。在移除数字之前,它还会创建一个网格副本(answerable_table)。

    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()

最后 3 个方法负责返回并打印谜题和/或答案。puzzle_table() 返回答案表(去掉部分单元格的谜题)。puzzle_answers() 返回完整的数独表格。print_sudoku() 同时打印完整的数独网格和答案网格。

创建游戏表

在创建游戏网格之前,我们先创建表格单元。在 cell.py 中,创建函数 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)

Cell() 类的属性包括:rowcol(单元格在表格中的位置)、cell_sizewidthheightabs_xabs_y(单元格在屏幕上的绝对 x 坐标和 y 坐标)、value(数值,空单元格为 0)、is_correct_guess(表示当前值是否为正确的猜测值)和 guesses(列表,表示空单元格的可能猜测值,如果单元格已填充,则表示无)。

update() 方法负责更新屏幕上单元格的图形表示。它使用 pygame.draw.rect 绘制一个指定颜色的矩形。根据单元格是填充的(value != 0)还是空的(value ==0),它要么在填充的单元格中绘制数值,要么在空的单元格中绘制可能的猜测。

如果单元格为空并且有可能的猜测,则使用 convert_list() 函数将猜测列表转换为二维列表。然后遍历转换后的列表,并在单元格的相应位置绘制每个猜测。它会使用小字体 (g_font) 将每个猜测渲染为文本。根据二维列表中的位置,计算每个猜测在单元格中的绝对位置。然后,在计算出的位置将文本显示(绘制)到屏幕上。

现在,让我们继续创建游戏表格。在 table.py 中创建一个类并命名为 Table。它使用 Pygame 库创建数独网格,处理用户输入,并显示谜题、数字选择、按钮和计时器。

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))

Table 类的 __init__() 方法(构造函数)初始化了各种属性,如 Pygame 屏幕、数独谜题、时钟、答案、可回答的表格以及其他与游戏相关的变量。

    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)))

_draw_grid() 方法负责绘制数独网格;它使用 Pygame 函数根据单元格的大小绘制网格线。_draw_buttons() 方法负责绘制删除和猜测按钮;它使用 Pygame 函数绘制带有适当颜色和信息的矩形按钮。

    def _get_cell_from_pos(self, pos):
        for cell in self.table_cells:
            if (cell.row, cell.col) == (pos[0], pos[1]):
                return cell

_get_cell_from_pos() 方法返回数独表中给定位置(行、列)上的单元格对象。

    # 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

方法 _not_in_row()_not_in_col()_not_in_subgroup()_remove_guessed_num() 负责检查数字在行、列或子群中是否有效,并在正确放置后删除猜测的数字。

    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

handle_mouse_click() 方法根据鼠标在屏幕上的位置来处理鼠标点击。它会相应地更新游戏变量,如 clicked_cellclicked_num_belowcell_to_empty

    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

_puzzle_solved() 方法通过比较每个单元格中的值与正确答案,检查数独谜题是否已解。

    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]))

update 方法负责更新显示内容。它更新单元格和数字的图形表示,绘制网格和按钮,检查谜题是否已解开或游戏是否已结束,以及更新计时器。

添加游戏计时器

在代码的最后一部分,我们要为计时器创建一个类。在 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

start_timer() 方法在调用时使用 time.time() 将 start_time 属性设置为当前时间。这标志着计时器的开始。

update_timer() 方法计算定时器开始后的耗时。如果 start_time 属性不是 None,则用 start_time 减去当前时间来更新 elapsed_time

display_timer() 方法会将已用时间转换为分钟和秒。然后使用 Pygame 字体以 "MM:SS "格式创建时间的文本表示。渲染后的文本将被返回。

stop_timer() 方法将 start_time 重置为 None,从而有效地停止计时器。

现在,我们的编码工作完成了要体验我们的游戏,只需在进入项目目录后在终端运行 python main.py 或 python3 main.py。下面是一些游戏快照:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

结论

最后,本教程概述了使用 Pygame 库在 Python 中开发数独游戏的过程。实现过程涵盖了数独谜题生成、图形表示、用户交互和计时器功能等关键方面。通过将代码分解为模块化类(如数独、单元格、表格和时钟),本教程强调了一种结构化和有组织的游戏开发方法。对于那些希望创建自己的数独游戏或加深对使用 Pygame 开发 Python 游戏的理解的人来说,本教程是一个宝贵的资源。


网站公告

今日签到

点亮在社区的每一天
去签到