使用 Python 和 Pygame 开发数独游戏

PythonBeginner
立即练习

介绍

在这个项目中,我们将使用 Python 和 Pygame 库创建一个数独游戏。该游戏将生成指定难度级别的数独网格,并让玩家通过在空白单元格中填入数字来解决谜题。游戏将提供诸如选择难度、突出显示所选单元格以及检查网格是否完成等功能。

👀 预览

数独游戏预览

🎯 任务

在这个项目中,你将学习:

  • 如何导入所需的库
  • 如何初始化 PyGame
  • 如何定义颜色
  • 如何设置游戏窗口的尺寸和标题
  • 如何创建游戏窗口
  • 如何加载字体
  • 如何生成数独网格
  • 如何使用回溯算法解决数独网格
  • 如何根据难度从网格中移除数字
  • 如何在游戏窗口上绘制数独网格
  • 如何检查网格是否已完全填满
  • 如何获取鼠标位置下的单元格坐标
  • 如何选择难度级别
  • 如何实现主游戏循环

🏆 成果

完成这个项目后,你将能够:

  • 在 Python 中使用 Pygame 库进行游戏开发
  • 生成指定难度级别的数独网格
  • 使用回溯算法解决数独网格
  • 在 Pygame 中处理鼠标和键盘事件
  • 在游戏窗口上绘制形状和文本
  • 在 Pygame 中实现主游戏循环
这是一个实验(Guided Lab),提供逐步指导来帮助你学习和实践。请仔细按照说明完成每个步骤,获得实际操作经验。根据历史数据,这是一个 中级 级别的实验,完成率为 62%。获得了学习者 100% 的好评率。

创建项目文件

首先,创建一个名为sudoku_game.py的文件,并在你喜欢的文本编辑器或集成开发环境(IDE)中打开它。

cd ~/project
touch sudoku_game.py
✨ 查看解决方案并练习

导入所需的库

在文件开头导入所需的库。对于这个游戏,我们需要pygamerandom库。

import pygame
import random

使用pip命令安装pygame库。

sudo pip install pygame
✨ 查看解决方案并练习

初始化 PyGame

初始化 PyGame 库以设置游戏窗口。

pygame.init()
✨ 查看解决方案并练习

定义颜色

定义游戏中要使用的颜色。我们将使用 RGB 格式来表示颜色。

BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GRAY = (200, 200, 200)
BLUE = (0, 0, 255)
LIGHT_BLUE = (100, 100, 255)
✨ 查看解决方案并练习

设置游戏窗口尺寸和标题

设置游戏窗口的尺寸,并为游戏设置一个标题。

WINDOW_SIZE = (550, 550)
CELL_SIZE = WINDOW_SIZE[0] // 9

pygame.display.set_caption("Sudoku Game")
✨ 查看解决方案并练习

创建游戏窗口

使用指定的尺寸创建游戏窗口。

screen = pygame.display.set_mode(WINDOW_SIZE)
✨ 查看解决方案并练习

加载字体

我们需要加载字体以便在游戏屏幕上显示数字。加载两种字体,一种用于较大的数字,一种用于较小的数字。

font_large = pygame.font.SysFont("calibri", 50)
font_small = pygame.font.SysFont("calibri", 30)
✨ 查看解决方案并练习

生成数独网格

创建一个函数 generate_sudoku(difficulty),该函数生成指定难度级别的新数独网格。函数应返回生成的网格。

def generate_sudoku(difficulty):
    ## 函数代码在此处
    pass
✨ 查看解决方案并练习

填充对角子网格

generate_sudoku 函数内部,用随机数填充网格的对角子网格。这可确保每个子网格都包含 1 到 9 的数字且无重复。

## 填充对角子网格
for i in range(0, 9, 3):
    nums = random.sample(range(1, 10), 3)
    for j in range(3):
        grid[i + j][i + j] = nums[j]
✨ 查看解决方案并练习

求解数独网格

