博客 / 詳情

返回

Python 小遊戲實戰:打造視覺精美的數獨小遊戲

環境準備

首先安裝依賴庫,打開終端執行以下命令:

pip install pygame numpy

核心模塊實現(分步講解)

項目將分為3個核心模塊:數獨謎題生成器Pygame可視化界面交互邏輯處理。我們逐一實現,最後整合為完整代碼。

模塊1:數獨謎題生成器

生成合法的數獨謎題是基礎,核心思路是:先生成一個完整的合法數獨矩陣,再通過“挖空”的方式生成不同難度的謎題(挖空越多,難度越高)。

1.1 生成完整數獨矩陣(回溯法)

使用回溯法填充9x9矩陣,確保每行、每列、每個3x3宮格內數字不重複:

import numpy as np
import random

def is_valid(grid, row, col, num):
    """判斷數字num在grid[row][col]位置是否合法"""
    # 檢查行
    if num in grid[row]:
        return False
    # 檢查列
    if num in grid[:, col]:
        return False
    # 檢查3x3宮格
    start_row, start_col = 3 * (row // 3), 3 * (col // 3)
    if num in grid[start_row:start_row+3, start_col:start_col+3]:
        return False
    return True

def fill_grid(grid):
    """遞歸填充數獨矩陣(回溯法)"""
    for row in range(9):
        for col in range(9):
            if grid[row][col] == 0:
                # 隨機打亂1-9的順序,避免每次生成相同矩陣
                nums = random.sample(range(1, 10), 9)
                for num in nums:
                    if is_valid(grid, row, col, num):
                        grid[row][col] = num
                        # 遞歸填充下一個位置
                        if fill_grid(grid).any():
                            return grid
                        # 回溯:當前數字無效,重置為0
                        grid[row][col] = 0
                # 所有數字都無效,返回空矩陣(觸發回溯)
                return np.zeros((9, 9))
    # 矩陣填充完成,返回結果
    return grid

def generate_full_grid():
    """生成完整的合法數獨矩陣"""
    grid = np.zeros((9, 9), dtype=int)
    return fill_grid(grid)

1.2 挖空生成謎題(按難度區分)

通過隨機挖空完整矩陣的單元格,生成不同難度的謎題。挖空數量越多,難度越高(需確保至少有一個唯一解,入門級可簡化為“控制挖空數”):

def generate_puzzle(full_grid, difficulty="easy"):
    """
    挖空生成謎題
    :param full_grid: 完整數獨矩陣
    :param difficulty: 難度(easy/medium/hard)
    :return: 謎題矩陣、答案矩陣
    """
    puzzle = full_grid.copy()
    # 按難度設置挖空數量(總數81)
    empty_counts = {
        "easy": 30,    # 簡單:保留51個數字
        "medium": 40,  # 中等:保留41個數字
        "hard": 50     # 困難:保留31個數字
    }
    empty_count = empty_counts[difficulty]
    
    # 隨機選擇empty_count個位置挖空(設為0)
    empty_positions = random.sample([(r, c) for r in range(9) for c in range(9)], empty_count)
    for r, c in empty_positions:
        puzzle[r][c] = 0
    
    return puzzle, full_grid

模塊2:Pygame可視化界面

視覺精美是本次項目的重點,我們將通過顏色搭配、格子分層、字體設計、背景美化等方式提升界面質感。

2.1 初始化Pygame與全局配置

首先設置窗口大小、顏色方案、字體等全局參數(顏色搭配參考“極簡風”,避免過於鮮豔):

import pygame
from pygame.locals import *

# 初始化Pygame
pygame.init()

# ---------------------- 全局配置 ----------------------
# 窗口大小(9個格子 + 邊距,每個格子50px,邊距25px)
WINDOW_SIZE = 500
CELL_SIZE = 50
MARGIN = 25
GRID_SIZE = CELL_SIZE * 9 + MARGIN * 2  # 實際網格大小:50*9 +25*2=500

# 顏色方案(RGB)
COLOR_BG = (245, 245, 245)          # 背景色(淺灰)
COLOR_GRID_LIGHT = (200, 200, 200)  # 細格子線(淺灰)
COLOR_GRID_DARK = (100, 100, 100)   # 粗格子線(深灰,分隔3x3宮格)
COLOR_CELL_FILLED = (50, 50, 50)    # 預設數字顏色(深黑)
COLOR_CELL_INPUT = (0, 102, 204)    # 用户輸入數字顏色(藍色)
COLOR_CELL_ERROR = (220, 50, 50)    # 錯誤數字顏色(紅色)
COLOR_SELECTED = (173, 216, 230)    # 選中格子背景(淺藍)
COLOR_BUTTON = (0, 153, 204)        # 按鈕顏色(深藍)
COLOR_BUTTON_HOVER = (0, 102, 153)  # 按鈕hover顏色(深一點的藍)
COLOR_TEXT = (255, 255, 255)        # 按鈕文字顏色(白色)

# 字體(使用系統字體,需確保存在)
FONT_NUM = pygame.font.SysFont("Arial", 28, bold=True)  # 數字字體
FONT_BUTTON = pygame.font.SysFont("Arial", 16, bold=True)# 按鈕字體
FONT_TIMER = pygame.font.SysFont("Arial", 20, bold=True) # 計時器字體

# 創建窗口
screen = pygame.display.set_mode((GRID_SIZE, GRID_SIZE + 80))  # 底部留80px放按鈕和計時器
pygame.display.set_caption("Python 數獨小遊戲")

# 加載背景圖(可選,增強視覺效果)
try:
    bg_img = pygame.image.load("sudoku_bg.jpg").convert()
    bg_img = pygame.transform.scale(bg_img, (GRID_SIZE, GRID_SIZE + 80))
except:
    # 若沒有背景圖,用純色背景替代
    bg_img = None

2.2 繪製數獨棋盤

繪製9x9網格,重點區分“細格子線”(分隔單個單元格)和“粗格子線”(分隔3x3宮格),確保視覺層次清晰:

def draw_grid():
    """繪製數獨網格"""
    # 繪製細格子線(分隔單個單元格)
    for i in range(10):
        line_width = 1
        color = COLOR_GRID_LIGHT
        # 每3行/列繪製粗格子線(分隔3x3宮格)
        if i % 3 == 0:
            line_width = 3
            color = COLOR_GRID_DARK
        
        # 繪製橫線
        y = MARGIN + i * CELL_SIZE
        pygame.draw.line(screen, color, (MARGIN, y), (MARGIN + 9 * CELL_SIZE, y), line_width)
        # 繪製豎線
        x = MARGIN + i * CELL_SIZE
        pygame.draw.line(screen, color, (x, MARGIN), (x, MARGIN + 9 * CELL_SIZE), line_width)

2.3 繪製數字與選中狀態

根據謎題矩陣和用户輸入,繪製數字(區分“預設數字”和“用户輸入數字”),並高亮當前選中的格子:

def draw_numbers(puzzle, user_input, selected_pos):
    """
    繪製數字
    :param puzzle: 謎題矩陣(預設數字)
    :param user_input: 用户輸入矩陣(用户填寫的數字)
    :param selected_pos: 選中格子的位置 (row, col),None表示無選中
    """
    for row in range(9):
        for col in range(9):
            # 計算單元格中心座標
            x = MARGIN + col * CELL_SIZE + CELL_SIZE // 2
            y = MARGIN + row * CELL_SIZE + CELL_SIZE // 2
            # 文本居中偏移(Pygame的text.get_rect()獲取文本尺寸)
            text_rect = pygame.Rect(0, 0, CELL_SIZE, CELL_SIZE)
            
            # 1. 繪製選中格子的背景
            if selected_pos == (row, col):
                selected_rect = pygame.Rect(
                    MARGIN + col * CELL_SIZE,
                    MARGIN + row * CELL_SIZE,
                    CELL_SIZE,
                    CELL_SIZE
                )
                pygame.draw.rect(screen, COLOR_SELECTED, selected_rect)
            
            # 2. 繪製數字(優先顯示用户輸入,再顯示預設數字)
            if user_input[row][col] != 0:
                # 用户輸入數字:檢查是否合法(合法藍色,非法紅色)
                num = user_input[row][col]
                color = COLOR_CELL_INPUT if is_valid(puzzle + user_input, row, col, num) else COLOR_CELL_ERROR
                text = FONT_NUM.render(str(num), True, color)
            elif puzzle[row][col] != 0:
                # 預設數字:深黑色
                num = puzzle[row][col]
                text = FONT_NUM.render(str(num), True, COLOR_CELL_FILLED)
            else:
                # 空單元格:不繪製
                continue
            
            # 文本居中繪製
            text_rect.center = (x, y)
            screen.blit(text, text.get_rect(center=text_rect.center))

2.4 繪製按鈕與計時器

在窗口底部添加“重置”“提示”“檢查答案”按鈕,並顯示解題時間:

def draw_buttons(hovered_button):
    """
    繪製按鈕
    :param hovered_button: 當前hover的按鈕("reset"/"hint"/"check",None表示無)
    """
    button_width = 120
    button_height = 40
    y = GRID_SIZE + 20  # 按鈕y座標(在網格下方20px)
    
    # 1. 重置按鈕(左側)
    reset_rect = pygame.Rect(MARGIN, y, button_width, button_height)
    reset_color = COLOR_BUTTON_HOVER if hovered_button == "reset" else COLOR_BUTTON
    pygame.draw.rect(screen, reset_color, reset_rect, border_radius=5)  # 圓角按鈕
    reset_text = FONT_BUTTON.render("重置遊戲", True, COLOR_TEXT)
    screen.blit(reset_text, reset_text.get_rect(center=reset_rect.center))
    
    # 2. 提示按鈕(中間)
    hint_rect = pygame.Rect(GRID_SIZE//2 - button_width//2, y, button_width, button_height)
    hint_color = COLOR_BUTTON_HOVER if hovered_button == "hint" else COLOR_BUTTON
    pygame.draw.rect(screen, hint_color, hint_rect, border_radius=5)
    hint_text = FONT_BUTTON.render("提示", True, COLOR_TEXT)
    screen.blit(hint_text, hint_text.get_rect(center=hint_rect.center))
    
    # 3. 檢查答案按鈕(右側)
    check_rect = pygame.Rect(GRID_SIZE - MARGIN - button_width, y, button_width, button_height)
    check_color = COLOR_BUTTON_HOVER if hovered_button == "check" else COLOR_BUTTON
    pygame.draw.rect(screen, check_color, check_rect, border_radius=5)
    check_text = FONT_BUTTON.render("檢查答案", True, COLOR_TEXT)
    screen.blit(check_text, check_text.get_rect(center=check_rect.center))
    
    return reset_rect, hint_rect, check_rect

def draw_timer(elapsed_time):
    """繪製計時器(顯示已用時間)"""
    # 格式化時間為 分:秒
    minutes = elapsed_time // 60
    seconds = elapsed_time % 60
    timer_text = FONT_TIMER.render(f"用時: {minutes:02d}:{seconds:02d}", True, COLOR_CELL_FILLED)
    # 繪製在按鈕上方
    timer_rect = timer_text.get_rect(center=(GRID_SIZE//2, GRID_SIZE + 10))
    screen.blit(timer_text, timer_rect)

模塊3:交互邏輯處理

實現鼠標點擊選中格子、鍵盤輸入數字、按鈕功能(重置/提示/檢查答案)等核心交互。

3.1 鼠標事件處理

  • 點擊格子:將鼠標座標轉換為網格的行/列,選中對應格子。
  • 點擊按鈕:觸發重置、提示、檢查答案功能。

    def get_clicked_cell(mouse_pos):
      """將鼠標座標轉換為格子的(row, col),無效返回None"""
      x, y = mouse_pos
      # 檢查是否在網格區域內
      if MARGIN <= x <= MARGIN + 9 * CELL_SIZE and MARGIN <= y <= MARGIN + 9 * CELL_SIZE:
          col = (x - MARGIN) // CELL_SIZE
          row = (y - MARGIN) // CELL_SIZE
          return (row, col)
      return None
    
    def handle_mouse_event(event, selected_pos, reset_rect, hint_rect, check_rect, puzzle, full_grid):
      """處理鼠標事件(點擊格子、按鈕)"""
      mouse_pos = pygame.mouse.get_pos()
      if event.type == MOUSEBUTTONDOWN:
          # 1. 檢查是否點擊格子
          clicked_cell = get_clicked_cell(mouse_pos)
          if clicked_cell:
              row, col = clicked_cell
              # 只有謎題中的空單元格可選中(預設數字不可修改)
              if puzzle[row][col] == 0:
                  return {"selected_pos": clicked_cell, "action": None}
          
          # 2. 檢查是否點擊按鈕
          if reset_rect.collidepoint(mouse_pos):
              return {"selected_pos": selected_pos, "action": "reset"}
          elif hint_rect.collidepoint(mouse_pos):
              return {"selected_pos": selected_pos, "action": "hint"}
          elif check_rect.collidepoint(mouse_pos):
              return {"selected_pos": selected_pos, "action": "check"}
      
      # 鼠標hover:返回當前hover的按鈕
      if reset_rect.collidepoint(mouse_pos):
          return {"selected_pos": selected_pos, "action": None, "hovered_button": "reset"}
      elif hint_rect.collidepoint(mouse_pos):
          return {"selected_pos": selected_pos, "action": None, "hovered_button": "hint"}
      elif check_rect.collidepoint(mouse_pos):
          return {"selected_pos": selected_pos, "action": None, "hovered_button": "check"}
      else:
          return {"selected_pos": selected_pos, "action": None, "hovered_button": None}

3.2 鍵盤事件處理

  • 數字鍵(1-9):在選中格子中填入對應數字。
  • 退格鍵/刪除鍵:清除選中格子的數字。

    def handle_key_event(event, selected_pos, user_input):
      """處理鍵盤事件(輸入數字、刪除數字)"""
      if selected_pos is None:
          return user_input  # 無選中格子,不處理
      
      row, col = selected_pos
      if event.type == KEYDOWN:
          # 輸入數字(1-9)
          if event.unicode.isdigit() and 1 <= int(event.unicode) <= 9:
              user_input[row][col] = int(event.unicode)
          # 退格鍵/刪除鍵:清除數字
          elif event.key in (K_BACKSPACE, K_DELETE):
              user_input[row][col] = 0
      return user_input

3.3 按鈕功能實現

  • 重置遊戲:重新生成當前難度的謎題,清空用户輸入和計時器。
  • 提示功能:在用户未填寫的格子中,顯示一個正確答案(避免過度提示,每次提示後冷卻1秒)。
  • 檢查答案:對比用户輸入與完整矩陣,判斷是否正確,彈出結果提示。

    def reset_game(difficulty="easy"):
      """重置遊戲:生成新謎題,清空用户輸入,重置計時器"""
      full_grid = generate_full_grid()
      puzzle, answer = generate_puzzle(full_grid, difficulty)
      user_input = np.zeros((9, 9), dtype=int)
      start_time = pygame.time.get_ticks()  # 重置開始時間
      return puzzle, answer, user_input, start_time
    
    def give_hint(puzzle, user_input, answer):
      """給出提示:在空單元格中填入一個正確答案"""
      # 找到所有空單元格(謎題為0且用户未輸入)
      empty_cells = [(r, c) for r in range(9) for c in range(9) if puzzle[r][c] == 0 and user_input[r][c] == 0]
      if empty_cells:
          r, c = random.choice(empty_cells)
          user_input[r][c] = answer[r][c]
      return user_input
    
    def check_answer(user_input, answer):
      """檢查答案是否正確"""
      return np.array_equal(user_input + puzzle, answer)  # 謎題+用户輸入=完整矩陣
    
    def show_result(is_correct):
      """彈出結果提示框(正確/錯誤)"""
      result_width = 250
      result_height = 120
      result_rect = pygame.Rect(GRID_SIZE//2 - result_width//2, GRID_SIZE//2 - result_height//2, result_width, result_height)
      # 繪製提示框背景
      pygame.draw.rect(screen, COLOR_BG, result_rect)
      pygame.draw.rect(screen, COLOR_GRID_DARK, result_rect, width=3)
      # 繪製提示文字
      font_result = pygame.font.SysFont("Arial", 24, bold=True)
      if is_correct:
          text1 = font_result.render("恭喜!", True, (0, 153, 0))
          text2 = font_result.render("答案正確!", True, (0, 153, 0))
      else:
          text1 = font_result.render("加油!", True, COLOR_CELL_ERROR)
          text2 = font_result.render("答案還有錯誤", True, COLOR_CELL_ERROR)
      # 居中繪製文字
      screen.blit(text1, text1.get_rect(center=(GRID_SIZE//2, GRID_SIZE//2 - 20)))
      screen.blit(text2, text2.get_rect(center=(GRID_SIZE//2, GRID_SIZE//2 + 20)))
      # 更新屏幕顯示
      pygame.display.flip()
      # 停留2秒後關閉提示框
      pygame.time.wait(2000)

完整代碼整合與運行

將上述模塊整合,添加主循環處理事件、更新界面,實現完整遊戲流程:

def main():
    # 初始化遊戲狀態
    difficulty = "easy"
    puzzle, answer, user_input, start_time = reset_game(difficulty)
    selected_pos = None  # 當前選中的格子
    hovered_button = None  # 當前hover的按鈕
    running = True
    clock = pygame.time.Clock()  # 控制幀率
    
    while running:
        # 計算已用時間(毫秒轉秒)
        elapsed_time = (pygame.time.get_ticks() - start_time) // 1000
        
        # 1. 事件處理
        for event in pygame.event.get():
            if event.type == QUIT:
                running = False
            
            # 處理鼠標事件
            mouse_result = handle_mouse_event(event, selected_pos, reset_rect, hint_rect, check_rect, puzzle, answer)
            selected_pos = mouse_result["selected_pos"]
            action = mouse_result["action"]
            if "hovered_button" in mouse_result:
                hovered_button = mouse_result["hovered_button"]
            
            # 處理鍵盤事件
            user_input = handle_key_event(event, selected_pos, user_input)
            
            # 處理按鈕動作
            if action == "reset":
                puzzle, answer, user_input, start_time = reset_game(difficulty)
                selected_pos = None
            elif action == "hint":
                user_input = give_hint(puzzle, user_input, answer)
            elif action == "check":
                is_correct = check_answer(user_input, answer)
                show_result(is_correct)
        
        # 2. 繪製界面
        # 繪製背景(優先繪製背景圖,無則用純色)
        if bg_img:
            screen.blit(bg_img, (0, 0))
        else:
            screen.fill(COLOR_BG)
        
        # 繪製網格、數字、按鈕、計時器
        draw_grid()
        draw_numbers(puzzle, user_input, selected_pos)
        reset_rect, hint_rect, check_rect = draw_buttons(hovered_button)
        draw_timer(elapsed_time)
        
        # 3. 更新屏幕
        pygame.display.flip()
        clock.tick(30)  # 限制幀率為30,避免CPU佔用過高
    
    # 退出遊戲
    pygame.quit()

if __name__ == "__main__":
    main()
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.