Python通過Pygame實現(xiàn)一個功能完整的數(shù)獨游戲
項目概述
本文通過 Pygame 實現(xiàn)一個功能完整的數(shù)獨游戲(Sudoku)。
游戲隨機生成合法數(shù)獨題目,玩家通過鼠標點擊選格、鍵盤輸入數(shù)字來完成填寫,支持三種難度等級,并提供實時錯誤高亮、關(guān)聯(lián)格著色、計時、提示等功能。填滿所有格子且無誤即為通關(guān)。其中:
- 難度分級:簡單(挖空 36 格)/ 中等(挖空 46 格)/ 困難(挖空 54 格),點擊頂部按鈕切換,立即生成新題。
- 選格交互:鼠標點擊選中格子,選中格高亮藍色;同行、同列、同宮格自動淺藍著色;與選中格數(shù)字相同的其他格子也會同步高亮,方便排除法填寫。
- 錯誤提示:輸入與正確答案不符時,數(shù)字即時變紅,幫助玩家識別沖突。
- 提示功能:點擊"提示"按鈕,自動填入當前選中格的正確答案。
- 計時器:游戲啟動即開始計時,完成時停止并在勝利彈窗中展示用時。
- 鍵盤操作:數(shù)字鍵 1–9 輸入;Delete / Backspace / 0 清除;方向鍵在格子間移動;R(通過新游戲按鈕)重新開始。
- 勝利判定:所有格填滿且全部正確時觸發(fā)勝利遮罩,展示完成提示與用時。
游戲?qū)崿F(xiàn)

