Pygame 小游戏——数独

Pygame 小游戏------数独


项目概述

本文通过 Pygame 实现一个功能完整的数独游戏(Sudoku)。

游戏随机生成合法数独题目,玩家通过鼠标点击选格、键盘输入数字来完成填写,支持三种难度等级,并提供实时错误高亮、关联格着色、计时、提示等功能。填满所有格子且无误即为通关。其中:

  • 难度分级:简单(挖空 36 格)/ 中等(挖空 46 格)/ 困难(挖空 54 格),点击顶部按钮切换,立即生成新题。
  • 选格交互:鼠标点击选中格子,选中格高亮蓝色;同行、同列、同宫格自动浅蓝着色;与选中格数字相同的其他格子也会同步高亮,方便排除法填写。
  • 错误提示:输入与正确答案不符时,数字即时变红,帮助玩家识别冲突。
  • 提示功能:点击"提示"按钮,自动填入当前选中格的正确答案。
  • 计时器:游戏启动即开始计时,完成时停止并在胜利弹窗中展示用时。
  • 键盘操作:数字键 1--9 输入;Delete / Backspace / 0 清除;方向键在格子间移动;R(通过新游戏按钮)重新开始。
  • 胜利判定:所有格填满且全部正确时触发胜利遮罩,展示完成提示与用时。

游戏实现

初始化与基础设置

游戏启动时初始化 Pygame 并定义窗口尺寸、网格参数和颜色常量。

python 复制代码
pygame.init()

W, H = 540, 680
CELL = 56
GRID_X, GRID_Y = 27, 100
FPS = 60

screen = pygame.display.set_mode((W, H))
pygame.display.set_caption("数独")
clock = pygame.time.Clock()

游戏窗口为 540×680 像素,上方 100 像素留给标题与功能按钮,下方 9×9 的棋盘每格 56×56 像素,底部留出计时与说明文字区域。

颜色定义

python 复制代码
C_BG        = (248, 248, 252)   # 整体浅灰背景
C_GRID_BG   = (255, 255, 255)   # 棋盘白色底
C_LINE_THIN = (200, 200, 215)   # 细网格线
C_LINE_FAT  = (80,  80, 110)    # 粗宫格线
C_SELECT    = (200, 220, 255)   # 选中格高亮蓝
C_SAME_NUM  = (225, 235, 255)   # 同行/列/宫高亮
C_GIVEN     = (40,  40,  80)    # 题目预设数字(深蓝近黑)
C_INPUT     = (60, 100, 200)    # 玩家输入正确数字(蓝色)
C_ERROR     = (220,  50,  50)   # 输入错误数字(红色)
C_WIN       = (60, 180, 100)    # 胜利弹窗绿色

颜色体系以冷色调为主,区分"题目给定"与"玩家输入"的视觉层次,错误提示以红色醒目标出,不干扰正常状态的阅读感。

字体加载

python 复制代码
CHINESE_FONT_PATH = r"C:/Windows/Fonts/simsun.ttc"

FONT_NUM = pygame.font.Font(CHINESE_FONT_PATH, 32)   # 格内数字
FONT_SM  = pygame.font.Font(CHINESE_FONT_PATH, 17)   # 小说明文字
FONT_BTN = pygame.font.Font(CHINESE_FONT_PATH, 16)   # 按钮文字
FONT_TL  = pygame.font.Font(CHINESE_FONT_PATH, 28)   # 标题

数独生成算法

数独生成分两步:先用回溯算法生成一个完整合法解,再随机挖空指定数量的格子作为题目。

合法性验证 is_valid

