通过本综合教程,学习如何使用 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 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
, math
和 copy
:
# 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__()
),需要两个参数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 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()
类的属性包括: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 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_cell
、clicked_num_below
和 cell_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 游戏的理解的人来说,本教程是一个宝贵的资源。