初始化與基礎(chǔ)設(shè)置
游戲啟動時初始化 Pygame 并定義窗口尺寸、網(wǎng)格參數(shù)和顏色常量。
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("數(shù)獨")
clock = pygame.time.Clock()
游戲窗口為 540×680 像素,上方 100 像素留給標題與功能按鈕,下方 9×9 的棋盤每格 56×56 像素,底部留出計時與說明文字區(qū)域。
顏色定義
C_BG = (248, 248, 252) # 整體淺灰背景 C_GRID_BG = (255, 255, 255) # 棋盤白色底 C_LINE_THIN = (200, 200, 215) # 細網(wǎng)格線 C_LINE_FAT = (80, 80, 110) # 粗宮格線 C_SELECT = (200, 220, 255) # 選中格高亮藍 C_SAME_NUM = (225, 235, 255) # 同行/列/宮高亮 C_GIVEN = (40, 40, 80) # 題目預(yù)設(shè)數(shù)字(深藍近黑) C_INPUT = (60, 100, 200) # 玩家輸入正確數(shù)字(藍色) C_ERROR = (220, 50, 50) # 輸入錯誤數(shù)字(紅色) C_WIN = (60, 180, 100) # 勝利彈窗綠色
顏色體系以冷色調(diào)為主,區(qū)分"題目給定"與"玩家輸入"的視覺層次,錯誤提示以紅色醒目標出,不干擾正常狀態(tài)的閱讀感。
字體加載
CHINESE_FONT_PATH = r"C:/Windows/Fonts/simsun.ttc" FONT_NUM = pygame.font.Font(CHINESE_FONT_PATH, 32) # 格內(nèi)數(shù)字 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) # 標題
數(shù)獨生成算法
數(shù)獨生成分兩步:先用回溯算法生成一個完整合法解,再隨機挖空指定數(shù)量的格子作為題目。
合法性驗證is_valid
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,保證填入的每個數(shù)字合法。
回溯求解solve
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 的順序后逐一嘗試;若某格所有候選數(shù)均無法成立則回退。對候選數(shù) random.shuffle 是關(guān)鍵——正是這一步保證每次生成的完整解都不同,從而產(chǎn)生隨機題目。
題目生成generate_puzzle
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 貫穿整個游戲生命周期,是所有校驗邏輯的基準。
核心交互邏輯
狀態(tài)變量
board, solution, given = None, None, None selected = None errors = [[False] * 9 for _ in range(9)] won = False start_time = 0 elapsed = 0
given 是與 board 等大的布爾矩陣,記錄哪些格子是題目預(yù)設(shè)(不可修改)。errors 同樣是布爾矩陣,驅(qū)動紅色字體的顯示。selected 存儲當前選中格的 (row, col),為 None 時表示無選中狀態(tài)。
新游戲new_game
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
切換難度或點擊"新游戲"時調(diào)用,重置所有狀態(tài)并重新生成題目,計時器同步清零。
錯誤校驗check_errors
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
每次玩家輸入數(shù)字后立即調(diào)用。逐格比對 board 與 solution,空格不標記錯誤;若全格填滿且無誤,則將 won 置為 True,觸發(fā)勝利流程。
鍵盤輸入處理
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] 保護預(yù)設(shè)格不被修改。數(shù)字鍵通過 event.key - pygame.K_0 直接轉(zhuǎn)換為整數(shù)值寫入棋盤;Delete / Backspace / 0 均執(zhí)行清除操作,同時重置該格的錯誤狀態(tài)。方向鍵移動選中格,使鍵盤黨無需觸碰鼠標也能流暢操作。
繪制方法
關(guān)聯(lián)格高亮
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))
高亮分三層:選中格自身(深藍)→ 同行/列/宮(淺藍)→ 相同數(shù)字(淺綠)。三者互不覆蓋,優(yōu)先級由 if/elif 保證。這是數(shù)獨 UI 的核心輔助功能,大幅降低玩家的視覺搜索負擔(dān)。
數(shù)字渲染
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)))
三色區(qū)分:深藍近黑(題目預(yù)設(shè))→ 藍色(玩家正確輸入)→ 紅色(輸入有誤)。t.get_rect(center=...) 確保數(shù)字精確居中于格子內(nèi),無論字形寬窄都不偏移。
網(wǎng)格線繪制
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 宮格,與標準數(shù)獨印刷風(fēng)格一致。
計時顯示
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 格式化保證分秒始終兩位顯示。
勝利彈窗
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 中實現(xiàn)半透明遮罩的標準做法。
繪制順序為:背景 → 按鈕 → 高亮格 → 數(shù)字 → 網(wǎng)格線 → 計時 → 勝利遮罩,嚴格保證層次正確。
主循環(huán) main:
while True:
mx, my = pygame.mouse.get_pos()
if not won:
elapsed = time.time() - start_time
# 事件處理 ...
# 繪制 ...
pygame.display.flip()
clock.tick(FPS)
以固定 60 FPS 驅(qū)動主循環(huán),保證界面響應(yīng)流暢。數(shù)獨無需像貪吃蛇那樣用幀率控制游戲節(jié)奏,因此全程維持恒定幀率即可。
全部代碼
"""
數(shù)獨 — 隨機生成 + 錯誤提示 + 難度分級
操作:鼠標點擊選格,鍵盤數(shù)字輸入,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}
# ── 數(shù)獨生成 ──────────────────────────────────────────────────────────
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
# ── 主循環(huán) ────────────────────────────────────────────────────────────
def main():
screen = pygame.display.set_mode((W, H))
pygame.display.set_caption("數(shù)獨")
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("數(shù) 獨", 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))
# 網(wǎng)格背景
grid_rect = pygame.Rect(GRID_X, GRID_Y, 9*CELL, 9*CELL)
pygame.draw.rect(screen, C_GRID_BG, grid_rect)
# 高亮選中格周圍同數(shù)字
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))
# 數(shù)字
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)))
# 網(wǎng)格線
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("點擊格子 → 數(shù)字鍵輸入 | 方向鍵移動 | 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()
到此這篇關(guān)于Python通過Pygame實現(xiàn)一個功能完整的數(shù)獨游戲的文章就介紹到這了,更多相關(guān)Python Pygame數(shù)獨游戲內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
python程序?qū)崿F(xiàn)BTC(比特幣)挖礦的完整代碼
這篇文章主要介紹了python程序?qū)崿F(xiàn)BTC(比特幣)挖礦的完整代碼,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-01-01
python目標檢測yolo3詳解預(yù)測及代碼復(fù)現(xiàn)
這篇文章主要為大家介紹了python目標檢測yolo3詳解預(yù)測及代碼復(fù)現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-05-05
關(guān)于tensorflow和keras版本的對應(yīng)關(guān)系
這篇文章主要介紹了關(guān)于tensorflow和keras版本的對應(yīng)關(guān)系,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-06-06
使用python進行nc轉(zhuǎn)tif的3種情況解決
在進行氣候分析時,很多人都會用到ERA5數(shù)據(jù),下面這篇文章主要給大家介紹了關(guān)于如何使用python進行nc轉(zhuǎn)tif的3種情況的解決方法,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2022-03-03