python 复制代码
def is_valid(board, row, col, num):
    if num in board[row]: return False
    if num in [board[r][col] for r in range(9)]: return False
    br, bc = (row // 3) * 3, (col // 3) * 3
    for r in range(br, br + 3):
        for c in range(bc, bc + 3):
            if board[r][c] == num: return False
    return True

依次检查同行、同列、同宫格三个约束,任一冲突立即返回 False,保证填入的每个数字合法。

回溯求解 solve

python 复制代码
def solve(board):
    for r in range(9):
        for c in range(9):
            if board[r][c] == 0:
                nums = list(range(1, 10))
                random.shuffle(nums)
                for n in nums:
                    if is_valid(board, r, c, n):
                        board[r][c] = n
                        if solve(board): return True
                        board[r][c] = 0
                return False
    return True

标准回溯:逐格扫描,遇到空格则随机打乱 1--9 的顺序后逐一尝试;若某格所有候选数均无法成立则回退。对候选数 random.shuffle 是关键------正是这一步保证每次生成的完整解都不同,从而产生随机题目。

题目生成 generate_puzzle

python 复制代码
def generate_puzzle(remove_count):
    board = [[0] * 9 for _ in range(9)]
    solve(board)
    solution = copy.deepcopy(board)
    cells = [(r, c) for r in range(9) for c in range(9)]
    random.shuffle(cells)
    for r, c in cells[:remove_count]:
        board[r][c] = 0
    return board, solution

生成完整解后保留一份深拷贝作为 solution(用于错误校验和提示功能),再随机选取 remove_count 个格子置零,形成玩家看到的题目。solution 贯穿整个游戏生命周期,是所有校验逻辑的基准。


核心交互逻辑

状态变量

python 复制代码
board, solution, given = None, None, None
selected = None
errors = [[False] * 9 for _ in range(9)]
won = False
start_time = 0
elapsed = 0

given 是与 board 等大的布尔矩阵,记录哪些格子是题目预设(不可修改)。errors 同样是布尔矩阵,驱动红色字体的显示。selected 存储当前选中格的 (row, col),为 None 时表示无选中状态。

新游戏 new_game

python 复制代码
def new_game():
    remove = DIFFICULTIES[diff_keys[current_diff]]
    board, solution = generate_puzzle(remove)
    given = [[board[r][c] != 0 for c in range(9)] for r in range(9)]
    selected = None
    errors = [[False] * 9 for _ in range(9)]
    won = False
    start_time = time.time()
    elapsed = 0

切换难度或点击"新游戏"时调用,重置所有状态并重新生成题目,计时器同步清零。

错误校验 check_errors

python 复制代码
def check_errors():
    all_filled = True
    for r in range(9):
        for c in range(9):
            if board[r][c] == 0:
                all_filled = False
                errors[r][c] = False
            else:
                errors[r][c] = (board[r][c] != solution[r][c])
    if all_filled and not any(errors[r][c] for r in range(9) for c in range(9)):
        won = True

每次玩家输入数字后立即调用。逐格比对 boardsolution,空格不标记错误;若全格填满且无误,则将 won 置为 True,触发胜利流程。

键盘输入处理

python 复制代码
if event.type == pygame.KEYDOWN and selected and not won:
    r, c = selected
    if not given[r][c]:
        if event.key in range(pygame.K_1, pygame.K_9 + 1):
            board[r][c] = event.key - pygame.K_0
            check_errors()
        elif event.key in (pygame.K_0, pygame.K_DELETE, pygame.K_BACKSPACE):
            board[r][c] = 0
            errors[r][c] = False
    # 方向键移动选中格
    if event.key == pygame.K_UP    and r > 0: selected = (r - 1, c)
    if event.key == pygame.K_DOWN  and r < 8: selected = (r + 1, c)
    if event.key == pygame.K_LEFT  and c > 0: selected = (r, c - 1)
    if event.key == pygame.K_RIGHT and c < 8: selected = (r, c + 1)

given[r][c] 保护预设格不被修改。数字键通过 event.key - pygame.K_0 直接转换为整数值写入棋盘;Delete / Backspace / 0 均执行清除操作,同时重置该格的错误状态。方向键移动选中格,使键盘党无需触碰鼠标也能流畅操作。


绘制方法

关联格高亮

python 复制代码
if selected:
    sr, sc = selected
    sv = board[sr][sc]
    for r in range(9):
        for c in range(9):
            rx = GRID_X + c * CELL
            ry = GRID_Y + r * CELL
            if r == sr and c == sc:
                pygame.draw.rect(screen, C_SELECT, (rx, ry, CELL, CELL))
            elif r == sr or c == sc or (r // 3 == sr // 3 and c // 3 == sc // 3):
                pygame.draw.rect(screen, C_SAME_NUM, (rx, ry, CELL, CELL))
            elif sv and board[r][c] == sv:
                pygame.draw.rect(screen, (210, 230, 210), (rx, ry, CELL, CELL))

高亮分三层:选中格自身(深蓝)→ 同行/列/宫(浅蓝)→ 相同数字(浅绿)。三者互不覆盖,优先级由 if/elif 保证。这是数独 UI 的核心辅助功能,大幅降低玩家的视觉搜索负担。

数字渲染

python 复制代码
for r in range(9):
    for c in range(9):
        v = board[r][c]
        if v:
            color = C_GIVEN if given[r][c] else (C_ERROR if errors[r][c] else C_INPUT)
            t = FONT_NUM.render(str(v), True, color)
            cx = GRID_X + c * CELL + CELL // 2
            cy = GRID_Y + r * CELL + CELL // 2
            screen.blit(t, t.get_rect(center=(cx, cy)))

三色区分:深蓝近黑(题目预设)→ 蓝色(玩家正确输入)→ 红色(输入有误)。t.get_rect(center=...) 确保数字精确居中于格子内,无论字形宽窄都不偏移。

网格线绘制

python 复制代码
for i in range(10):
    lw = 3 if i % 3 == 0 else 1
    lc = C_LINE_FAT if i % 3 == 0 else C_LINE_THIN
    pygame.draw.line(screen, lc, (GRID_X + i * CELL, GRID_Y),
                                  (GRID_X + i * CELL, GRID_Y + 9 * CELL), lw)
    pygame.draw.line(screen, lc, (GRID_X, GRID_Y + i * CELL),
                                  (GRID_X + 9 * CELL, GRID_Y + i * CELL), lw)

i % 3 == 0 的线条加粗(3px)并加深颜色,将 9×9 棋盘自然分割为 9 个 3×3 宫格,与标准数独印刷风格一致。

计时显示

python 复制代码
mins = int(elapsed) // 60
secs = int(elapsed) % 60
t_str = f"⏱ {mins:02d}:{secs:02d}"
tt = FONT_SM.render(t_str, True, C_TIME)
screen.blit(tt, tt.get_rect(centerx=W // 2, y=GRID_Y + 9 * CELL + 12))

elapsed 在每帧由 time.time() - start_time 更新,胜利后停止更新,定格记录完成用时。:02d 格式化保证分秒始终两位显示。

胜利弹窗

python 复制代码
if won:
    ov = pygame.Surface((W, H), pygame.SRCALPHA)
    ov.fill((0, 0, 0, 100))
    screen.blit(ov, (0, 0))
    box = pygame.Rect(W // 2 - 160, H // 2 - 70, 320, 160)
    pygame.draw.rect(screen, (240, 255, 240), box, border_radius=16)
    pygame.draw.rect(screen, C_WIN, box, 3, border_radius=16)
    wt = FONT_TL.render("🎉 完成!", True, C_WIN)
    screen.blit(wt, wt.get_rect(centerx=W // 2, centery=H // 2 - 20))
    mt = FONT_SM.render(f"用时 {mins:02d}:{secs:02d}", True, C_TITLE)
    screen.blit(mt, mt.get_rect(centerx=W // 2, centery=H // 2 + 20))

胜利时先绘制半透明黑色全屏遮罩(alpha=100),再在中央绘制带圆角、绿色描边的弹窗,展示完成文字与用时。pygame.SRCALPHA Surface 配合 fill 中的第四个 alpha 分量,是 Pygame 中实现半透明遮罩的标准做法。

绘制顺序为:背景 → 按钮 → 高亮格 → 数字 → 网格线 → 计时 → 胜利遮罩,严格保证层次正确。

主循环 main

python 复制代码
while True:
    mx, my = pygame.mouse.get_pos()
    if not won:
        elapsed = time.time() - start_time
    # 事件处理 ...
    # 绘制 ...
    pygame.display.flip()
    clock.tick(FPS)

以固定 60 FPS 驱动主循环,保证界面响应流畅。数独无需像贪吃蛇那样用帧率控制游戏节奏,因此全程维持恒定帧率即可。


全部代码

python 复制代码
"""
数独 --- 随机生成 + 错误提示 + 难度分级
操作:鼠标点击选格,键盘数字输入,Delete/0清除
难度:Easy(36空) / Medium(46空) / Hard(54空)
"""

import pygame
import sys
import random
import copy
import time

pygame.init()

W, H = 540, 680
CELL = 56
GRID_X, GRID_Y = 27, 100
FPS = 60

C_BG        = (248, 248, 252)
C_GRID_BG   = (255, 255, 255)
C_LINE_THIN = (200, 200, 215)
C_LINE_FAT  = (80,  80, 110)
C_SELECT    = (200, 220, 255)
C_SAME_NUM  = (225, 235, 255)
C_GIVEN     = (40,  40,  80)
C_INPUT     = (60, 100, 200)
C_ERROR     = (220,  50,  50)
C_BTN       = (70,  90, 160)
C_BTN_TXT   = (255, 255, 255)
C_TITLE     = (40,  40,  80)
C_TIME      = (100, 100, 140)
C_WIN       = (60, 180, 100)

CHINESE_FONT_PATH = r"C:/Windows/Fonts/simsun.ttc"

FONT_NUM  = pygame.font.Font(CHINESE_FONT_PATH, 32)
FONT_SM   = pygame.font.Font(CHINESE_FONT_PATH, 17)
FONT_BTN  = pygame.font.Font(CHINESE_FONT_PATH, 16)
FONT_TL   = pygame.font.Font(CHINESE_FONT_PATH, 28)

DIFFICULTIES = {"简单": 36, "中等": 46, "困难": 54}

# ── 数独生成 ──────────────────────────────────────────────────────────
def is_valid(board, row, col, num):
    if num in board[row]: return False
    if num in [board[r][col] for r in range(9)]: return False
    br, bc = (row//3)*3, (col//3)*3
    for r in range(br, br+3):
        for c in range(bc, bc+3):
            if board[r][c] == num: return False
    return True

def solve(board):
    for r in range(9):
        for c in range(9):
            if board[r][c] == 0:
                nums = list(range(1,10))
                random.shuffle(nums)
                for n in nums:
                    if is_valid(board, r, c, n):
                        board[r][c] = n
                        if solve(board): return True
                        board[r][c] = 0
                return False
    return True

def generate_puzzle(remove_count):
    board = [[0]*9 for _ in range(9)]
    solve(board)
    solution = copy.deepcopy(board)
    cells = [(r,c) for r in range(9) for c in range(9)]
    random.shuffle(cells)
    for r,c in cells[:remove_count]:
        board[r][c] = 0
    return board, solution

# ── 主循环 ────────────────────────────────────────────────────────────
def main():
    screen = pygame.display.set_mode((W, H))
    pygame.display.set_caption("数独")
    clock = pygame.time.Clock()

    diff_keys = list(DIFFICULTIES.keys())
    current_diff = 1  # 中等
    board, solution, given = None, None, None
    selected = None
    errors = [[False]*9 for _ in range(9)]
    won = False
    start_time = 0
    elapsed = 0

    def new_game():
        nonlocal board, solution, given, selected, errors, won, start_time, elapsed
        remove = DIFFICULTIES[diff_keys[current_diff]]
        board, solution = generate_puzzle(remove)
        given = [[board[r][c] != 0 for c in range(9)] for r in range(9)]
        selected = None
        errors = [[False]*9 for _ in range(9)]
        won = False
        start_time = time.time()
        elapsed = 0

    new_game()

    def check_errors():
        nonlocal errors, won
        all_filled = True
        for r in range(9):
            for c in range(9):
                if board[r][c] == 0:
                    all_filled = False
                    errors[r][c] = False
                else:
                    errors[r][c] = (board[r][c] != solution[r][c])
        if all_filled and not any(errors[r][c] for r in range(9) for c in range(9)):
            won = True

    def cell_at(mx, my):
        gx = mx - GRID_X
        gy = my - GRID_Y
        if 0 <= gx < 9*CELL and 0 <= gy < 9*CELL:
            return gy // CELL, gx // CELL
        return None

    while True:
        mx, my = pygame.mouse.get_pos()
        if not won:
            elapsed = time.time() - start_time

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit(); sys.exit()

            if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
                # 难度按钮
                for i, dk in enumerate(diff_keys):
                    bx = 27 + i * 100
                    br = pygame.Rect(bx, 58, 90, 32)
                    if br.collidepoint(mx, my):
                        nonlocal_ci = i
                        current_diff_ref = [current_diff]
                        current_diff_ref[0] = i
                        # 用闭包技巧
                        globals()['_cd'] = i
                        break

                # 新游戏按钮
                ng_btn = pygame.Rect(360, 58, 80, 32)
                if ng_btn.collidepoint(mx, my):
                    new_game()

                # 提示按钮
                hint_btn = pygame.Rect(445, 58, 70, 32)
                if hint_btn.collidepoint(mx, my) and selected and not given[selected[0]][selected[1]]:
                    r, c = selected
                    board[r][c] = solution[r][c]
                    errors[r][c] = False
                    check_errors()

                # 选格
                cell = cell_at(mx, my)
                if cell:
                    selected = cell

            if event.type == pygame.KEYDOWN and selected and not won:
                r, c = selected
                if not given[r][c]:
                    if event.key in range(pygame.K_1, pygame.K_9+1):
                        board[r][c] = event.key - pygame.K_0
                        check_errors()
                    elif event.key in (pygame.K_0, pygame.K_DELETE, pygame.K_BACKSPACE):
                        board[r][c] = 0
                        errors[r][c] = False
                # 方向键移动
                if event.key == pygame.K_UP    and r > 0: selected = (r-1, c)
                if event.key == pygame.K_DOWN  and r < 8: selected = (r+1, c)
                if event.key == pygame.K_LEFT  and c > 0: selected = (r, c-1)
                if event.key == pygame.K_RIGHT and c < 8: selected = (r, c+1)

        # 难度切换(外部变量技巧)
        if '_cd' in globals():
            current_diff = globals()['_cd']
            del globals()['_cd']
            new_game()

        # ── 绘制 ──────────────────────────────────────────────────────
        screen.fill(C_BG)

        title = FONT_TL.render("数  独", True, C_TITLE)
        screen.blit(title, title.get_rect(centerx=W//2, y=14))

        # 难度按钮
        for i, dk in enumerate(diff_keys):
            bx = 27 + i * 100
            br = pygame.Rect(bx, 58, 90, 32)
            col = (40, 60, 130) if i == current_diff else C_BTN
            pygame.draw.rect(screen, col, br, border_radius=8)
            t = FONT_BTN.render(dk, True, C_BTN_TXT)
            screen.blit(t, t.get_rect(center=br.center))

        # 新游戏
        ng_btn = pygame.Rect(360, 58, 80, 32)
        pygame.draw.rect(screen, (100, 160, 80), ng_btn, border_radius=8)
        screen.blit(FONT_BTN.render("新游戏", True, C_BTN_TXT),
                    FONT_BTN.render("新游戏", True, C_BTN_TXT).get_rect(center=ng_btn.center))

        # 提示
        hint_btn = pygame.Rect(445, 58, 70, 32)
        pygame.draw.rect(screen, (180, 120, 40), hint_btn, border_radius=8)
        screen.blit(FONT_BTN.render("提示", True, C_BTN_TXT),
                    FONT_BTN.render("提示", True, C_BTN_TXT).get_rect(center=hint_btn.center))

        # 网格背景
        grid_rect = pygame.Rect(GRID_X, GRID_Y, 9*CELL, 9*CELL)
        pygame.draw.rect(screen, C_GRID_BG, grid_rect)

        # 高亮选中格周围同数字
        if selected:
            sr, sc = selected
            sv = board[sr][sc]
            for r in range(9):
                for c in range(9):
                    rx = GRID_X + c*CELL
                    ry = GRID_Y + r*CELL
                    if r == sr and c == sc:
                        pygame.draw.rect(screen, C_SELECT, (rx,ry,CELL,CELL))
                    elif r == sr or c == sc or (r//3==sr//3 and c//3==sc//3):
                        pygame.draw.rect(screen, C_SAME_NUM, (rx,ry,CELL,CELL))
                    elif sv and board[r][c] == sv:
                        pygame.draw.rect(screen, (210,230,210), (rx,ry,CELL,CELL))

        # 数字
        for r in range(9):
            for c in range(9):
                v = board[r][c]
                if v:
                    color = C_GIVEN if given[r][c] else (C_ERROR if errors[r][c] else C_INPUT)
                    t = FONT_NUM.render(str(v), True, color)
                    cx = GRID_X + c*CELL + CELL//2
                    cy = GRID_Y + r*CELL + CELL//2
                    screen.blit(t, t.get_rect(center=(cx,cy)))

        # 网格线
        for i in range(10):
            lw = 3 if i % 3 == 0 else 1
            lc = C_LINE_FAT if i % 3 == 0 else C_LINE_THIN
            pygame.draw.line(screen, lc, (GRID_X+i*CELL, GRID_Y), (GRID_X+i*CELL, GRID_Y+9*CELL), lw)
            pygame.draw.line(screen, lc, (GRID_X, GRID_Y+i*CELL), (GRID_X+9*CELL, GRID_Y+i*CELL), lw)

        # 计时
        mins = int(elapsed) // 60
        secs = int(elapsed) % 60
        t_str = f"⏱ {mins:02d}:{secs:02d}"
        tt = FONT_SM.render(t_str, True, C_TIME)
        screen.blit(tt, tt.get_rect(centerx=W//2, y=GRID_Y + 9*CELL + 12))

        # 提示文字
        ht = FONT_SM.render("点击格子 → 数字键输入 | 方向键移动 | Del 清除", True, C_TIME)
        screen.blit(ht, ht.get_rect(centerx=W//2, y=GRID_Y + 9*CELL + 36))

        # 胜利
        if won:
            ov = pygame.Surface((W, H), pygame.SRCALPHA)
            ov.fill((0,0,0,100))
            screen.blit(ov, (0,0))
            box = pygame.Rect(W//2-160, H//2-70, 320, 160)
            pygame.draw.rect(screen, (240,255,240), box, border_radius=16)
            pygame.draw.rect(screen, C_WIN, box, 3, border_radius=16)
            wt = FONT_TL.render("🎉 完成!", True, C_WIN)
            screen.blit(wt, wt.get_rect(centerx=W//2, centery=H//2-20))
            mt = FONT_SM.render(f"用时 {mins:02d}:{secs:02d}", True, C_TITLE)
            screen.blit(mt, mt.get_rect(centerx=W//2, centery=H//2+20))

        pygame.display.flip()
        clock.tick(FPS)

if __name__ == "__main__":
    main()

附:文章说明

本文仅为个人理解,若有不当之处,欢迎指正~

相关推荐
吠品1 小时前
处理 Python 类继承中那些变来变去的初始化参数
linux·前端·python
人道领域1 小时前
【LeetCode刷题日记】90.子集Ⅱ--- 归纳题解
java·开发语言·leetcode
会Tk矩阵群控的小木1 小时前
小红书矩阵软件:基于Python+ADB的多设备批量管理自动化脚本实战
运维·python·adb·矩阵·自动化·新媒体运营·个人开发
ch.ju2 小时前
Java Programming Chapter 4——Characteristics of inheritance
java·开发语言
复园电子2 小时前
企业PDF批量盖章开发集成指南:API对接OA/LIMS系统,高并发落地实战
开发语言·python·pdf
SunnyDays10112 小时前
如何使用 C# 自动调整 Excel 行高和列宽
开发语言·c#·excel
石山代码2 小时前
类型限定符的底层实现原理是什么?
python
雾沉川2 小时前
PyCharm 2025.2 完整安装与配置技术教程
ide·python·pycharm
a诠释淡然2 小时前
C++模板元编程—现代C++的黑魔法
开发语言·c++