用Python做数独游戏:从零开始,新手也能看懂

JSON 2026-01-05 15:31:32 750

一、准备工作:装好用得到的工具

1.1 我们需要哪些工具?

核心工具是Pygame,这是Python专门用来做游戏的第三方库,能帮我们画出游戏界面、处理鼠标点击、键盘输入这些交互操作。除此之外,还会用到Python自带的几个小工具:

  • random:用来生成随机数,让每次玩的数独都不一样
  • sys:帮我们处理游戏退出的操作
  • time:实现计时功能,记录我们玩一局用了多久

这些自带的工具不用额外装,只有Pygame需要手动装一下。

1.2 怎么装环境?一步一步来

首先得确保你电脑上装了Python(建议装3.7及以上版本,网上搜“Python下载”就能找到官方安装包,跟着指引装就行)。装完Python后,按下面步骤装Pygame:

  1. 打开电脑的命令行(Windows按Win+R,输cmd回车;Mac打开终端)
  2. 在命令行里输入:pip install pygame,然后按回车
  3. 等一会儿,出现成功提示就装好了。可以验证一下:打开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 运行步骤(超简单)

  1. 复制上面的完整代码,粘贴到记事本(或Notepad++、VS Code等文本编辑器)里
  2. 点击“保存”,文件名填“sudoku_game.py”,保存类型选“所有文件”,然后选个容易找到的文件夹(比如桌面)保存
  3. (可选)准备音效文件:找4个wav格式的音效文件,分别命名为correct.wav(填对)、wrong.wav(填错)、win.wav(赢)、lose.wav(输),放在和代码文件同一个文件夹里。没有的话也能玩,就是没音效
  4. 打开命令行,切换到代码保存的文件夹(比如桌面:输入cd Desktop,按回车)
  5. 输入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. 从第一个格子开始,随机选一个1-9的数字试填
  2. 检查这个数字在当前行、当前列、当前3×3小格子里有没有重复,没重复就填进去
  3. 接着填下一个格子,重复步骤1-2
  4. 如果填到某个格子,1-9的数字都不能填(填了就重复),就把上一个填的格子清空,换个数字再试(这就是“回填”)
  5. 一直试到所有格子都填满,一个完整的正确数独就做好了
  6. 最后根据难度删掉一些数字,做成空白的可玩棋盘

5.2 怎么判断填的数字对不对?

就检查三个地方,都没重复就是对的:

  1. 当前行:看这一行有没有和要填的数字一样的
  2. 当前列:看这一列有没有和要填的数字一样的
  3. 当前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工具。解决方案:

  1. 先找到Python的安装路径(比如Windows默认可能在“C:\Users\你的用户名\AppData\Local\Programs\Python\Python310\Scripts”,注意把“你的用户名”和“Python310”换成你实际的用户名和安装版本);
  2. 把这个路径添加到系统环境变量(Windows:右键“此电脑”→“属性”→“高级系统设置”→“环境变量”→“系统变量”里找到“Path”→“编辑”→“新建”,粘贴刚才的路径,然后一路点“确定”);
  3. 关闭已经打开的命令行,重新打开一个,再输入“pip install pygame”即可。

问题2:安装Pygame成功后,运行代码提示“import pygame 报错:No module named 'pygame'”

