通過本綜合教程,學習如何使用 Pygame 在 Python 中創建自己的數獨游戲。本指南涵蓋安裝、游戲邏輯、用戶界面和計時器功能,是希望創建功能性和可擴展性數獨益智游戲的愛好者的理想之選。
數獨是一種經典的數字謎題,多年來一直吸引著謎題愛好者。在本教程中,我們將介紹使用 Python 創建數獨游戲的過程。本指南結束時,您將擁有一個功能齊全的數獨游戲,您可以玩這個游戲,甚至可以進一步擴展。
安裝和設置
讓我們先確保 Pygame 已安裝在電腦上;前往終端,使用 pip
安裝 pygame
模塊。
$ pip install pygame
然后,為游戲創建一個目錄,并在其中創建以下 .py 文件:settings.py
、main.py
、sudoku.py
、cell.py
、table.py
和 clock.py
。
讓我們在 settings.py
中定義游戲變量和有用的外部函數:
# setting.py
from itertools import isliceWIDTH, HEIGHT = 450, 450
N_CELLS = 9
CELL_SIZE = (WIDTH // N_CELLS, HEIGHT // N_CELLS)# Convert 1D list to 2D list
def convert_list(lst, var_lst):it = iter(lst)return [list(islice(it, i)) for i in var_lst]
接下來,讓我們創建游戲的主類。該類將負責調用游戲和運行游戲循環:
# main.py
import pygame, sys
from settings import WIDTH, HEIGHT, CELL_SIZE
from table import Tablepygame.init()screen = pygame.display.set_mode((WIDTH, HEIGHT + (CELL_SIZE[1] * 3)))
pygame.display.set_caption("Sudoku")pygame.font.init()class Main:def __init__(self, screen):self.screen = screenself.FPS = pygame.time.Clock()self.lives_font = pygame.font.SysFont("monospace", CELL_SIZE[0] // 2)self.message_font = pygame.font.SysFont('Bauhaus 93', (CELL_SIZE[0]))self.color = pygame.Color("darkgreen")def main(self):table = Table(self.screen)while True:self.screen.fill("gray")for event in pygame.event.get():if event.type == pygame.QUIT:pygame.quit()sys.exit()if event.type == pygame.MOUSEBUTTONDOWN:if not table.game_over:table.handle_mouse_click(event.pos)# lower screen displayif not table.game_over:my_lives = self.lives_font.render(f"Lives Left: {table.lives}", True, pygame.Color("black"))self.screen.blit(my_lives, ((WIDTH // table.SRN) - (CELL_SIZE[0] // 2), HEIGHT + (CELL_SIZE[1] * 2.2)))else:if table.lives <= 0:message = self.message_font.render("GAME OVER!!", True, pygame.Color("red"))self.screen.blit(message, (CELL_SIZE[0] + (CELL_SIZE[0] // 2), HEIGHT + (CELL_SIZE[1] * 2)))elif table.lives > 0:message = self.message_font.render("You Made It!!!", True, self.color)self.screen.blit(message, (CELL_SIZE[0] , HEIGHT + (CELL_SIZE[1] * 2)))table.update()pygame.display.flip()self.FPS.tick(30)if __name__ == "__main__":play = Main(screen)play.main()
從名稱本身來看,Main
類將是我們游戲的主類。它的參數 screen
將作為游戲窗口,用于制作游戲動畫。
main()
函數將運行并更新我們的游戲。它將首先初始化 Table
(作為謎題表)。為了保持游戲運行而不故意退出,我們在其中設置了一個 while
循環。在循環內部,我們還將設置另一個循環(for
循環),它將捕捉游戲窗口內發生的所有事件,如按鍵、鼠標移動、鼠標按鍵點擊或玩家點擊退出鍵等事件。
main()
還負責顯示玩家的 "剩余生命 "和游戲結束信息,無論玩家是贏還是輸。為了更新游戲,我們調用 table.update()
來更新游戲表中的變化。然后,pygame.display.flip()
會呈現這些變化。self.FPS.tick(30)
控制幀頻更新速度。
生成數獨謎題
sudoku()
類將負責為我們隨機生成數獨謎題。在 sudoku.py
中創建一個類并命名為 Sudoku
。首先導入必要的模塊:random
, math
和 copy
:
# sudoku.py
import random
import math
import copyclass Sudoku:def __init__(self, N, E):self.N = Nself.E = E# compute square root of Nself.SRN = int(math.sqrt(N))self.table = [[0 for x in range(N)] for y in range(N)]self.answerable_table = Noneself._generate_table()def _generate_table(self):# fill the subgroups diagonally table/matricesself.fill_diagonal()# fill remaining empty subgroupsself.fill_remaining(0, self.SRN)# Remove random Key digits to make gameself.remove_digits()
該類有一個初始化方法(__init__()
),需要兩個參數N
和E
,分別代表數獨網格的大小和創建謎題時需要移除的單元格數。類屬性包括 N
(網格大小)、E
(需要刪除的單元格數)、SRN
(N 的平方根)、table
(數獨網格)和 answerable_table
(刪除部分單元格后的網格副本)。在創建對象時,會立即調用 _generate_table()
方法來設置數獨謎題。
主要數字填充:
def fill_diagonal(self):for x in range(0, self.N, self.SRN):self.fill_cell(x, x)def not_in_subgroup(self, rowstart, colstart, num):for x in range(self.SRN):for y in range(self.SRN):if self.table[rowstart + x][colstart + y] == num:return Falsereturn Truedef fill_cell(self, row, col):num = 0for x in range(self.SRN):for y in range(self.SRN):while True:num = self.random_generator(self.N)if self.not_in_subgroup(row, col, num):breakself.table[row + x][col + y] = num
fill_diagonal()
方法通過調用每個子組的 fill_cell()
方法對角填充子組。fill_cell()
方法會在每個子組單元格中生成并放置一個唯一的數字。
def random_generator(self, num):return math.floor(random.random() * num + 1)def safe_position(self, row, col, num):return (self.not_in_row(row, num) and self.not_in_col(col, num) and self.not_in_subgroup(row - row % self.SRN, col - col % self.SRN, num))def not_in_row(self, row, num):for col in range(self.N):if self.table[row][col] == num:return Falsereturn Truedef not_in_col(self, col, num):for row in range(self.N):if self.table[row][col] == num:return Falsereturn Truedef fill_remaining(self, row, col):# check if we have reached the end of the matrixif row == self.N - 1 and col == self.N:return True# move to the next row if we have reached the end of the current rowif col == self.N:row += 1col = 0# skip cells that are already filledif self.table[row][col] != 0:return self.fill_remaining(row, col + 1)# try filling the current cell with a valid valuefor num in range(1, self.N + 1):if self.safe_position(row, col, num):self.table[row][col] = numif self.fill_remaining(row, col + 1):return Trueself.table[row][col] = 0# no valid value was found, so backtrackreturn False
定義了幾個輔助方法(random_generator()
、safe_position()
、not_in_row()
、not_in_col()
和 not_in_subgroup()
)。這些方法有助于生成隨機數、檢查放置數字的位置是否安全,以及確保行、列或子群中沒有已存在的數字。
def remove_digits(self):count = self.E# replicates the table so we can have a filled and pre-filled copyself.answerable_table = copy.deepcopy(self.table)# removing random numbers to create the puzzle sheetwhile (count != 0):row = self.random_generator(self.N) - 1col = self.random_generator(self.N) - 1if (self.answerable_table[row][col] != 0):count -= 1self.answerable_table[row][col] = 0
remove_digits()
方法會從填滿的網格中移除指定數量的隨機數字來創建謎題。在移除數字之前,它還會創建一個網格副本(answerable_table
)。
def puzzle_table(self):return self.answerable_tabledef puzzle_answers(self):return self.tabledef print_sudoku(self):for row in range(self.N):for col in range(self.N):print(self.table[row][col], end=" ")print()print("")for row in range(self.N):for col in range(self.N):print(self.answerable_table[row][col], end=" ")print()if __name__ == "__main__":N = 9E = (N * N) // 2sudoku = Sudoku(N, E)sudoku.print_sudoku()
最后 3 個方法負責返回并打印謎題和/或答案。puzzle_table()
返回答案表(去掉部分單元格的謎題)。puzzle_answers()
返回完整的數獨表格。print_sudoku()
同時打印完整的數獨網格和答案網格。
創建游戲表
在創建游戲網格之前,我們先創建表格單元。在 cell.py
中,創建函數 Cell()
:
# cell.py
import pygame
from settings import convert_listpygame.font.init()class Cell:def __init__(self, row, col, cell_size, value, is_correct_guess = None):self.row = rowself.col = colself.cell_size = cell_sizeself.width = self.cell_size[0]self.height = self.cell_size[1]self.abs_x = row * self.widthself.abs_y = col * self.heightself.value = valueself.is_correct_guess = is_correct_guessself.guesses = None if self.value != 0 else [0 for x in range(9)]self.color = pygame.Color("white")self.font = pygame.font.SysFont('monospace', self.cell_size[0])self.g_font = pygame.font.SysFont('monospace', (cell_size[0] // 3))self.rect = pygame.Rect(self.abs_x,self.abs_y,self.width,self.height)def update(self, screen, SRN = None):pygame.draw.rect(screen, self.color, self.rect)if self.value != 0:font_color = pygame.Color("black") if self.is_correct_guess else pygame.Color("red")num_val = self.font.render(str(self.value), True, font_color)screen.blit(num_val, (self.abs_x, self.abs_y))elif self.value == 0 and self.guesses != None:cv_list = convert_list(self.guesses, [SRN, SRN, SRN])for y in range(SRN):for x in range(SRN):num_txt = " "if cv_list[y][x] != 0:num_txt = cv_list[y][x]num_txt = self.g_font.render(str(num_txt), True, pygame.Color("orange"))abs_x = (self.abs_x + ((self.width // SRN) * x))abs_y = (self.abs_y + ((self.height // SRN) * y))abs_pos = (abs_x, abs_y)screen.blit(num_txt, abs_pos)
Cell()
類的屬性包括:row
和 col
(單元格在表格中的位置)、cell_size
、width
和 height
、abs_x
和 abs_y
(單元格在屏幕上的絕對 x 坐標和 y 坐標)、value
(數值,空單元格為 0)、is_correct_guess
(表示當前值是否為正確的猜測值)和 guesses
(列表,表示空單元格的可能猜測值,如果單元格已填充,則表示無)。
update()
方法負責更新屏幕上單元格的圖形表示。它使用 pygame.draw.rect
繪制一個指定顏色的矩形。根據單元格是填充的(value != 0)還是空的(value ==0),它要么在填充的單元格中繪制數值,要么在空的單元格中繪制可能的猜測。
如果單元格為空并且有可能的猜測,則使用 convert_list()
函數將猜測列表轉換為二維列表。然后遍歷轉換后的列表,并在單元格的相應位置繪制每個猜測。它會使用小字體 (g_font
) 將每個猜測渲染為文本。根據二維列表中的位置,計算每個猜測在單元格中的絕對位置。然后,在計算出的位置將文本顯示(繪制)到屏幕上。
現在,讓我們繼續創建游戲表格。在 table.py
中創建一個類并命名為 Table
。它使用 Pygame 庫創建數獨網格,處理用戶輸入,并顯示謎題、數字選擇、按鈕和計時器。
import pygame
import math
from cell import Cell
from sudoku import Sudoku
from clock import Clockfrom settings import WIDTH, HEIGHT, N_CELLS, CELL_SIZEpygame.font.init()class Table:def __init__(self, screen):self.screen = screenself.puzzle = Sudoku(N_CELLS, (N_CELLS * N_CELLS) // 2)self.clock = Clock()self.answers = self.puzzle.puzzle_answers()self.answerable_table = self.puzzle.puzzle_table()self.SRN = self.puzzle.SRNself.table_cells = []self.num_choices = []self.clicked_cell = Noneself.clicked_num_below = Noneself.cell_to_empty = Noneself.making_move = Falseself.guess_mode = Trueself.lives = 3self.game_over = Falseself.delete_button = pygame.Rect(0, (HEIGHT + CELL_SIZE[1]), (CELL_SIZE[0] * 3), (CELL_SIZE[1]))self.guess_button = pygame.Rect((CELL_SIZE[0] * 6), (HEIGHT + CELL_SIZE[1]), (CELL_SIZE[0] * 3), (CELL_SIZE[1]))self.font = pygame.font.SysFont('Bauhaus 93', (CELL_SIZE[0] // 2))self.font_color = pygame.Color("white")self._generate_game()self.clock.start_timer()def _generate_game(self):# generating sudoku tablefor y in range(N_CELLS):for x in range(N_CELLS):cell_value = self.answerable_table[y][x]is_correct_guess = True if cell_value != 0 else Falseself.table_cells.append(Cell(x, y, CELL_SIZE, cell_value, is_correct_guess))# generating number choicesfor x in range(N_CELLS):self.num_choices.append(Cell(x, N_CELLS, CELL_SIZE, x + 1))
Table
類的 __init__()
方法(構造函數)初始化了各種屬性,如 Pygame 屏幕、數獨謎題、時鐘、答案、可回答的表格以及其他與游戲相關的變量。
def _draw_grid(self):grid_color = (50, 80, 80)pygame.draw.rect(self.screen, grid_color, (-3, -3, WIDTH + 6, HEIGHT + 6), 6)i = 1while (i * CELL_SIZE[0]) < WIDTH:line_size = 2 if i % 3 > 0 else 4pygame.draw.line(self.screen, grid_color, ((i * CELL_SIZE[0]) - (line_size // 2), 0), ((i * CELL_SIZE[0]) - (line_size // 2), HEIGHT), line_size)pygame.draw.line(self.screen, grid_color, (0, (i * CELL_SIZE[0]) - (line_size // 2)), (HEIGHT, (i * CELL_SIZE[0]) - (line_size // 2)), line_size)i += 1def _draw_buttons(self):# adding delete button detailsdl_button_color = pygame.Color("red")pygame.draw.rect(self.screen, dl_button_color, self.delete_button)del_msg = self.font.render("Delete", True, self.font_color)self.screen.blit(del_msg, (self.delete_button.x + (CELL_SIZE[0] // 2), self.delete_button.y + (CELL_SIZE[1] // 4)))# adding guess button detailsgss_button_color = pygame.Color("blue") if self.guess_mode else pygame.Color("purple")pygame.draw.rect(self.screen, gss_button_color, self.guess_button)gss_msg = self.font.render("Guess: On" if self.guess_mode else "Guess: Off", True, self.font_color)self.screen.blit(gss_msg, (self.guess_button.x + (CELL_SIZE[0] // 3), self.guess_button.y + (CELL_SIZE[1] // 4)))
_draw_grid()
方法負責繪制數獨網格;它使用 Pygame 函數根據單元格的大小繪制網格線。_draw_buttons()
方法負責繪制刪除和猜測按鈕;它使用 Pygame 函數繪制帶有適當顏色和信息的矩形按鈕。
def _get_cell_from_pos(self, pos):for cell in self.table_cells:if (cell.row, cell.col) == (pos[0], pos[1]):return cell
_get_cell_from_pos()
方法返回數獨表中給定位置(行、列)上的單元格對象。
# checking rows, cols, and subgroups for adding guesses on each celldef _not_in_row(self, row, num):for cell in self.table_cells:if cell.row == row:if cell.value == num:return Falsereturn Truedef _not_in_col(self, col, num):for cell in self.table_cells:if cell.col == col:if cell.value == num:return Falsereturn Truedef _not_in_subgroup(self, rowstart, colstart, num):for x in range(self.SRN):for y in range(self.SRN):current_cell = self._get_cell_from_pos((rowstart + x, colstart + y))if current_cell.value == num:return Falsereturn True# remove numbers in guess if number already guessed in the same row, col, subgroup correctlydef _remove_guessed_num(self, row, col, rowstart, colstart, num):for cell in self.table_cells:if cell.row == row and cell.guesses != None:for x_idx,guess_row_val in enumerate(cell.guesses):if guess_row_val == num:cell.guesses[x_idx] = 0if cell.col == col and cell.guesses != None:for y_idx,guess_col_val in enumerate(cell.guesses):if guess_col_val == num:cell.guesses[y_idx] = 0for x in range(self.SRN):for y in range(self.SRN):current_cell = self._get_cell_from_pos((rowstart + x, colstart + y))if current_cell.guesses != None:for idx,guess_val in enumerate(current_cell.guesses):if guess_val == num:current_cell.guesses[idx] = 0
方法 _not_in_row()
、_not_in_col()
、_not_in_subgroup()
和 _remove_guessed_num()
負責檢查數字在行、列或子群中是否有效,并在正確放置后刪除猜測的數字。
def handle_mouse_click(self, pos):x, y = pos[0], pos[1]# getting table cell clickedif x <= WIDTH and y <= HEIGHT:x = x // CELL_SIZE[0]y = y // CELL_SIZE[1]clicked_cell = self._get_cell_from_pos((x, y))# if clicked empty cellif clicked_cell.value == 0:self.clicked_cell = clicked_cellself.making_move = True# clicked unempty cell but with wrong number guesselif clicked_cell.value != 0 and clicked_cell.value != self.answers[y][x]:self.cell_to_empty = clicked_cell# getting number selectedelif x <= WIDTH and y >= HEIGHT and y <= (HEIGHT + CELL_SIZE[1]):x = x // CELL_SIZE[0]self.clicked_num_below = self.num_choices[x].value# deleting numberselif x <= (CELL_SIZE[0] * 3) and y >= (HEIGHT + CELL_SIZE[1]) and y <= (HEIGHT + CELL_SIZE[1] * 2):if self.cell_to_empty:self.cell_to_empty.value = 0self.cell_to_empty = None# selecting modeselif x >= (CELL_SIZE[0] * 6) and y >= (HEIGHT + CELL_SIZE[1]) and y <= (HEIGHT + CELL_SIZE[1] * 2):self.guess_mode = True if not self.guess_mode else False# if making a moveif self.clicked_num_below and self.clicked_cell != None and self.clicked_cell.value == 0:current_row = self.clicked_cell.rowcurrent_col = self.clicked_cell.colrowstart = self.clicked_cell.row - self.clicked_cell.row % self.SRNcolstart = self.clicked_cell.col - self.clicked_cell.col % self.SRNif self.guess_mode:# checking the vertical group, the horizontal group, and the subgroupif self._not_in_row(current_row, self.clicked_num_below) and self._not_in_col(current_col, self.clicked_num_below):if self._not_in_subgroup(rowstart, colstart, self.clicked_num_below):if self.clicked_cell.guesses != None:self.clicked_cell.guesses[self.clicked_num_below - 1] = self.clicked_num_belowelse:self.clicked_cell.value = self.clicked_num_below# if the player guess correctlyif self.clicked_num_below == self.answers[self.clicked_cell.col][self.clicked_cell.row]:self.clicked_cell.is_correct_guess = Trueself.clicked_cell.guesses = Noneself._remove_guessed_num(current_row, current_col, rowstart, colstart, self.clicked_num_below)# if guess is wrongelse:self.clicked_cell.is_correct_guess = Falseself.clicked_cell.guesses = [0 for x in range(9)]self.lives -= 1self.clicked_num_below = Noneself.making_move = Falseelse:self.clicked_num_below = None
handle_mouse_click(
) 方法根據鼠標在屏幕上的位置來處理鼠標點擊。它會相應地更新游戲變量,如 clicked_cell
、clicked_num_below
和 cell_to_empty
。
def _puzzle_solved(self):check = Nonefor cell in self.table_cells:if cell.value == self.answers[cell.col][cell.row]:check = Trueelse:check = Falsebreakreturn check
_puzzle_solved()
方法通過比較每個單元格中的值與正確答案,檢查數獨謎題是否已解。
def update(self):[cell.update(self.screen, self.SRN) for cell in self.table_cells][num.update(self.screen) for num in self.num_choices]self._draw_grid()self._draw_buttons()if self._puzzle_solved() or self.lives == 0:self.clock.stop_timer()self.game_over = Trueelse:self.clock.update_timer()self.screen.blit(self.clock.display_timer(), (WIDTH // self.SRN,HEIGHT + CELL_SIZE[1]))
update 方法負責更新顯示內容。它更新單元格和數字的圖形表示,繪制網格和按鈕,檢查謎題是否已解開或游戲是否已結束,以及更新計時器。
添加游戲計時器
在代碼的最后一部分,我們要為計時器創建一個類。在 clock.py
中創建時鐘類:
import pygame, time
from settings import CELL_SIZEpygame.font.init()class Clock:def __init__(self):self.start_time = Noneself.elapsed_time = 0self.font = pygame.font.SysFont("monospace", CELL_SIZE[0])self.message_color = pygame.Color("black")# Start the timerdef start_timer(self):self.start_time = time.time()# Update the timerdef update_timer(self):if self.start_time is not None:self.elapsed_time = time.time() - self.start_time# Display the timerdef display_timer(self):secs = int(self.elapsed_time % 60)mins = int(self.elapsed_time / 60)my_time = self.font.render(f"{mins:02}:{secs:02}", True, self.message_color)return my_time# Stop the timerdef stop_timer(self):self.start_time = None
start_timer()
方法在調用時使用 time.time() 將 start_time 屬性設置為當前時間。這標志著計時器的開始。
update_timer()
方法計算定時器開始后的耗時。如果 start_time
屬性不是 None
,則用 start_time
減去當前時間來更新 elapsed_time
。
display_timer()
方法會將已用時間轉換為分鐘和秒。然后使用 Pygame 字體以 "MM:SS "格式創建時間的文本表示。渲染后的文本將被返回。
stop_timer()
方法將 start_time
重置為 None
,從而有效地停止計時器。
現在,我們的編碼工作完成了要體驗我們的游戲,只需在進入項目目錄后在終端運行 python main.py 或 python3 main.py。下面是一些游戲快照:
結論
最后,本教程概述了使用 Pygame 庫在 Python 中開發數獨游戲的過程。實現過程涵蓋了數獨謎題生成、圖形表示、用戶交互和計時器功能等關鍵方面。通過將代碼分解為模塊化類(如數獨、單元格、表格和時鐘),本教程強調了一種結構化和有組織的游戲開發方法。對于那些希望創建自己的數獨游戲或加深對使用 Pygame 開發 Python 游戲的理解的人來說,本教程是一個寶貴的資源。