创建一个函数 solve_sudoku(grid),该函数使用回溯算法解决数独网格。如果网格可解,函数应返回 True,否则返回 False

def solve_sudoku(grid):
    ## 函数代码在此处
    pass
✨ 查看解决方案并练习

查找空白单元格

solve_sudoku 函数内部,创建一个辅助函数 find_empty_cell(grid),该函数返回网格中下一个空白单元格的坐标。如果没有空白单元格,则返回 None

def find_empty_cell(grid):
    for row in range(9):
        for col in range(9):
            if grid[row][col] == 0:
                return (row, col)

    return None
✨ 查看解决方案并练习

检查有效移动

solve_sudoku 函数内部,创建一个辅助函数 is_valid_move(grid, row, col, num),该函数检查在一个单元格中放置一个数字是否是一个有效的移动。如果移动有效,该函数应返回 True,否则返回 False

def is_valid_move(grid, row, col, num):
    ## 函数代码在此处
    pass
✨ 查看解决方案并练习

求解数独网格(续)

solve_sudoku 函数内部,使用辅助函数 find_empty_cellis_valid_move 来实现回溯算法。如果找到解决方案,返回 True。如果当前数字导致无效的解决方案,则通过将当前单元格设置为 0 来回溯。

## 尝试用 1 到 9 的数字填充空白单元格
for num in range(1, 10):
    if is_valid_move(grid, row, col, num):
        grid[row][col] = num

        if solve_sudoku(grid):
            return True

        ## 如果当前数字导致无效的解决方案,回溯
        grid[row][col] = 0

return False
✨ 查看解决方案并练习

根据难度移除数字

generate_sudoku 函数内部,根据指定的难度级别从网格中移除数字。难度级别为:1(简单)、2(中等)、3(困难)。要移除的数字数量计算为 num_to_remove = 45 + 10 * difficulty

## 根据难度级别移除数字
num_to_remove = 45 + 10 * difficulty
for _ in range(num_to_remove):
    row = random.randint(0, 8)
    col = random.randint(0, 8)
    grid[row][col] = 0

return grid
✨ 查看解决方案并练习

绘制网格

创建一个函数 draw_grid(grid, selected_cell),该函数在游戏窗口上绘制数独网格。此函数应负责绘制单元格、数字以及突出显示所选单元格。

def draw_grid(grid, selected_cell):
    ## 函数代码在此处
    pass
✨ 查看解决方案并练习

填充单元格并绘制数字

draw_grid 函数内部,遍历网格,并使用 Pygame 函数 pygame.draw.rectscreen.blit 来绘制单元格和数字。

## 绘制单元格
for row in range(9):
    for col in range(9):
        cell_rect = pygame.Rect(
            col * CELL_SIZE, row * CELL_SIZE, CELL_SIZE, CELL_SIZE
        )
        pygame.draw.rect(screen, GRAY, cell_rect)

        ## 绘制数字
        if grid[row][col]!= 0:
            number = font_small.render(str(grid[row][col]), True, BLACK)
            text_rect = number.get_rect(
                center=(
                    col * CELL_SIZE + CELL_SIZE // 2,
                    row * CELL_SIZE + CELL_SIZE // 2,
                )
            )
            screen.blit(number, text_rect)
✨ 查看解决方案并练习

突出显示所选单元格

draw_grid 函数内部,如果所选单元格不为 None,则使用 pygame.draw.rect 函数突出显示它。

## 突出显示所选单元格
if (row, col) == selected_cell:
    pygame.draw.rect(screen, LIGHT_BLUE, cell_rect, 3)
✨ 查看解决方案并练习

绘制网格线

draw_grid 函数内部,使用 pygame.draw.line 函数绘制线条以创建网格。

## 绘制线条
for i in range(10):
    if i % 3 == 0:
        thickness = 4
    else:
        thickness = 1

    pygame.draw.line(
        screen,
        BLACK,
        (0, i * CELL_SIZE),
        (WINDOW_SIZE[0], i * CELL_SIZE),
        thickness,
    )
    pygame.draw.line(
        screen,
        BLACK,
        (i * CELL_SIZE, 0),
        (i * CELL_SIZE, WINDOW_SIZE[1]),
        thickness,
    )