原因:可能是电脑里装了多个Python版本,pip把Pygame装到了其他版本里,而你运行代码用的是另一个版本。解决方案:

  1. 先确认运行代码的Python版本:打开命令行,输入“python --version”,记录下版本号(比如3.10.6);
  2. 用对应版本的pip重新安装:如果版本是3.x,输入“python3 -m pip install pygame”(Mac/Linux)或“python -m pip install pygame”(Windows),强制用当前Python版本的pip安装;
  3. 如果还是不行,打开文本编辑器(比如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:代码复制后保存,运行提示“找不到文件”

原因:保存的文件名不对,或者命令行切换的文件夹不是代码保存的文件夹。解决方案:

  1. 确认文件名是“sudoku_game.py”,后缀必须是“.py”,不是“.txt”(保存时注意选“保存类型:所有文件”,避免自动加“.txt”后缀);
  2. 命令行切换文件夹要准确:比如代码保存在桌面,Windows输入“cd Desktop”,Mac输入“cd ~/Desktop”,按回车后再输入“python sudoku_game.py”。

问题2:运行代码后,窗口一闪就消失

原因:要么是代码复制不完整(比如少了最后几行),要么是运行环境有问题,导致游戏主循环没正常执行。解决方案:

  1. 重新检查代码,确保从“import pygame”开始到“if __name__ == "__main__": main()”完整复制,没有遗漏;
  2. 如果代码完整,在代码最后一行(main()后面)添加两行代码:“input("按任意键退出...")”,保存后重新运行,能看到报错信息,根据报错信息排查问题。

问题3:游戏窗口能打开,但点击格子没反应/无法输入数字

原因:可能是鼠标点击位置计算错误,或者代码里的“scale”缩放参数异常。解决方案:

  1. 先检查代码里的“scale”初始化部分,确保是“scale = 1.0”,没有被改成其他异常值;
  2. 如果还是没反应,重启游戏试试,或者重新复制完整代码替换掉原来的文件,可能是之前复制时不小心改了代码里的参数。

问题4:提示“没找到音效文件”,游戏无法运行

原因:代码里有加载音效的逻辑,但文件夹里没有对应的音效文件,虽然代码写了“静音运行”的处理,但部分版本可能出现兼容问题。解决方案:

  1. 简单方法:直接注释掉音效相关代码(在代码里找到“try:”开始的音效加载部分,用“#”把这部分代码全部注释掉,包括try、except和里面的内容);
  2. 彻底方法:去免费音效网站(比如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 print(num)”,就直接打开代码第50行,检查“print(num)”是不是写错了(比如少了括号、num变量没定义)。

8.2 用“print语句”打印关键信息

这是新手最容易掌握的调试方法,在代码的关键位置添加print语句,打印变量的值,看是否符合预期。示例:

  1. 如果不确定数独是否生成成功,可以在“generate_full_sudoku()”函数的最后添加“print(grid)”,运行代码后,命令行会打印出完整的数独网格,看是否都是1-9的数字,没有重复;
  2. 如果点击格子没反应,可以在处理鼠标点击的代码里添加“print(x, y)”,打印出点击的坐标,看是否和网格的坐标范围匹配。

注意:调试完成后,记得把这些临时添加的print语句删掉,避免影响游戏运行。

8.3 分段测试代码,逐步排查

如果不想一次性运行整个代码,可以把代码分成几个部分单独测试,比如:

  1. 先测试“生成数独”功能:单独运行“generate_full_sudoku()”和“remove_numbers()”函数,看是否能生成正确的空白数独;
  2. 再测试“画棋盘”功能:注释掉游戏逻辑、交互操作相关代码,只保留画网格、画数字的代码,运行后看窗口是否能正常显示9×9的网格;
  3. 最后测试交互功能:逐步添加鼠标点击、键盘输入的代码,每添加一部分就运行测试一次,确保功能正常。

8.4 借助简单的调试工具

如果觉得print语句不够方便,可以用VS Code自带的调试功能:

  1. 打开VS Code,加载代码文件;
  2. 点击左侧的“运行和调试”按钮(或按Ctrl+Shift+D);
  3. 点击“创建launch.json文件”,选择“Python”,自动生成配置;
  4. 在代码的关键行左侧点击,添加“断点”(会出现一个红色圆点);
  5. 点击“启动调试”,代码会在断点处暂停,此时可以查看各个变量的值、逐步执行代码,清晰看到代码的运行过程。

调试的核心思路是“缩小问题范围”,从报错位置入手,逐步排查,新手多练几次就能熟练掌握啦!

版权所属:SO JSON在线解析

原文地址:https://www.sojson.com/blog/555.html

转载时必须以链接形式注明原始出处及本声明。

本文主题:

如果本文对你有帮助,那么请你赞助我,让我更有激情的写下去,帮助更多的人。

关于作者
一个低调而闷骚的男人。
相关文章
Python print() 函
Python print() 函
Python元组剖析
PHP用户定义函详细讲解
python之numpy常用的100种数值相关方法及代码示例
nodejs开发网站哪个框架?有什么优势?
PHP用户定义函详细讲解
Python元组剖析
JSON.stringify 函 (JavaScript)讲解
python基础代码示例(可免费复制)
最新文章
文件上传漏洞与防御 4058
前端构建工具选型指南:Webpack、Vite、Rollup、esbuild 深度对比 1444
物联网时代2026年时序数据库选型指南 1151
SaaS行业面临AI挑战:从“无限复用”到“灵活适应” 1269
神经网络:从构造到模型训练全链路解析 1168
一文吃透 Redis 核心存储结构:ziplist、listpack 与哈希表扩容 / 并发查询 1593
Linux sudo提权完整指南:从基础用法到生产级安全配置 691
XSS 和 CSRF 的本质区别及开发防御全解析 772
JVM垃圾回收(GC)全维度解析:从原理到调优实战 813
Linux动静态库与ELF加载全解析:从实操制作到底层原理 912
最热文章
免费天气API,天气JSON API,不限次数获取十五天的天气预报 783114
最新MyEclipse8.5注册码,有效期到2020年 (已经更新) 711464
苹果电脑Mac怎么恢复出厂系统?苹果系统怎么重装系统? 679993
Jackson 时间格式化,时间注解 @JsonFormat 用法、时差问题说明 562673
我为什么要选择RabbitMQ ,RabbitMQ简介,各种MQ选型对比 512621
Elasticsearch教程(四) elasticsearch head 插件安装和使用 484794
Jackson 美化输出JSON,优雅的输出JSON数据,格式化输出JSON数据... ... 302947
Java 信任所有SSL证书,HTTPS请求抛错,忽略证书请求完美解决 247433
Elasticsearch教程(一),全程直播(小白级别) 233097
谈谈斐讯路由器劫持,你用斐讯路由器,你需要知道的事情 228329
支付扫码

所有赞助/开支都讲公开明细,用于网站维护:赞助名单查看

查看我的收藏

正在加载... ...