用Python做数独游戏:从零开始,新手也能看懂
一、准备工作:装好用得到的工具
1.1 我们需要哪些工具?
核心工具是Pygame,这是Python专门用来做游戏的第三方库,能帮我们画出游戏界面、处理鼠标点击、键盘输入这些交互操作。除此之外,还会用到Python自带的几个小工具:
- random:用来生成随机数,让每次玩的数独都不一样
- sys:帮我们处理游戏退出的操作
- time:实现计时功能,记录我们玩一局用了多久
这些自带的工具不用额外装,只有Pygame需要手动装一下。
1.2 怎么装环境?一步一步来
首先得确保你电脑上装了Python(建议装3.7及以上版本,网上搜“Python下载”就能找到官方安装包,跟着指引装就行)。装完Python后,按下面步骤装Pygame:
- 打开电脑的命令行(Windows按Win+R,输cmd回车;Mac打开终端)
- 在命令行里输入:pip install pygame,然后按回车
- 等一会儿,出现成功提示就装好了。可以验证一下:打开Python软件,输入import pygame,没弹出红色错误提示,就说明装对了
二、游戏功能规划:先做基础,再添高级功能
做游戏不用一步到位,我们先把核心的“能玩”功能做好,再慢慢加好看、好用的高级功能。这样思路清晰,不容易出错。
2.1 基础功能:先让数独能玩起来
- 生成数独:先做出一个填满正确数字的完整数独,再删掉一些数字做成空白的可玩棋盘
- 画棋盘:画出9×9的格子,用粗线标出3×3的小宫格,选中的格子要高亮显示,还要区分“一开始就有的提示数字”和“我们自己填的数字”
- 基础操作:能用鼠标点选格子,用键盘输1-9,填错了能按Delete或Backspace删掉
- 自动检查:填数字的时候自动判断对不对,比如同一行有没有重复的数字
2.2 高级功能:让游戏更好玩、更贴心
- 启动菜单:打开游戏先看到菜单,能选“开始游戏”“选难度”“退出游戏”,用方向键上下切换,回车键确认
- 难度选择:分简单、中等、困难三种,简单的空白少(30个空),困难的空白多(50个空)
- 计时器:游戏开始后自动计时,左下角显示“分:秒”,比如1分20秒
- 错误限制:最多能错5次,错多了就游戏失败,右下角显示错误次数
- 暂停/重置:按P键暂停或继续,按R键重新玩当前难度的数独
- 提示功能:卡壳的时候能点提示按钮,最多用3次,直接告诉当前格子的正确数字
- 记录最佳成绩:每种难度的最快完成时间会存在电脑里,下次玩能看到自己的最好成绩
- 音效反馈:填对了有“叮”的提示音,填错了有“错”的提示音,赢了输了也有对应的音效
- 棋盘缩放:用鼠标滚轮能放大、缩小棋盘,大屏小屏都能适配
- 触摸支持:屏幕底部有数字键盘,用鼠标点或者平板触摸都能输入,不用非要用键盘
三、完整代码:复制就能用,每步都有注释
下面是整合了所有功能的完整代码,我给关键步骤都加了注释,跟着注释看就能明白每段代码在做什么。把这段代码复制到文本编辑器里,保存成“sudoku_game.py”(注意后缀是.py),就能直接运行。
import pygame
import random
import sys
import time
import json
# 初始化Pygame,准备游戏环境
pygame.init()
# 初始化音效功能(用来播放提示音)
pygame.mixer.init()
# 游戏窗口大小:宽540像素,高600像素(下面多留60像素放计时、错误次数这些)
WIDTH, HEIGHT = 540, 600
CELL_SIZE = WIDTH // 9 # 每个小格子的大小:540÷9=60像素
screen = pygame.display.set_mode((WIDTH, HEIGHT)) # 创建游戏窗口
pygame.display.set_caption("好看又好玩的数独小游戏") # 窗口标题
# 颜色定义(给不同部分上色)
WHITE = (255, 255, 255) # 背景底色
BLACK = (0, 0, 0) # 文字、粗网格线颜色
GRAY = (200, 200, 200) # 细网格线颜色
BLUE = (0, 0, 255) # 提示数字颜色(一开始就有的数字)
RED = (255, 0, 0) # 选中提示、失败提示颜色
LIGHT_BLUE = (200, 230, 255) # 选中格子、按钮背景色
GREEN = (0, 255, 0) # 完成提示颜色
SHADOW_GRAY = (150, 150, 150) # 文字阴影颜色
# 字体设置(用微软雅黑,更好看,Windows默认有,Mac/Linux可以自己装)
font = pygame.font.SysFont("Microsoft YaHei", 24) # 普通数字字体
big_font = pygame.font.SysFont("Microsoft YaHei", 36) # 标题、结果提示字体
small_font = pygame.font.SysFont("Microsoft YaHei", 18)# 计时、错误次数字体
# 加载音效(需要把4个音效文件放在代码同一个文件夹里;没有的话就静音玩)
try:
correct_sound = pygame.mixer.Sound("correct.wav") # 填对了的音效
wrong_sound = pygame.mixer.Sound("wrong.wav") # 填错了的音效
win_sound = pygame.mixer.Sound("win.wav") # 赢了的音效
lose_sound = pygame.mixer.Sound("lose.wav") # 输了的音效
except:
print("没找到音效文件,游戏会静音运行")
correct_sound = wrong_sound = win_sound = lose_sound = None
# 核心函数1:生成一个完整的、正确的数独(用试错回填的方法)
def generate_full_sudoku():
grid = [[0 for _ in range(9)] for _ in range(9)] # 先建一个9×9的空格子
def fill_grid():
# 逐个遍历每个格子
for row in range(9):
for col in range(9):
if grid[row][col] == 0: # 找到空格子
# 随机打乱1-9,让每次生成的数独不一样
nums = list(range(1, 10))
random.shuffle(nums)
for num in nums:
# 检查这个数字能不能填进当前格子
if is_valid_move(grid, row, col, num):
grid[row][col] = num # 能填就填进去
# 继续填下一个格子,填完所有就返回成功
if fill_grid():
return True
# 填不下去就回溯:把当前格子清空,换个数字试试
grid[row][col] = 0
# 所有数字都试了不行,返回失败,让上一步换数字
return False
# 所有格子都填满了,返回成功
return True
fill_grid()
return grid
# 核心函数2:检查数字能不能填进这个格子(行、列、小宫格都不重复)
def is_valid_move(grid, row, col, num):
# 适配不同格式的格子数据,统一转换成列表方便检查
if isinstance(grid, dict):
current_grid = [[grid[(r, c)]["value"] for c in range(9)] for r in range(9)]
else:
current_grid = grid
# 检查当前行有没有这个数字
if num in current_grid[row]:
return False
# 检查当前列有没有这个数字
for r in range(9):
if current_grid[r][col] == num:
return False
# 检查当前3×3小宫格有没有这个数字
start_row, start_col = 3 * (row // 3), 3 * (col // 3) # 找到小宫格的左上角
for r in range(start_row, start_row + 3):
for c in range(start_col, start_col + 3):
if current_grid[r][c] == num:
return False
# 都没问题,能填
return True
# 核心函数3:根据难度删数字,做成可玩的数独
def remove_numbers(grid, difficulty):
# 不同难度删不同数量的数字:简单删30个,中等删40个,困难删50个
empty_count = {
"easy": 30,
"medium": 40,
"hard": 50
}[difficulty]
# 随机删数字,确保不重复删同一个格子
while empty_count > 0:
row = random.randint(0, 8)
col = random.randint(0, 8)
if grid[row][col] != 0:
grid[row][col] = 0
empty_count -= 1
return grid
# 绘制函数1:画数独网格(带渐变背景,区分小宫格)
def draw_grid(selected_row, selected_col, scale):
# 画渐变背景:从浅蓝到白色,比单一白色好看
gradient_surface = pygame.Surface((WIDTH, HEIGHT))
for y in range(HEIGHT):
r = 255
g = 230 + (255 - 230) * y // HEIGHT
b = 255
pygame.draw.line(gradient_surface, (r, g, b), (0, y), (WIDTH, y))
screen.blit(gradient_surface, (0, 0))
# 计算缩放后的网格位置,让网格居中显示
offset_x = (WIDTH - CELL_SIZE * 9 * scale) // 2
offset_y = (WIDTH - CELL_SIZE * 9 * scale) // 2
# 画网格线:每3行/列画粗线,其他画细线
for i in range(10):
line_color = BLACK if i % 3 == 0 else GRAY
line_width = 2 if i % 3 == 0 else 1 # 粗线2像素,细线1像素
# 画垂直线
x = offset_x + i * CELL_SIZE * scale
pygame.draw.line(screen, line_color, (x, offset_y), (x, offset_y + CELL_SIZE * 9 * scale), line_width)
# 画水平线
y = offset_y + i * CELL_SIZE * scale
pygame.draw.line(screen, line_color, (offset_x, y), (offset_x + CELL_SIZE * 9 * scale, y), line_width)
# 给选中的格子画个粗边框,高亮显示
if selected_row is not None and selected_col is not None:
x = offset_x + selected_col * CELL_SIZE * scale
y = offset_y + selected_row * CELL_SIZE * scale
width = CELL_SIZE * scale
height = CELL_SIZE * scale
pygame.draw.rect(screen, LIGHT_BLUE, (x, y, width, height), 3)
return offset_x, offset_y # 返回网格偏移位置,方便后续画数字
# 辅助函数:给文字加阴影,更好看
def draw_shadow_text(text_surface, x, y, shadow_color=SHADOW_GRAY, offset=2):
# 先画阴影(偏移2像素)
shadow_surface = text_surface.copy()
shadow_surface.fill(shadow_color, special_flags=pygame.BLEND_RGBA_MULT)
screen.blit(shadow_surface, (x + offset, y + offset))
# 再画文字本身
screen.blit(text_surface, (x, y))
# 辅助函数:数字淡入效果(填数字时慢慢显示,更流畅)
def draw_fade_number(row, col, num, alpha, scale, offset_x, offset_y):
# 按缩放比例调整字体大小
scaled_font = pygame.font.SysFont("Microsoft YaHei", int(24 * scale))
text = scaled_font.render(str(num), True, BLACK)
text.set_alpha(alpha) # alpha值0是全透明,255是不透明
# 计算数字位置
x = offset_x + col * CELL_SIZE * scale + 15 * scale
y = offset_y + row * CELL_SIZE * scale + 15 * scale
screen.blit(text, (x, y))
# 绘制函数2:画数字(区分提示数字和玩家填的数字,带淡入效果)
def draw_numbers(game_grid, original_grid, scale, offset_x, offset_y):
for row in range(9):
for col in range(9):
data = game_grid[(row, col)]
num = data["value"]
if num != 0:
# 处理淡入动画
if data["is_animating"]:
data["alpha"] += 10 # 每次透明度加10,慢慢变清晰
if data["alpha"] >= 255:
data["alpha"] = 255
data["is_animating"] = False # 完全显示后停止动画
draw_fade_number(row, col, num, data["alpha"], scale, offset_x, offset_y)
else:
# 提示数字(一开始就有的)用蓝色,玩家填的用黑色
text_color = BLUE if original_grid[row][col] != 0 else BLACK
scaled_font = pygame.font.SysFont("Microsoft YaHei", int(24 * scale))
text = scaled_font.render(str(num), True, text_color)
# 画带阴影的数字
x = offset_x + col * CELL_SIZE * scale + 15 * scale
y = offset_y + row * CELL_SIZE * scale + 15 * scale
draw_shadow_text(text, x, y)
# 绘制函数3:画提示按钮(显示剩余提示次数)
def draw_hint_button(remaining_hints):
# 画按钮背景
button_rect = pygame.Rect(WIDTH // 2 - 50, HEIGHT - 35, 100, 30)
pygame.draw.rect(screen, LIGHT_BLUE, button_rect)
# 画按钮文字
hint_text = small_font.render(f"提示: {remaining_hints}", True, BLACK)
draw_shadow_text(hint_text, WIDTH // 2 - hint_text.get_width() // 2, HEIGHT - 30)
return button_rect # 返回按钮位置,方便判断是否被点击
# 绘制函数4:画屏幕底部的数字键盘(支持鼠标/触摸点击)
def draw_touch_keyboard(scale, offset_x):
keyboard_rects = [] # 存每个数字按钮的位置和对应的数字
# 键盘放在屏幕底部
keyboard_y = HEIGHT - 120
key_width = 50 * scale
key_height = 35 * scale
key_gap = 10 * scale # 按钮之间的间距
for i in range(9):
row = i // 3 # 行号(0-2,共3行)
col = i % 3 # 列号(0-2,共3列)
# 计算每个按钮的位置
x = offset_x + col * (key_width + key_gap) + (WIDTH - 3 * (key_width + key_gap) + key_gap) // 2
y = keyboard_y + row * (key_height + key_gap)
rect = pygame.Rect(x, y, key_width, key_height)
# 画按钮背景
pygame.draw.rect(screen, LIGHT_BLUE, rect)
# 画按钮上的数字
scaled_font = pygame.font.SysFont("Microsoft YaHei", int(24 * scale))
text = scaled_font.render(str(i + 1), True, BLACK)
text_x = rect.x + (rect.width - text.get_width()) // 2
text_y = rect.y + (rect.height - text.get_height()) // 2
draw_shadow_text(text, text_x, text_y)
# 记录按钮信息
keyboard_rects.append((rect, i + 1))
return keyboard_rects
# 检查游戏是否完成(所有格子填满且都正确)
def is_complete(game_grid):
# 把格子数据转换成列表,方便检查
current_grid = [[game_grid[(r, c)]["value"] for c in range(9)] for r in range(9)]
for row in range(9):
for col in range(9):
# 有空格子,或者数字不正确,就还没完成
if current_grid[row][col] == 0 or not is_valid_move(current_grid, row, col, current_grid[row][col]):
return False
return True # 所有格子都对了,完成!
# 读取历史最佳成绩(存在sudoku_best_scores.txt文件里)
def read_best_scores():
try:
with open("sudoku_best_scores.txt", "r", encoding="utf-8") as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
# 没找到文件或文件损坏,就初始化一个空成绩表
return {"easy": None, "medium": None, "hard": None}
# 保存最佳成绩(只有当前成绩比历史好,才更新)
def save_best_score(difficulty, time):
scores = read_best_scores()
if scores[difficulty] is None or time < scores[difficulty]:
scores[difficulty] = time
with open("sudoku_best_scores.txt", "w", encoding="utf-8") as f:
json.dump(scores, f, indent=2)
# 菜单函数1:启动菜单(主菜单)
def start_menu():
menu_options = ["开始游戏", "选择难度", "退出游戏"]
selected_index = 0 # 默认选中第一个选项
while True:
# 画和游戏内一样的渐变背景,风格统一
gradient_surface = pygame.Surface((WIDTH, HEIGHT))
for y in range(HEIGHT):
r = 255
g = 230 + (255 - 230) * y // HEIGHT
b = 255
pygame.draw.line(gradient_surface, (r, g, b), (0, y), (WIDTH, y))
screen.blit(gradient_surface, (0, 0))
# 画菜单标题(带阴影)
title_text = big_font.render("数独小游戏", True, BLACK)
draw_shadow_text(title_text, WIDTH // 2 - title_text.get_width() // 2, 100)
# 画菜单选项,选中的用红色标出来
for i, option in enumerate(menu_options):
option_color = RED if i == selected_index else BLACK
option_text = font.render(option, True, option_color)
# 选项居中,上下间隔50像素
draw_shadow_text(option_text, WIDTH // 2 - option_text.get_width() // 2, 200 + i * 50)
# 更新显示
pygame.display.flip()
# 处理菜单操作
for event in pygame.event.get():
if event.type == pygame.QUIT: # 点击窗口关闭按钮
pygame.quit()
sys.exit()
elif event.type == pygame.KEYDOWN:
# 方向键上下切换选项
if event.key == pygame.K_UP:
selected_index = (selected_index - 1) % len(menu_options)
elif event.key == pygame.K_DOWN:
selected_index = (selected_index + 1) % len(menu_options)
# 回车键确认选择
elif event.key == pygame.K_RETURN:
if selected_index == 0:
return "start" # 选了开始游戏
elif selected_index == 1:
return "difficulty" # 选了选择难度
elif selected_index == 2:
pygame.quit()
sys.exit()
# 菜单函数2:难度选择菜单
def difficulty_menu():
difficulties = ["简单", "中等", "困难"]
difficulty_mapping = ["easy", "medium", "hard"] # 和后面的难度参数对应
selected_index = 1 # 默认选中中等难度
while True:
# 画渐变背景
gradient_surface = pygame.Surface((WIDTH, HEIGHT))
for y in range(HEIGHT):
r = 255
g = 230 + (255 - 230) * y // HEIGHT
b = 255
pygame.draw.line(gradient_surface, (r, g, b), (0, y), (WIDTH, y))
screen.blit(gradient_surface, (0, 0))
# 画标题
title_text = big_font.render("选择难度", True, BLACK)
draw_shadow_text(title_text, WIDTH // 2 - title_text.get_width() // 2, 100)
# 画难度选项,选中的用红色
for i, diff in enumerate(difficulties):
diff_color = RED if i == selected_index else BLACK
diff_text = font.render(diff, True, diff_color)
draw_shadow_text(diff_text, WIDTH // 2 - diff_text.get_width() // 2, 200 + i * 50)
# 更新显示
pygame.display.flip()
# 处理难度选择操作
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.KEYDOWN:
# 方向键上下切换
if event.key == pygame.K_UP:
selected_index = (selected_index - 1) % len(difficulties)
elif event.key == pygame.K_DOWN:
selected_index = (selected_index + 1) % len(difficulties)
# 回车键确认
elif event.key == pygame.K_RETURN:
return difficulty_mapping[selected_index]
# 游戏主循环:所有游戏逻辑都在这里
def main_game(difficulty):
# 生成数独:先做完整的,再按难度删数字
original_grid = generate_full_sudoku() # 完整的正确数独(用来区分提示数字)
# 初始化游戏格子:用字典存每个格子的数值、动画状态、透明度
game_grid = {}
for r in range(9):
for c in range(9):
val = original_grid[r][c]
game_grid[(r, c)] = {
"value": val,
"is_animating": False,
"alpha": 0 if val == 0 else 255
}
# 按难度删数字,做成空白的可玩数独
empty_grid = remove_numbers([row[:] for row in original_grid], difficulty)
for r in range(9):
for c in range(9):
if empty_grid[r][c] == 0:
game_grid[(r, c)]["value"] = 0
game_grid[(r, c)]["alpha"] = 0
# 游戏状态初始化
selected_row, selected_col = None, None # 选中的格子(一开始没有选中的)
start_time = time.time() # 游戏开始时间
is_paused = False # 暂停标记(默认不暂停)
wrong_count = 0 # 错误次数
MAX_WRONG = 5 # 最多错5次
has_played_win = False # 胜利音效是否已经播放(避免重复放)
has_played_lose = False # 失败音效是否已经播放
# 提示功能初始化:最多3次提示
max_hints = 3
remaining_hints = max_hints
# 最佳成绩初始化:读取历史最好成绩
best_scores = read_best_scores()
best_time = best_scores[difficulty]
# 缩放功能初始化:默认1倍大小,可放大到1.5倍,缩小到0.8倍
scale = 1.0
min_scale = 0.8
max_scale = 1.5
scale_step = 0.1 # 每次滚轮缩放0.1倍
running = True
while running:
if not is_paused: # 没暂停就正常显示游戏内容
# 画网格和背景
offset_x, offset_y = draw_grid(selected_row, selected_col, scale)
# 画数字
draw_numbers(game_grid, original_grid, scale, offset_x, offset_y)
# 画计时器(左下角)
elapsed_time = int(time.time() - start_time)
minute = elapsed_time // 60
second = elapsed_time % 60
time_text = small_font.render(f"时间: {minute}:{second:02d}", True, BLACK)
draw_shadow_text(time_text, 10, HEIGHT - 30)
# 画错误次数(右下角)
wrong_text = small_font.render(f"错误次数: {wrong_count}/{MAX_WRONG}", True, BLACK)
draw_shadow_text(wrong_text, WIDTH - wrong_text.get_width() - 10, HEIGHT - 30)
# 画提示按钮(底部中间)
hint_button_rect = draw_hint_button(remaining_hints)
# 画底部的数字键盘
keyboard_rects = draw_touch_keyboard(scale, offset_x)
# 检查游戏是否结束(完成或失败)
game_complete = is_complete(game_grid)
if game_complete:
# 显示胜利提示
complete_text = big_font.render("恭喜完成!", True, GREEN)
draw_shadow_text(complete_text, WIDTH // 2 - complete_text.get_width() // 2, HEIGHT - 60)
# 计算完成时间,保存最佳成绩
finish_time = int(time.time() - start_time)
save_best_score(difficulty, finish_time)
# 显示最佳成绩
best_text = small_font.render(
f"最佳时间: {best_time//60}:{best_time%60:02d}" if best_time else "新纪录!",
True, GREEN
)
draw_shadow_text(best_text, WIDTH // 2 - best_text.get_width() // 2, HEIGHT - 30)
# 播放胜利音效(只放一次)
if win_sound and not has_played_win:
win_sound.play()
has_played_win = True
elif wrong_count >= MAX_WRONG:
# 显示失败提示
fail_text = big_font.render("游戏失败!", True, RED)
draw_shadow_text(fail_text, WIDTH // 2 - fail_text.get_width() // 2, HEIGHT - 60)
# 播放失败音效(只放一次)
if lose_sound and not has_played_lose:
lose_sound.play()
has_played_lose = True
else: # 暂停状态,显示暂停提示
pause_text = big_font.render("游戏暂停", True, RED)
draw_shadow_text(pause_text, WIDTH // 2 - pause_text.get_width() // 2, HEIGHT // 2)
# 更新屏幕显示
pygame.display.flip()
# 处理玩家操作(鼠标、键盘)
for event in pygame.event.get():
if event.type == pygame.QUIT: # 关闭窗口
pygame.quit()
sys.exit()
elif event.type == pygame.KEYDOWN:
# 按P键暂停/继续
if event.key == pygame.K_p:
is_paused = not is_paused
# 按R键重新开始
elif event.key == pygame.K_r:
return "restart"
# 没暂停且选中了格子,处理数字输入
elif not is_paused and selected_row is not None and selected_col is not None:
# 按1-9数字键
if event.key in range(pygame.K_1, pygame.K_9 + 1):
input_num = event.key - pygame.K_0 # 把键盘按键转换成数字
# 只能改空白的格子(提示数字不能改)
if original_grid[selected_row][selected_col] == 0:
# 检查数字能不能填
if is_valid_move(game_grid, selected_row, selected_col, input_num):
game_grid[(selected_row, selected_col)]["value"] = input_num
game_grid[(selected_row, selected_col)]["is_animating"] = True
game_grid[(selected_row, selected_col)]["alpha"] = 0
if correct_sound:
correct_sound.play() # 填对了,放提示音
else:
wrong_count += 1 # 填错了,计数加1
if wrong_sound:
wrong_sound.play() # 放错误提示音
# 按Delete或Backspace删数字
elif event.key == pygame.K_DELETE or event.key == pygame.K_BACKSPACE:
if original_grid[selected_row][selected_col] == 0:
game_grid[(selected_row, selected_col)]["value"] = 0
# 鼠标滚轮:缩放棋盘
elif event.type == pygame.MOUSEWHEEL:
if event.y > 0: # 滚轮向上,放大
scale = min(scale + scale_step, max_scale)
else: # 滚轮向下,缩小
scale = max(scale - scale_step, min_scale)
# 没暂停时,处理鼠标点击
elif not is_paused and event.type == pygame.MOUSEBUTTONDOWN:
x, y = event.pos # 获取点击的位置
# 1. 检查是不是点了提示按钮
if hint_button_rect.collidepoint(x, y):
if remaining_hints > 0 and selected_row is not None and selected_col is not None:
# 选中的是空白格子,才能提示
if game_grid[(selected_row, selected_col)]["value"] == 0 and original_grid[selected_row][selected_col] == 0:
correct_num = original_grid[selected_row][selected_col] # 从完整数独里拿正确数字
game_grid[(selected_row, selected_col)]["value"] = correct_num
game_grid[(selected_row, selected_col)]["is_animating"] = True
game_grid[(selected_row, selected_col)]["alpha"] = 0
remaining_hints -= 1 # 提示次数减1
# 2. 检查是不是点了底部的数字键盘
for rect, num in keyboard_rects:
if rect.collidepoint(x, y) and selected_row is not None and selected_col is not None:
if original_grid[selected_row][selected_col] == 0:
if is_valid_move(game_grid, selected_row, selected_col, num):
game_grid[(selected_row, selected_col)]["value"] = num
game_grid[(selected_row, selected_col)]["is_animating"] = True
game_grid[(selected_row, selected_col)]["alpha"] = 0
if correct_sound:
correct_sound.play()
else:
wrong_count += 1
if wrong_sound:
wrong_sound.play()
# 3. 检查是不是点了网格,选中格子
if y< offset_y + CELL_SIZE * 9 * scale and y >= offset_y:
# 计算选中的格子位置(适配缩放)
selected_col = int((x - offset_x) // (CELL_SIZE * scale))
selected_row = int((y - offset_y) // (CELL_SIZE * scale))
# 确保选中的格子在0-8范围内(不超出棋盘)
selected_col = max(0, min(8, selected_col))
selected_row = max(0, min(8, selected_row))
# 程序入口:把菜单和游戏连起来
def main():
current_difficulty = "medium" # 默认中等难度
while True:
# 显示启动菜单,看玩家选什么
menu_action = start_menu()
if menu_action == "start":
# 进入游戏,玩完后如果选重新开始,就回到菜单
game_result = main_game(current_difficulty)
if game_result == "restart":
continue
elif menu_action == "difficulty":
# 玩家选了难度,更新当前难度
current_difficulty = difficulty_menu()
if __name__ == "__main__":
main()四、怎么运行游戏?操作说明看这里
4.1 运行步骤(超简单)
- 复制上面的完整代码,粘贴到记事本(或Notepad++、VS Code等文本编辑器)里
- 点击“保存”,文件名填“sudoku_game.py”,保存类型选“所有文件”,然后选个容易找到的文件夹(比如桌面)保存
- (可选)准备音效文件:找4个wav格式的音效文件,分别命名为correct.wav(填对)、wrong.wav(填错)、win.wav(赢)、lose.wav(输),放在和代码文件同一个文件夹里。没有的话也能玩,就是没音效
- 打开命令行,切换到代码保存的文件夹(比如桌面:输入cd Desktop,按回车)
- 输入python sudoku_game.py,按回车,游戏就启动了
4.2 怎么玩?操作全说明
1. 菜单操作
- 方向键↑↓:上下切换菜单选项
- 回车键:确认选择
- 启动菜单有三个选项:开始游戏、选择难度、退出游戏
2. 游戏内操作
- 选格子:用鼠标点击想填数字的格子,选中后会有浅蓝色粗边框
- 填数字:两种方式——一是按键盘上的1-9键;二是点击屏幕底部的数字按钮
- 删数字:按Delete键或Backspace键,能删掉自己填的数字(提示数字删不了)
- 暂停/继续:按P键,再按一次P键恢复
- 重新开始:按R键,会回到启动菜单,重新开一局当前难度的游戏
- 提示:点击底部中间的“提示”按钮,能看到当前选中格子的正确数字,最多用3次
- 缩放棋盘:滚动鼠标滚轮,向上放大,向下缩小(范围0.8-1.5倍)
3. 游戏结束条件
- 胜利:把所有空白格子都填满,而且都正确,会显示“恭喜完成!”,还会记录你的完成时间
- 失败:错误输入达到5次,会显示“游戏失败!”
五、核心功能怎么实现的?简单说清楚
5.1 数独是怎么生成的?
用的是“试错回填”的方法,就像我们自己填数独一样:
- 从第一个格子开始,随机选一个1-9的数字试填
- 检查这个数字在当前行、当前列、当前3×3小格子里有没有重复,没重复就填进去
- 接着填下一个格子,重复步骤1-2
- 如果填到某个格子,1-9的数字都不能填(填了就重复),就把上一个填的格子清空,换个数字再试(这就是“回填”)
- 一直试到所有格子都填满,一个完整的正确数独就做好了
- 最后根据难度删掉一些数字,做成空白的可玩棋盘
5.2 怎么判断填的数字对不对?
就检查三个地方,都没重复就是对的:
- 当前行:看这一行有没有和要填的数字一样的
- 当前列:看这一列有没有和要填的数字一样的
- 当前3×3小格子:先找到小格子的左上角(比如第4行第5列,左上角就是第3行第3列),然后检查这个小格子里9个格子有没有重复
5.3 那些好看的效果是怎么实现的?
- 渐变背景:一行一行画不同颜色的线,从浅蓝慢慢过渡到白色
- 文字阴影:先画一个偏移2像素的灰色文字(阴影),再在上面画黑色文字
- 数字淡入:填数字时,让数字的透明度从0慢慢升到255(从全透明到不透明),看起来就是慢慢显示出来的
- 网格区分:3×3小格子的边框用粗黑线,其他网格用细灰线,一眼就能分清小格子
六、总结:新手也能学会的游戏开发
这篇教程从装环境到写代码,再到运行游戏,一步步都讲得很清楚,用的都是通俗的语言,没有复杂的专业术语。通过做这个数独游戏,不仅能学会用Pygame做游戏界面、处理交互,还能理解“试错回填”这种解决问题的思路。
如果是Python新手,建议先把基础功能(生成数独、画棋盘、基础输入)看懂,跑起来之后,再慢慢研究提示、音效、最佳成绩这些高级功能。如果有基础,也可以在这个代码的基础上改,比如加个深色主题、多弄几种难度,或者做个在线排行榜。
希望这篇教程能帮到想学习Python游戏开发的你,赶紧把代码复制过去试试吧!
七、新手常见问题及解决方案
新手在操作过程中很容易遇到一些小问题,这里整理了最常见的几种情况,附上具体解决方法,帮你快速避坑。
7.1 环境安装相关问题
问题1:输入“pip install pygame”报错,提示“pip不是内部或外部命令”
原因:安装Python时没把Python的“Scripts”文件夹添加到系统环境变量里,电脑找不到pip工具。解决方案:
- 先找到Python的安装路径(比如Windows默认可能在“C:\Users\你的用户名\AppData\Local\Programs\Python\Python310\Scripts”,注意把“你的用户名”和“Python310”换成你实际的用户名和安装版本);
- 把这个路径添加到系统环境变量(Windows:右键“此电脑”→“属性”→“高级系统设置”→“环境变量”→“系统变量”里找到“Path”→“编辑”→“新建”,粘贴刚才的路径,然后一路点“确定”);
- 关闭已经打开的命令行,重新打开一个,再输入“pip install pygame”即可。
问题2:安装Pygame成功后,运行代码提示“import pygame 报错:No module named 'pygame'”
原因:可能是电脑里装了多个Python版本,pip把Pygame装到了其他版本里,而你运行代码用的是另一个版本。解决方案:
- 先确认运行代码的Python版本:打开命令行,输入“python --version”,记录下版本号(比如3.10.6);
- 用对应版本的pip重新安装:如果版本是3.x,输入“python3 -m pip install pygame”(Mac/Linux)或“python -m pip install pygame”(Windows),强制用当前Python版本的pip安装;
- 如果还是不行,打开文本编辑器(比如VS Code),在设置里找到“Python解释器”,选择安装了Pygame的那个Python版本,再运行代码。
问题3:Mac系统安装Pygame后,运行代码提示“pygame.error: No available video device”
原因:Mac系统的图形驱动适配问题,需要额外安装依赖。解决方案:打开终端,输入“brew install sdl2 sdl2_image sdl2_ttf sdl2_mixer”(需要先安装brew,brew是Mac的包管理工具,官网https://brew.sh/ 有安装教程),安装完成后重新运行代码。
7.2 代码运行相关问题
问题1:代码复制后保存,运行提示“找不到文件”
原因:保存的文件名不对,或者命令行切换的文件夹不是代码保存的文件夹。解决方案:
- 确认文件名是“sudoku_game.py”,后缀必须是“.py”,不是“.txt”(保存时注意选“保存类型:所有文件”,避免自动加“.txt”后缀);
- 命令行切换文件夹要准确:比如代码保存在桌面,Windows输入“cd Desktop”,Mac输入“cd ~/Desktop”,按回车后再输入“python sudoku_game.py”。
问题2:运行代码后,窗口一闪就消失
原因:要么是代码复制不完整(比如少了最后几行),要么是运行环境有问题,导致游戏主循环没正常执行。解决方案:
- 重新检查代码,确保从“import pygame”开始到“if __name__ == "__main__": main()”完整复制,没有遗漏;
- 如果代码完整,在代码最后一行(main()后面)添加两行代码:“input("按任意键退出...")”,保存后重新运行,能看到报错信息,根据报错信息排查问题。
问题3:游戏窗口能打开,但点击格子没反应/无法输入数字
原因:可能是鼠标点击位置计算错误,或者代码里的“scale”缩放参数异常。解决方案:
- 先检查代码里的“scale”初始化部分,确保是“scale = 1.0”,没有被改成其他异常值;
- 如果还是没反应,重启游戏试试,或者重新复制完整代码替换掉原来的文件,可能是之前复制时不小心改了代码里的参数。
问题4:提示“没找到音效文件”,游戏无法运行
原因:代码里有加载音效的逻辑,但文件夹里没有对应的音效文件,虽然代码写了“静音运行”的处理,但部分版本可能出现兼容问题。解决方案:
- 简单方法:直接注释掉音效相关代码(在代码里找到“try:”开始的音效加载部分,用“#”把这部分代码全部注释掉,包括try、except和里面的内容);
- 彻底方法:去免费音效网站(比如https://freesound.org/)下载4个wav格式的音效,分别命名为correct.wav、wrong.wav、win.wav、lose.wav,放在代码同一个文件夹里。
八、代码调试技巧(新手入门级)
如果运行代码出现错误,或者功能不符合预期,不用慌,掌握这几个简单的调试技巧,就能快速找到问题所在。
8.1 先看报错信息,定位问题位置
代码运行出错时,命令行会弹出红色的报错信息,重点看两个地方:
- 最后一行的“Error: xxx”:这是错误类型,比如“SyntaxError”是语法错误(代码写错了格式),“IndexError”是索引错误(比如访问了不存在的格子);
- 报错信息里的“File "sudoku_game.py", line 123”:这表示问题出在“sudoku_game.py”文件的第123行,直接定位到这一行检查即可。
示例:如果报错“File "sudoku_game.py", line 50, in
8.2 用“print语句”打印关键信息
这是新手最容易掌握的调试方法,在代码的关键位置添加print语句,打印变量的值,看是否符合预期。示例:
- 如果不确定数独是否生成成功,可以在“generate_full_sudoku()”函数的最后添加“print(grid)”,运行代码后,命令行会打印出完整的数独网格,看是否都是1-9的数字,没有重复;
- 如果点击格子没反应,可以在处理鼠标点击的代码里添加“print(x, y)”,打印出点击的坐标,看是否和网格的坐标范围匹配。
注意:调试完成后,记得把这些临时添加的print语句删掉,避免影响游戏运行。
8.3 分段测试代码,逐步排查
如果不想一次性运行整个代码,可以把代码分成几个部分单独测试,比如:
- 先测试“生成数独”功能:单独运行“generate_full_sudoku()”和“remove_numbers()”函数,看是否能生成正确的空白数独;
- 再测试“画棋盘”功能:注释掉游戏逻辑、交互操作相关代码,只保留画网格、画数字的代码,运行后看窗口是否能正常显示9×9的网格;
- 最后测试交互功能:逐步添加鼠标点击、键盘输入的代码,每添加一部分就运行测试一次,确保功能正常。
8.4 借助简单的调试工具
如果觉得print语句不够方便,可以用VS Code自带的调试功能:
- 打开VS Code,加载代码文件;
- 点击左侧的“运行和调试”按钮(或按Ctrl+Shift+D);
- 点击“创建launch.json文件”,选择“Python”,自动生成配置;
- 在代码的关键行左侧点击,添加“断点”(会出现一个红色圆点);
- 点击“启动调试”,代码会在断点处暂停,此时可以查看各个变量的值、逐步执行代码,清晰看到代码的运行过程。
调试的核心思路是“缩小问题范围”,从报错位置入手,逐步排查,新手多练几次就能熟练掌握啦!
版权所属:SO JSON在线解析
原文地址:https://www.sojson.com/blog/555.html
转载时必须以链接形式注明原始出处及本声明。
如果本文对你有帮助,那么请你赞助我,让我更有激情的写下去,帮助更多的人。
