環境準備
首先安裝依賴庫,打開終端執行以下命令:
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()