通过本综合教程,学习如何使用 Pygame 在 Python 中创建自己的数独游戏。本指南涵盖安装、游戏逻辑、用户界面和计时器功能,是希望创建功能性和可扩展性数独益智游戏的爱好者的理想之选。
数独是一种经典的数字谜题,多年来一直吸引着谜题爱好者。在本教程中,我们将介绍使用 Python 创建数独游戏的过程。本指南结束时,您将拥有一个功能齐全的数独游戏,您可以玩这个游戏,甚至可以进一步扩展。
安装和设置
让我们先确保 Pygame 已安装在电脑上;前往终端,使用 pip
安装 pygame
模块。
$ pip install pygame
然后,为游戏创建一个目录,并在其中创建以下 .py 文件:settings.py
、main.py
、sudoku.py
、cell.py
、table.py
和 clock.py
。
让我们在 settings.py
中定义游戏变量和有用的外部函数:
# setting.py
from itertools import isliceWIDTH, 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 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 = screenself.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 displayif 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
, math
和 copy
:
# sudoku.py
import random
import math
import copyclass Sudoku:def __init__(self, N, E):self.N = Nself.E = E# compute square root of Nself.SRN = int(math.sqrt(N))self.table = [[0 for x in range(N)] for y in range(N)]self.answerable_table = Noneself._generate_table()def _generate_table(self):# fill the subgroups diagonally table/matricesself.fill_diagonal()# fill remaining empty subgroupsself.fill_remaining(0, self.SRN)# Remove random Key digits to make gameself.remove_digits()
该类有一个初始化方法(__init__()
),需要两个参数N
和E
,分别代表数独网格的大小和创建谜题时需要移除的单元格数。类属性包括 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 Falsereturn Truedef fill_cell(self, row, col):num = 0for 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):breakself.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 Falsereturn Truedef not_in_col(self, col, num):for row in range(self.N):if self.table[row][col] == num:return Falsereturn Truedef fill_remaining(self, row, col):# check if we have reached the end of the matrixif row == self.N - 1 and col == self.N:return True# move to the next row if we have reached the end of the current rowif col == self.N:row += 1col = 0# skip cells that are already filledif self.table[row][col] != 0:return self.fill_remaining(row, col + 1)# try filling the current cell with a valid valuefor num in range(1, self.N + 1):if self.safe_position(row, col, num):self.table[row][col] = numif self.fill_remaining(row, col + 1):return Trueself.table[row][col] = 0# no valid value was found, so backtrackreturn 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 copyself.answerable_table = copy.deepcopy(self.table)# removing random numbers to create the puzzle sheetwhile (count != 0):row = self.random_generator(self.N) - 1col = self.random_generator(self.N) - 1if (self.answerable_table[row][col] != 0):count -= 1self.answerable_table[row][col] = 0
remove_digits()
方法会从填满的网格中移除指定数量的随机数字来创建谜题。在移除数字之前,它还会创建一个网格副本(answerable_table
)。
def puzzle_table(self):return self.answerable_tabledef puzzle_answers(self):return self.tabledef 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 = 9E = (N * N) // 2sudoku = Sudoku(N, E)sudoku.print_sudoku()
最后 3 个方法负责返回并打印谜题和/或答案。puzzle_table()
返回答案表(去掉部分单元格的谜题)。puzzle_answers()
返回完整的数独表格。print_sudoku()
同时打印完整的数独网格和答案网格。
创建游戏表
在创建游戏网格之前,我们先创建表格单元。在 cell.py
中,创建函数 Cell()
:
# cell.py
import pygame
from settings import convert_listpygame.font.init()class Cell:def __init__(self, row, col, cell_size, value, is_correct_guess = None):self.row = rowself.col = colself.cell_size = cell_sizeself.width = self.cell_size[0]self.height = self.cell_size[1]self.abs_x = row * self.widthself.abs_y = col * self.heightself.value = valueself.is_correct_guess = is_correct_guessself.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()
类的属性包括:row
和 col
(单元格在表格中的位置)、cell_size
、width
和 height
、abs_x
和 abs_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 Clockfrom settings import WIDTH, HEIGHT, N_CELLS, CELL_SIZEpygame.font.init()class Table:def __init__(self, screen):self.screen = screenself.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.SRNself.table_cells = []self.num_choices = []self.clicked_cell = Noneself.clicked_num_below = Noneself.cell_to_empty = Noneself.making_move = Falseself.guess_mode = Trueself.lives = 3self.game_over = Falseself.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 tablefor 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 Falseself.table_cells.append(Cell(x, y, CELL_SIZE, cell_value, is_correct_guess))# generating number choicesfor 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 = 1while (i * CELL_SIZE[0]) < WIDTH:line_size = 2 if i % 3 > 0 else 4pygame.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 += 1def _draw_buttons(self):# adding delete button detailsdl_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 detailsgss_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 celldef _not_in_row(self, row, num):for cell in self.table_cells:if cell.row == row:if cell.value == num:return Falsereturn Truedef _not_in_col(self, col, num):for cell in self.table_cells:if cell.col == col:if cell.value == num:return Falsereturn Truedef _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 Falsereturn True# remove numbers in guess if number already guessed in the same row, col, subgroup correctlydef _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] = 0if 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] = 0for 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 clickedif 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 cellif clicked_cell.value == 0:self.clicked_cell = clicked_cellself.making_move = True# clicked unempty cell but with wrong number guesselif clicked_cell.value != 0 and clicked_cell.value != self.answers[y][x]:self.cell_to_empty = clicked_cell# getting number selectedelif 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 numberselif 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 = 0self.cell_to_empty = None# selecting modeselif 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 moveif self.clicked_num_below and self.clicked_cell != None and self.clicked_cell.value == 0:current_row = self.clicked_cell.rowcurrent_col = self.clicked_cell.colrowstart = self.clicked_cell.row - self.clicked_cell.row % self.SRNcolstart = self.clicked_cell.col - self.clicked_cell.col % self.SRNif self.guess_mode:# checking the vertical group, the horizontal group, and the subgroupif 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_belowelse:self.clicked_cell.value = self.clicked_num_below# if the player guess correctlyif self.clicked_num_below == self.answers[self.clicked_cell.col][self.clicked_cell.row]:self.clicked_cell.is_correct_guess = Trueself.clicked_cell.guesses = Noneself._remove_guessed_num(current_row, current_col, rowstart, colstart, self.clicked_num_below)# if guess is wrongelse:self.clicked_cell.is_correct_guess = Falseself.clicked_cell.guesses = [0 for x in range(9)]self.lives -= 1self.clicked_num_below = Noneself.making_move = Falseelse:self.clicked_num_below = None
handle_mouse_click(
) 方法根据鼠标在屏幕上的位置来处理鼠标点击。它会相应地更新游戏变量,如 clicked_cell
、clicked_num_below
和 cell_to_empty
。
def _puzzle_solved(self):check = Nonefor cell in self.table_cells:if cell.value == self.answers[cell.col][cell.row]:check = Trueelse:check = Falsebreakreturn 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 = Trueelse: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_SIZEpygame.font.init()class Clock:def __init__(self):self.start_time = Noneself.elapsed_time = 0self.font = pygame.font.SysFont("monospace", CELL_SIZE[0])self.message_color = pygame.Color("black")# Start the timerdef start_timer(self):self.start_time = time.time()# Update the timerdef update_timer(self):if self.start_time is not None:self.elapsed_time = time.time() - self.start_time# Display the timerdef 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 timerdef 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 游戏的理解的人来说,本教程是一个宝贵的资源。