✨ 查看解决方案并练习

更新显示

draw_grid 函数内部,使用 pygame.display.update 函数更新显示。

pygame.display.update()
✨ 查看解决方案并练习

检查网格是否已满

创建一个函数 is_grid_full(grid),用于检查数独网格是否已完全填满。这可以通过遍历网格并检查是否有任何单元格的值为 0 来实现。

def is_grid_full(grid):
    ## 函数代码写在这里
    pass
✨ 查看解决方案并练习

获取鼠标下方的单元格

创建一个函数 get_cell_under_mouse(pos),该函数返回鼠标位置下方的单元格坐标。这可以通过将鼠标位置除以单元格大小来计算。

def get_cell_under_mouse(pos):
    ## 函数代码写在这里
    pass
✨ 查看解决方案并练习

选择难度

创建一个函数 select_difficulty(),该函数允许玩家在开始游戏前选择难度级别。此函数应显示难度选项并返回所选的难度级别。

def select_difficulty():
    ## 函数代码写在这里
    pass
✨ 查看解决方案并练习

实现难度选择

select_difficulty 函数内部,使用 Pygame 函数 pygame.mouse.get_pospygame.event.get 来实现难度选择逻辑。

## 显示难度选择文本
title_text = font_large.render("Select Difficulty", True, BLACK)
screen.blit(title_text, (110, 200))

