建模與消除判定核心邏輯)
簡(jiǎn)介面向Python學(xué)習(xí)者的游戲開(kāi)發(fā)實(shí)戰(zhàn)資源以經(jīng)典消除類(lèi)游戲“開(kāi)心消消樂(lè)”為項(xiàng)目載體從基礎(chǔ)語(yǔ)法、列表與字典等數(shù)據(jù)結(jié)構(gòu)到游戲主循環(huán)、GUI界面、狀態(tài)管理和事件處理完整呈現(xiàn)一個(gè)可運(yùn)行項(xiàng)目的代碼組織方式。壓縮包共138個(gè)文件大小約5.21MB包含2個(gè)源文件及編譯后的pyc、92張png圖片素材、20段mp3和20段ogg音樂(lè)音效以及README說(shuō)明文檔素材與代碼分離存放便于按場(chǎng)景、按鈕和消除反饋對(duì)照學(xué)習(xí)。已有577人學(xué)習(xí)下載。源碼中涉及相鄰元素匹配的深度/廣度優(yōu)先搜索、元素消除后下落填充、隨機(jī)生成新元素、邊界條件處理等核心算法同時(shí)展示了得分、剩余步數(shù)、生命值等游戲狀態(tài)的維護(hù)方式并配有游戲場(chǎng)景背景音樂(lè)和多種消除音效適合畢業(yè)設(shè)計(jì)、課程設(shè)計(jì)或休閑游戲開(kāi)發(fā)入門(mén)參考。通過(guò)閱讀運(yùn)行源碼可掌握從游戲框架搭建、渲染優(yōu)化到調(diào)試測(cè)試的完整流程還能參考如何為代碼匹配美術(shù)與音頻資源。1. 開(kāi)心消消樂(lè)源碼課設(shè)、練手與二次開(kāi)發(fā)的完整底子做 Python 練手項(xiàng)目的人大部分會(huì)卡在同一個(gè)地方語(yǔ)法看懂了但不知道一個(gè)完整游戲是怎么組織出來(lái)的。這份開(kāi)心消消樂(lè)源碼正好補(bǔ)上這塊——三消游戲的棋盤(pán)建模、匹配檢測(cè)、消除下落、計(jì)分狀態(tài)分文件分函數(shù)寫(xiě)得清清楚楚不是把代碼全塞進(jìn)一個(gè) main.py 的選手級(jí)寫(xiě)法。它對(duì)打算交課程設(shè)計(jì)的學(xué)生、想學(xué) Pygame 游戲邏輯的初學(xué)者以及需要一份能改著玩的底子的從業(yè)者都有用。三消游戲品類(lèi)雖老但數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)依然能體現(xiàn)基本功放在簡(jiǎn)歷項(xiàng)目里也拿得出手。我拆包跑通后發(fā)現(xiàn)最值錢(qián)的部分不是那幾行繪圖調(diào)用而是棋盤(pán)模型與消除判定那一整套設(shè)計(jì)——搞懂它你就搞懂了整個(gè)游戲的核心以后換皮膚、加道具、調(diào)難度都不再是瞎拍腦袋。2. 棋盤(pán)模型先立住二維數(shù)組、方塊枚舉與開(kāi)局四步2.1 為什么三消游戲必須先定義棋盤(pán)而不是先畫(huà)界面接觸過(guò)一些剛?cè)腴T(mén)的朋友拿到這類(lèi)源碼第一反應(yīng)是找pygame.draw去了解繪制流程。但繪制只是表現(xiàn)層三消游戲的一切玩法邏輯都建立在棋盤(pán)的數(shù)據(jù)模型上。一個(gè)方塊在什么位置、什么顏色、能不能交換完全由底層數(shù)據(jù)決定畫(huà)面不過(guò)是對(duì)數(shù)據(jù)的可視化。如果先把數(shù)據(jù)模型定義清楚后面匹配、消除、下落實(shí)現(xiàn)起來(lái)會(huì)順很多反之畫(huà)都畫(huà)好了邏輯一改重畫(huà)的東西比想象的還多。這份源碼的做法是典型的游戲開(kāi)發(fā)思路用二維數(shù)組存棋盤(pán)用枚舉類(lèi)型限定方塊的種類(lèi)再用一個(gè)類(lèi)把棋盤(pán)數(shù)據(jù)和操作打包起來(lái)。數(shù)據(jù)結(jié)構(gòu)先行渲染放后面分的很清楚。2.2 方塊枚舉與棋盤(pán)初始化代碼打開(kāi)源碼會(huì)看到類(lèi)似下面這樣的枚舉定義import random from enum import Enum class TileType(Enum): EMPTY 0 # 空格子 RED 1 GREEN 2 BLUE 3 YELLOW 4 PURPLE 5枚舉的作用是讓代碼里的每一處都只和類(lèi)型名打交道不用去記1代表紅還是2代表綠。后面寫(xiě)消除判定時(shí)代碼的可讀性會(huì)明顯好于直接比較數(shù)字。這種小習(xí)慣在工程里比想象中重要改需求時(shí)搜TileType.RED比搜魔法數(shù)字1快得多。棋盤(pán)的初始化邏輯也很常規(guī)核心就兩步先全部置空再按行列填方塊并在填完一整行后檢查這一行是否有三連有就重新生成。源碼里大概長(zhǎng)這樣ROWS, COLS 8, 8 def init_board(rowsROWS, colsCOLS): board [[TileType.EMPTY for _ in range(cols)] for _ in range(rows)] for r in range(rows): for c in range(cols): board[r][c] random.choice(list(TileType))[1:] return board把方塊類(lèi)型直接傳給random.choice確保每個(gè)格子都能取到非空類(lèi)型。board是行優(yōu)先排列的二維列表board[r][c]表示第 r 行第 c 列后續(xù)所有算法都基于這個(gè)坐標(biāo)約定這也是這類(lèi)源碼里最常見(jiàn)的約定。2.3 開(kāi)局消除與基礎(chǔ)參數(shù)說(shuō)明開(kāi)局棋盤(pán)直接隨機(jī)填充大概率自帶三連所以源碼里在生成后會(huì)做一次預(yù)檢掃描到三連就重填那一行。性能上是完全夠用的三消棋盤(pán)單行重試一兩次也就穩(wěn)定了比整體重新生成要省不少。代碼里這兩個(gè)參數(shù)建議記一下參數(shù)位置含義建議ROWS/COLS全局棋盤(pán)行列數(shù)改小 難度降低改大 棋盤(pán)更密組合更多TileType枚舉數(shù)量全局方塊種類(lèi)種類(lèi)越少越容易出三連越多越難如果想降低游戲難度最直接的辦法就是把方塊種類(lèi)從 5 種砍到 4 種。三消的數(shù)學(xué)規(guī)律擺在那里顏色越少隨機(jī)排列出三連的概率越高新手挪兩步就能消一次體驗(yàn)會(huì)好不少。這塊邏輯改動(dòng)很小全局刪掉一個(gè)枚舉值即可。3. 核心匹配判定三連檢測(cè)的實(shí)現(xiàn)與四個(gè)邊界條件3.1 從交換到判定的完整調(diào)用鏈三消游戲的操作鏈路其實(shí)很短玩家點(diǎn)一個(gè)格子再點(diǎn)相鄰格子程序交換這兩個(gè)方塊然后檢查交換后的棋盤(pán)中是否存在三個(gè)或以上連成一線(xiàn)的同色方塊。有就消除沒(méi)有就換回去。整個(gè)循環(huán)聽(tīng)起來(lái)簡(jiǎn)單但實(shí)現(xiàn)里的邊界條件能把人折磨瘋。這套源碼把判定抽成了獨(dú)立函數(shù)一次交換觸發(fā)一次檢測(cè)代碼路徑比界面事件驅(qū)動(dòng)的方式更直觀。源碼里檢測(cè)的核心是一個(gè)按行、按列分別掃描的函數(shù)大概邏輯如下def find_matches(board): matches set() rows, cols len(board), len(board[0]) # 按行檢查三個(gè)以上連續(xù)同色 for r in range(rows): for c in range(cols - 2): if board[r][c] board[r][c1] board[r][c2]: if board[r][c] ! TileType.EMPTY: matches.add((r, c)) matches.add((r, c1)) matches.add((r, c2)) # 按列檢查三個(gè)以上連續(xù)同色 for c in range(cols): for r in range(rows - 2): if board[r][c] board[r1][c] board[r2][c]: if board[r][c] ! TileType.EMPTY: matches.add((r, c)) matches.add((r1, c)) matches.add((r2, c)) return list(matches)matches用set而不是list是為了避免一個(gè)方塊同時(shí)屬于橫向和縱向兩個(gè)匹配組時(shí)被重復(fù)記錄。如果你拿到手的源碼用的是list消除時(shí)大概率出現(xiàn)過(guò)方塊少消的情況——這就是根源。3.2 檢測(cè)邏輯的推導(dǎo)與邊界細(xì)節(jié)這個(gè)函數(shù)里range(cols - 2)和range(rows - 2)是整份源碼最容易抄錯(cuò)的地方。循環(huán)里一次取三個(gè)格子如果棋盤(pán)寬度是 8那么起始列只需要從 0 到 5 取就行因?yàn)橄聵?biāo) 6、7 作為起點(diǎn)時(shí)根本沒(méi)有足夠的格子湊出三連。見(jiàn)過(guò)一些人把范圍寫(xiě)成range(cols)導(dǎo)致后面訪(fǎng)問(wèn)board[r][c2]越界程序直接崩掉。另兩個(gè)邊界點(diǎn)是空方塊被判定為匹配、以及兩個(gè)相鄰相同方塊組成的假三連。源碼用board[r][c] ! TileType.EMPTY擋住空格子又要求連續(xù)相等天然排除了中間斷開(kāi)的組合。如果你做二次開(kāi)發(fā)要加四連、五連特效建議不要把匹配函數(shù)直接改復(fù)雜另行寫(xiě)一個(gè)計(jì)分?jǐn)U展函數(shù)去識(shí)別更長(zhǎng)連擊更穩(wěn)妥保留基礎(chǔ)函數(shù)作為通用判定。3.3 交換合法性與回滾的代碼走讀現(xiàn)在再看交換函數(shù)會(huì)發(fā)現(xiàn)它是整個(gè)游戲里最講究的一個(gè)環(huán)節(jié)。源碼里交換前會(huì)先判斷兩個(gè)格子是否相鄰不相鄰直接拒絕操作這避免了玩家亂點(diǎn)造成的詭異狀態(tài)def swap_tiles(board, pos1, pos2): r1, c1 pos1 r2, c2 pos2 if abs(r1 - r2) abs(c1 - c2) ! 1: return False, board board[r1][c1], board[r2][c2] board[r2][c2], board[r1][c1] matches find_matches(board) if matches: return True, board # 還原交換 board[r1][c1], board[r2][c2] board[r2][c2], board[r1][c1] return False, board曼哈頓距離判斷相鄰是最經(jīng)典也最不容易出錯(cuò)的寫(xiě)法。對(duì)角線(xiàn)距離是 2不滿(mǎn)足條件上下左右相鄰距離是 1通過(guò)。交換后如果沒(méi)有匹配立刻原樣換回來(lái)。這一段判斷的順序不能反如果先驗(yàn)證有無(wú)匹配再去判斷相鄰?fù)婕尹c(diǎn)兩個(gè)對(duì)角線(xiàn)位置的方塊時(shí)棋盤(pán)狀態(tài)會(huì)被白白改動(dòng)一次雖然最終會(huì)回滾但會(huì)觸發(fā)額外的無(wú)意義下落和檢查拖慢節(jié)奏。4. 消除與重力下落連鎖反應(yīng)的處理方式4.1 消除一格的標(biāo)記法而不是直接刪格子找到匹配后源碼把匹配的格子標(biāo)記為EMPTY而不是直接pop()刪掉列表元素。這兩種做法在視覺(jué)效果上沒(méi)區(qū)別但邏輯上差距很大棋盤(pán)的行列結(jié)構(gòu)是固定的pop一下整行的長(zhǎng)度變了后續(xù)所有按列索引的坐標(biāo)全部錯(cuò)位查找與匹配邏輯就要推翻重寫(xiě)。保留結(jié)構(gòu)、只改值坐標(biāo)系統(tǒng)永遠(yuǎn)不亂這是處理網(wǎng)格游戲時(shí)一條很值得記住的原則。4.2 下落填充的經(jīng)典寫(xiě)法與參數(shù)說(shuō)明消除后的棋盤(pán)會(huì)出現(xiàn)空洞需要讓上方方塊落下來(lái)補(bǔ)齊并在頂部生成新方塊。源碼的實(shí)現(xiàn)方式是從下往上掃描每一列把非空格子依次往下挪最后在頂部補(bǔ)新方塊def apply_gravity_and_refill(board): rows, cols len(board), len(board[0]) for c in range(cols): write_row rows - 1 for r in range(rows - 1, -1, -1): if board[r][c] ! TileType.EMPTY: board[write_row][c] board[r][c] if write_row ! r: board[r][c] TileType.EMPTY write_row - 1 # 填充頂部空位 for r in range(write_row, -1, -1): board[r][c] random.choice(list(TileType))這里關(guān)鍵在write_row變量它表示當(dāng)前列最靠下的空位下標(biāo)。從下往上遍歷遇到非空方塊就把它搬到write_row位置然后write_row上移。這個(gè)過(guò)程結(jié)束后write_row以上的格子全是空位再統(tǒng)一生成新方塊填進(jìn)去。這個(gè)寫(xiě)法的好處是每個(gè)單元格最多被讀寫(xiě)兩次效率很高而且不會(huì)出現(xiàn)上下方塊交換位置時(shí)的覆蓋問(wèn)題——如果從上往下遍歷上面的方塊可能會(huì)被下面的方塊覆蓋掉數(shù)據(jù)就丟了。4.3 連鎖反應(yīng)循環(huán)與死循環(huán)保護(hù)下落完成后新方塊會(huì)引入新的匹配所以源碼在最外層包了一層while循環(huán)一直檢測(cè)到?jīng)]有匹配為止。連鎖反應(yīng)是這份源碼里體驗(yàn)最爽的一部分但也是最容易出bug的地方def process_matches(board): total_score 0 while True: matches find_matches(board) if not matches: break total_score len(matches) * 10 for r, c in matches: board[r][c] TileType.EMPTY apply_gravity_and_refill(board) return total_score計(jì)分直接按消除方塊數(shù)量乘以單格分值夠用也好理解。唯一風(fēng)險(xiǎn)是死循環(huán)——極端情況下下落生成的新方塊不斷形成三連循環(huán)無(wú)法終止。雖然隨機(jī)填充下概率極低但嚴(yán)謹(jǐn)一點(diǎn)可以在循環(huán)里加一個(gè)計(jì)數(shù)器超過(guò)比如 20 次連鎖就手動(dòng)打斷避免某次隨機(jī)數(shù)序列糟糕時(shí)游戲卡死。這個(gè)問(wèn)題在純粹用隨機(jī)數(shù)的棋盤(pán)里出現(xiàn)的概率不高但如果你后期加了特殊方塊連鎖概率會(huì)上升需要提前防一手。此外這一整段邏輯做完棋盤(pán)才重新回到穩(wěn)定狀態(tài)這時(shí)候游戲才應(yīng)該檢查是否還有可行步數(shù)。源碼把這一步放在了下一個(gè)章節(jié)的可行步檢測(cè)里這一步的順序很關(guān)鍵因?yàn)橹挥蟹€(wěn)定的棋盤(pán)才能正確判斷還有沒(méi)有得玩。5. 避坑與排查把這份源碼跑穩(wěn)的常見(jiàn)問(wèn)題記錄5.1 現(xiàn)象運(yùn)行報(bào)錯(cuò)IndexError: list index out of range原因棋盤(pán)下標(biāo)越界。最常見(jiàn)的來(lái)源是手動(dòng)修改了ROWS或COLS之后沒(méi)有檢查交換、匹配、下落三個(gè)函數(shù)是否全部使用了新尺寸。源碼里如果有一處用了硬編碼的8棋盤(pán)改成 6 行 7 列后那個(gè)函數(shù)就開(kāi)始越界。解決全局搜索硬編碼數(shù)字統(tǒng)一替換為ROWS和COLS變量引用。改完再跑一遍還報(bào)錯(cuò)的話(huà)就在swap_tiles入口處打印pos1pos2看坐標(biāo)是否超出棋盤(pán)邊界。5.2 現(xiàn)象方塊交換后永遠(yuǎn)無(wú)法消除棋盤(pán)死板原因交換判定和回滾邏輯里相鄰判斷寫(xiě)成了對(duì)角線(xiàn)也允許或者匹配檢測(cè)的范圍寫(xiě)錯(cuò)導(dǎo)致交換后明明有三連卻沒(méi)有被確認(rèn)。也可能是交換前的碰撞檢測(cè)太嚴(yán)格玩家根本沒(méi)法完成半步操作。解決先檢查abs(r1 - r2) abs(c1 - c2) ! 1這一行是否存在。如果源碼里寫(xiě)的是或立刻改掉。然后單獨(dú)調(diào)用find_matches打一段已知的三連排列進(jìn)去手動(dòng)驗(yàn)證檢測(cè)函數(shù)本身是否正確。5.3 現(xiàn)象消除后棋盤(pán)出現(xiàn)整行錯(cuò)位方塊數(shù)量對(duì)不上原因下落填充邏輯里遍歷方向?qū)懛戳?。從range(rows)正向遍歷時(shí)上方的方塊先被挪到下方空位但下方還沒(méi)遍歷到的方塊可能已經(jīng)占據(jù)了目標(biāo)位置導(dǎo)致互相覆蓋。解決確認(rèn)遍歷是range(rows - 1, -1, -1)從底向上而不是從頂向下。如果源碼里寫(xiě)的是正向遍歷改過(guò)來(lái)后下落行為恢復(fù)正常。5.4 現(xiàn)象pygame 窗口里中文文字顯示為方塊原因pygame 默認(rèn)字體不支持中文源碼里如果用pygame.font.SysFont(None, size)渲染中文字符串會(huì)得到一個(gè)個(gè)方框。這個(gè)問(wèn)題在拿源碼做課程設(shè)計(jì)、需要加中文提示時(shí)幾乎必踩。解決改成指定支持中文的字體文件Windows 上一般用C:/Windows/Fonts/msyh.ttcfont pygame.font.Font(C:/Windows/Fonts/msyh.ttc, 28)如果這個(gè)路徑不存在先搜一下系統(tǒng)里simhei.ttf或simsun.ttc用其中任意一個(gè)。macOS 上路徑一般是/System/Library/Fonts/PingFang.ttc。5.5 現(xiàn)象游戲玩幾步后沒(méi)有可行交換但程序沒(méi)提示原因源碼缺少「無(wú)可行步檢測(cè)」。三消游戲的棋盤(pán)在每次穩(wěn)定之后都應(yīng)該掃描一遍全局是否還存在至少一對(duì)相鄰方塊交換后能形成三連。如果完全不存在就應(yīng)該重新洗牌或結(jié)束游戲。很多簡(jiǎn)化版源碼根本沒(méi)有這段邏輯游戲會(huì)卡死在有可消除但無(wú)法交換的狀態(tài)。解決在process_matches返回后加一個(gè)has_valid_move(board)函數(shù)對(duì)每個(gè)相鄰對(duì)嘗試交換并執(zhí)行一次find_matches能找到就恢復(fù)。這個(gè)函數(shù)比較耗時(shí)但 8×8 棋盤(pán)跑一次毫秒級(jí)完成沒(méi)有任何性能壓力。6. 幾個(gè)低成本的二次開(kāi)發(fā)技巧把源碼改出自己的樣子對(duì)這份源碼做修改性?xún)r(jià)比最高的是加連擊放大、特殊方塊和開(kāi)場(chǎng)動(dòng)畫(huà)三件事。加連擊放大很簡(jiǎn)單在process_matches的循環(huán)里維護(hù)一個(gè)combo計(jì)數(shù)每輪循環(huán)加一計(jì)分乘上combo系數(shù)就行combo 1 while True: matches find_matches(board) if not matches: break total_score len(matches) * 10 * combo combo 1 ...改完后連鎖消除越深單次得分越高玩起來(lái)會(huì)有一個(gè)明顯的手感變化。特殊方塊屬于較大的改動(dòng)需要在TileType里新增類(lèi)型并在消除檢測(cè)中識(shí)別不建議新手一開(kāi)始就嘗試。更穩(wěn)妥的改法是先換顏色方案和方塊圖案——pygame.draw部分只需改 RGB 顏色值和半徑就能做出一版完全不同的視覺(jué)效果代碼風(fēng)險(xiǎn)最小。游戲速度這塊用兩處參數(shù)控制下落動(dòng)畫(huà)每幀的位移像素?cái)?shù)以及交換動(dòng)畫(huà)的時(shí)長(zhǎng)。前者改大后面方塊像砸下來(lái)手感偏硬后者縮短后操作反饋更跟手。我建議把這兩處也做成常量放到代碼頭部統(tǒng)一管理以后調(diào)起來(lái)不用到處翻函數(shù)體。整套源碼最讓我滿(mǎn)意的地方是它保留了游戲的主循環(huán)脈絡(luò)沒(méi)有用花哨的框架把核心邏輯藏起來(lái)。這也就意味著你有充分的空間在它上面做加法——把無(wú)可行步檢測(cè)補(bǔ)上把計(jì)分動(dòng)畫(huà)加上把音效資源掛好它就能從一個(gè)課程設(shè)計(jì)變成一份真正拿得出手的作品集項(xiàng)目。從那以后我拿到這類(lèi)游戲源碼第一件事永遠(yuǎn)是跑通它的核心循環(huán)再?gòu)?qiáng)行改動(dòng)一個(gè)參數(shù)觀察效果確認(rèn)數(shù)據(jù)模型和表現(xiàn)層是解耦的才開(kāi)始動(dòng)手改。整個(gè)流程走一遍這個(gè)源碼包能給你帶來(lái)多少東西心里就徹底有數(shù)了。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取