## 显示难度选项
option_y = 300
for difficulty, label in difficulties.items():
    option_text = font_small.render(f"{difficulty}. {label}", True, BLACK)
    text_rect = option_text.get_rect(center=(WINDOW_SIZE[0] // 2, option_y))
    screen.blit(option_text, text_rect)
    option_y += 70

pygame.display.update()

## 等待难度选择
difficulty_selected = False
difficulty = 1

while not difficulty_selected:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            return
        elif event.type == pygame.MOUSEBUTTONDOWN:
            if event.button == 1:  ## 鼠标左键
                pos = pygame.mouse.get_pos()
                if 180 <= pos[0] <= 380:
                    if 300 <= pos[1] <= 350:
                        difficulty = 1
                        difficulty_selected = True
                    elif 370 <= pos[1] <= 420:
                        difficulty = 2
                        difficulty_selected = True
                    elif 440 <= pos[1] <= 490:
                        difficulty = 3
                        difficulty_selected = True

return difficulty
✨ 查看解决方案并练习

主游戏循环

通过定义一个函数 main() 来创建主游戏循环。

def main():
    ## 函数代码写在这里
    pass
✨ 查看解决方案并练习

选择难度并生成网格

main 函数内部,通过调用 select_difficulty() 来选择难度级别,如果难度为 None 则返回。然后,通过调用 generate_sudoku(difficulty) 使用所选难度级别生成一个数独网格。

## 选择难度级别
difficulty = select_difficulty()
if difficulty is None:
    return

## 生成一个新的数独网格
grid = generate_sudoku(difficulty)
✨ 查看解决方案并练习

游戏循环变量

main 函数内部,创建游戏循环变量 selected_cellrunning。将 selected_cell 设置为 None,将 running 设置为 True

selected_cell = None
running = True
✨ 查看解决方案并练习

启用按键重复

main 函数内部,使用 pygame.key.set_repeat 函数启用按键重复。我们将延迟和间隔设置为 100 毫秒,以便玩家更轻松地填写单元格。

## 启用按键重复
pygame.key.set_repeat(100, 100)
✨ 查看解决方案并练习

处理事件

在游戏循环中,使用 pygame.event.get 函数处理事件。检查退出事件、鼠标按钮事件和键盘事件。

for event in pygame.event.get():
    if event.type == pygame.QUIT:
        running = False
    elif event.type == pygame.MOUSEBUTTONDOWN:
        ## 处理鼠标按钮事件
        pass
    elif event.type == pygame.KEYDOWN:
        ## 处理键盘事件
        pass
✨ 查看解决方案并练习

处理鼠标按钮事件

在鼠标按钮事件的事件处理中,检查是否点击了鼠标左键或右键。

if event.button == 1:  ## 鼠标左键
    ## 处理鼠标左键点击
    pass
elif event.button == 3:  ## 鼠标右键
    ## 处理鼠标右键点击
    pass
✨ 查看解决方案并练习

处理鼠标左键点击

在鼠标左键点击处理中,通过调用 get_cell_under_mouse(pos) 获取鼠标位置下方的单元格坐标。

## 获取鼠标位置下方的单元格
pos = pygame.mouse.get_pos()
row, col = get_cell_under_mouse(pos)
✨ 查看解决方案并练习

处理鼠标左键点击(续)

在鼠标左键点击处理中,如果单元格为空(通过检查 grid[row][col] == 0),则选择该单元格。

## 如果单元格为空,则选择它
if grid[row][col] == 0:
    selected_cell = (row, col)
✨ 查看解决方案并练习

处理鼠标右键点击

在鼠标右键点击处理中,如果所选单元格不为 None 且为空(通过检查 grid[selected_cell[0]][selected_cell[1]] == 0),则清除该单元格。

if selected_cell:
    ## 如果所选单元格为空,则清除它
    if grid[selected_cell[0]][selected_cell[1]] == 0:
        grid[selected_cell[0]][selected_cell[1]] = 0
✨ 查看解决方案并练习

处理按键事件

在按键事件的事件处理中,检查按下了哪个数字键,并相应地设置所选单元格的值。

if selected_cell:
    ## 获取按下的键
    if event.key == pygame.K_1:
        grid[selected_cell[0]][selected_cell[1]] = 1
    elif event.key == pygame.K_2:
        grid[selected_cell[0]][selected_cell[1]] = 2
    elif event.key == pygame.K_3:
        grid[selected_cell[0]][selected_cell[1]] = 3
    elif event.key == pygame.K_4:
        grid[selected_cell[0]][selected_cell[1]] = 4
    elif event.key == pygame.K_5:
        grid[selected_cell[0]][selected_cell[1]] = 5
    elif event.key == pygame.K_6:
        grid[selected_cell[0]][selected_cell[1]] = 6
    elif event.key == pygame.K_7:
        grid[selected_cell[0]][selected_cell[1]] = 7
    elif event.key == pygame.K_8:
        grid[selected_cell[0]][selected_cell[1]] = 8
    elif event.key == pygame.K_9:
        grid[selected_cell[0]][selected_cell[1]] = 9
✨ 查看解决方案并练习

绘制网格并检查完成情况

在游戏循环中,通过调用 draw_grid(grid, selected_cell) 来绘制网格。然后,通过调用 is_grid_full(grid) 检查网格是否已填满。

## 绘制网格
draw_grid(grid, selected_cell)

## 检查网格是否已填满
if is_grid_full(grid):
    print("Congratulations! You solved the Sudoku puzzle.")
✨ 查看解决方案并练习

更新显示并退出

在游戏循环之后,使用 pygame.display.update 函数更新显示,并使用 pygame.quit() 退出游戏。

## 更新显示
pygame.display.update()

## 退出游戏
pygame.quit()
✨ 查看解决方案并练习

运行游戏

最后,添加一个条件来检查当前文件是否为程序的主入口点。如果是,则通过调用 main() 函数来运行游戏。

if __name__ == "__main__":
    main()

使用 python 命令运行游戏。

python sudoku_game.py
数独游戏执行截图
✨ 查看解决方案并练习

总结

恭喜你!你已经成功地使用 Python 和 Pygame 库创建了一个数独游戏。这个游戏允许玩家选择难度级别,用数字填充空白单元格,并检查网格是否完成。享受玩数独游戏和解谜的乐趣吧!