【AI 加持下的 Python 編程實戰 2_11】DIY 拓展:從掃雷小游戲開發再探問題分解與 AI 代碼調試能力(下)

(接 上篇)

5 復盤與 Copilot 的交互過程

前面兩篇文章分別涵蓋了掃雷游戲的問題分解和代碼實現過程,不知道各位是否會有代碼一氣呵成的錯覺?實際上,為了達到最終效果(如下所示),我和 GitHub Copilot 進行了多次正面交鋒,其間也走了很多彎路,這一篇就來和大家聊聊看似簡單的 AI 輔助編程暗含的陷阱和我實戰時踩過的坑。

先說說 Copilot 的優點吧。由于看過書中作者和 Copilot Chat 的交互過程十分低效,我用得最多的仍然是代碼實時補全功能。Copilot 在回答很具體的小微型問題時是非常給力的,比如 utils.js 工具模塊的通用函數提示、函數 jsdoc 文檔的生成以及周邊單元格的邊界討論方面都非常出彩,幾乎不用二次修改。這可能跟 GitHub Copilot 底層大模型的訓練數據有關——掃雷游戲開發已經是一個爛大街的練手項目了,跟平時經常刷到的吃豆子、貪吃蛇、俄羅斯方塊等屬于同一個級別的編程問題,因此數據質量是有保證的,效果也的確不錯。

但是對于一些有難度的處理邏輯,Copilot 就有點力不從心了。本例中的典型代表,當屬單元格遞歸檢索部分的代碼實現。先上代碼:

cell.onmousedown = ({ target, which }) => {/*...*/if (which === 1) { // 左擊// 1. 如果已插旗,則不處理if (cellObj.flagged) return;// 2. 踩雷,游戲結束:if (cellObj.isMine) {/*...*/return;}// 3. 若為安全區域,標記為已檢查searchAround(cellObj, target, lv.col, mines);// 4. 查看是否勝利/*...*/}
});function searchAround(curCell, curDom, colSize, mines) {curCell.checked = true;// Render the current cellcurDom.classList.add('number', `mc-${curCell.mineCount}`);curDom.innerHTML = curCell.mineCount;// 如果是空白單元格,則遞歸顯示周圍的格子,直到遇到非空白單元格if (curCell.mineCount === 0) {curDom.innerHTML = '';curCell.neighbors.forEach(nbId => {const nbCell = mines[nbId - 1];const nbDom = $(`[data-id="${getIJ(nbId, colSize)}"]`);if(!nbCell.checked && !nbCell.flagged && !nbCell.isMine) {searchAround(nbCell, nbDom, colSize, mines);}});}
}

上述遞歸子函數 searchAround() 中,最核心的 L29-L37 其實是我自己寫的,因為在此之前我讓 Copilot 嘗試了不下五次都沒能給出最正確的版本。

這一部分的原始版本其實是 Copilot 根據我的注釋內容補全的,當時它用的是 MouseEvent 實例,將周邊單元格的狀態計算通過重新觸發一次鼠標點擊來實現,看上去是那么的人畜無害:

if(cellObj.mineCount > 0) {// 如果不是空白單元格,則顯示數字target.classList.add('number', `mc-${cellObj.mineCount}`);target.innerHTML = cellObj.mineCount;} else {// 如果是空白單元格,則遞歸顯示周圍的格子target.classList.add('number', 'mc-0');target.innerHTML = '';const colSize = getCurrentLevel(cfgs).col;cellObj.neighbors.forEach(nbId => {const nbCell = mines[nbId - 1];const nbDom = $(`[data-id="${getIJ(nbId, colSize)}"]`);if (!nbCell.checked) {nbDom.dispatchEvent(new MouseEvent('mousedown', { which: 1 }));}});}

結果換到中高級難度時,偶爾就會出現堆棧溢出的情況:

圖 3 利用 Copilot 補全的代碼出現的堆棧溢出的情況截圖

【圖 3 利用 Copilot 補全的代碼出現的堆棧溢出的情況截圖】

雖然報錯代碼定位在了 L14;即便這樣,但憑借對前面問題分解的過分自信,我還是沒往 Copilot 提示錯誤的方向思考,而是認定遺漏了某個邊界條件。再一捋,還真被我找到一個看似合理的解釋:隨機分布地雷時安全邊界未完全閉合,導致算過的區域又竄到另一塊區域重復計算(如圖 6 所示):

圖 4:對比 Windows 掃雷游戲發現的邊界不閉合問題(左上第一個框中區域)

【圖 4:對比 Windows 掃雷游戲發現的邊界不閉合問題(左上第一個框中區域)】

為了驗證這個假設,我還特意試了試 Windows 自帶的掃雷游戲,邊界果然都是完全閉合的:

圖 5:觀察 Windows 自帶的掃雷游戲看到的完全閉合邊界

【圖 5:觀察 Windows 自帶的掃雷游戲看到的完全閉合邊界】

抱著懷疑的態度,我又問了 Copilot 是否是這個原因導致的,它說“很有可能”。這樣一來,假設就得到了“多方驗證”,接下來就是大刀闊斧地重構代碼了:先確定邊界完全閉合的判定條件,然后在初始化雷區時逐一判定,發現一處就重新隨機生成,直到邊界完全閉合。改了一大堆代碼,這是其中兩個核心邏輯:

// Check if the mine distribution is valid
function checkInvalidCorner(mineCells, col) {return mineCells.filter(({ isMine, mineCount }) => !isMine && mineCount === 0).reduce((acc, cell) => {const { id, neighbors } = cell, idLeft = id - 1, idRight = id + 1, idTop = id - col, idBottom = id + col;const cornerChecker = checkCorner(neighbors, mineCells, col);const [foundTL, ij1] = cornerChecker([idTop, idLeft], arr => Math.min(...arr) - 1 - 1);const [foundTR, ij2] = cornerChecker([idTop, idRight], arr => Math.min(...arr) + 1 - 1);const [foundBL, ij3] = cornerChecker([idBottom, idLeft], arr => Math.max(...arr) - 1 - 1);const [foundBR, ij4] = cornerChecker([idBottom, idRight], arr => Math.max(...arr) + 1 - 1);if (foundTL || foundTR || foundBL || foundBR) {const coordinates = [ij1, ij2, ij3, ij4].filter(Boolean).map(c => `(${c})`);acc.push(...coordinates);}return acc;}, []);
}function checkCorner(neighbors, mineCells, col) {return (group, indexCb) => {const nbs = neighbors.filter(nb => group.includes(nb));const inPair = nbs.length === 2;if (!inPair) {return [false];}const bothNearMine = nbs.every(nbId => {const target = mineCells[nbId - 1];return (!!target) && (target.mineCount > 0);});if(!bothNearMine) {return [false];}// 檢查:左上角單元格存在且 mineCount > 0const cornerIndex = indexCb(nbs);const cornerCell = mineCells[cornerIndex];const invalid = (!!cornerCell) && cornerCell.mineCount === 0;if(!invalid) {// 為有效單元格,跳過return [false];}const ij = getIJ(cornerCell.id, col);return [invalid, ij];};
}

如此折騰下來,堆棧溢出的問題明顯少了很多,后臺也能看到重新生成的次數,下一步就是繼續探索新的邊界條件了:

圖 6:根據安全邊界完全閉合的說法重構的游戲界面與控制臺提示信息截圖

【圖 6:根據安全邊界完全閉合的說法重構的游戲界面與控制臺提示信息截圖】

正當我為自己的階段性勝利沾沾自喜時,老天似乎都看不下去了,特意讓我在一次 Windows 原生掃雷游戲中看到了一次邊界也有問題的 特例

圖 7:Windows 掃雷游戲也出現了不完全閉合的安全邊界,分分鐘打臉之前的假設

【圖 7:Windows 掃雷游戲也出現了不完全閉合的安全邊界】

聰明的你沒有看錯,這是剛開局不久第一次探雷的結果:即便框中部分的邊界并沒有“完全”閉合,也絲毫不影響安全區域的最終擴散。之前自信心爆棚的假設驗證環節就這樣不攻自破了。我也才猛然醒悟 Copilot 那句代碼的真正問題:四周的八個單元格依次觸發 mousedown 事件,到最后一個鄰近區域時如果周邊還是沒有地雷,就又會以該點為中心,把此前計算過的區域劃為下一輪計算目標,由此導致循環往復。這說明在遞歸查詢時還應該補充一個狀態位,檢查過的單元格就不要再算下去了,這樣才能從源頭上控制溢出。

順著這個思路,我讓 Copilot 自行生成對應的遞歸實現,結果問了好幾次都不成功:無論使用什么樣的提示詞,無論怎么完善前置信息,Copilot 始終不能跳出當前的代碼邏輯,幫我抽象出一個滿足遞歸調用的新版本:

圖 8:多次卡住 GitHub Copilot 的“高難度”待重構代碼片段

【圖 8:多次卡住 GitHub Copilot 的“高難度”待重構代碼片段】

最終只能我自己動手修復了這個終極 Bug

function searchAround(curCell, curDom, colSize, mines) {curCell.checked = true;// Render the current cellcurDom.classList.add('number', `mc-${curCell.mineCount}`);curDom.innerHTML = curCell.mineCount;// 如果是空白單元格,則遞歸顯示周圍的格子,直到遇到非空白單元格if (curCell.mineCount === 0) {curDom.innerHTML = '';curCell.neighbors.forEach(nbId => {const nbCell = mines[nbId - 1];const nbDom = $(`[data-id="${getIJ(nbId, colSize)}"]`);if(!nbCell.checked && !nbCell.flagged && !nbCell.isMine) {searchAround(nbCell, nbDom, colSize, mines);}});}
}

經此一役,Copilot 在我心中的地位也直線下滑,成功實現了 AI 輔助編程“祛魅”。

種種跡象再次印證了當前 AI 的一個突出問題:無法真正理解補全代碼的具體含義。

按理說,掃雷游戲的開源代碼不算少了,但為什么 Copilot 屢試屢敗呢?這還是跟具體的訓練數據有關,至少采用我這樣遞歸查詢算法的掃雷實現方案明顯不足。到 GitHub 隨手一搜,就看到一段沒有使用遞歸檢索的核心邏輯:

this.reveal1 = function() {/*...*/var row, col;var curCell, nbCell;var stack = [];stack.push(this);this.pushed = true;while (stack.length > 0) {curCell = stack.pop();if (!curCell.isRevealed() && !curCell.isFlagged()) {if (curCell.isMine()) {return false}curCell.setClass(`square open${curCell.getValue()}`);curCell.setRevealed(true);if(!curCell.isHidden()) {if (--remainingSafeCells == 0) {handleGameWinning();return true}if (curCell.getValue() == 0) {// Recursive reveal of neighborsfor (row = -1; row <= 1; row++) {for (col = -1; col <= 1; col++) {nbCell = gameGrid[curCell.getRow() + row][curCell.getCol() + col];if (!nbCell.pushed && !nbCell.isHidden() && !nbCell.isRevealed()) {stack.push(nbCell); // push the neighbor cell to the stacknbCell.pushed = true}}}}}}}/*...*/
}

看吧,人家都是自行維護調用棧,根本不會出現堆棧溢出的情況。

類似的例子還有很多,就不一一引用了,反正承認自己的版本非常小眾且弱雞就是了。

因此,想要真正讓 AI 輔助編程大放異彩,至少現階段還是困難重重:因為它理解不了代碼的真正含義,所以可供選擇的平替方案非常有限:

  1. 要么依靠高質量的精準數據定向投喂,發揮 AI 的相關性推斷優勢;
  2. 要么從算法層面再次突圍:可惜不是所有公司都叫 DeepSeek
  3. 要么就只能像文中的我,自己動手豐衣足食了。

現在再看第八章作者的吐血推薦,真是感覺字字珠璣——

… Last, always, and we mean always, test every function you write.
(……最后,重要的事情說三遍,務必要測一測寫出的每一個函數。)

姜,果然還是老的辣。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/77355.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/77355.shtml
英文地址,請注明出處:http://en.pswp.cn/web/77355.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

游戲狀態管理:用Pygame實現場景切換與暫停功能

游戲狀態管理:用Pygame實現場景切換與暫停功能 在開發游戲時,管理游戲的不同狀態(如主菜單、游戲進行中、暫停等)是非常重要的。這不僅有助于提升玩家的游戲體驗,還能使代碼結構更加清晰。本文將通過一個簡單的示例,展示如何使用Pygame庫來實現游戲中的場景切換和暫停功…

Java后端開發day36--源碼解析:HashMap

&#xff08;以下內容均來自上述課程&#xff09; 1. HashMap&#xff08;一&#xff09; 底層&#xff1a;數組鏈表紅黑樹 1.1 前提準備 查看源碼&#xff1a;選中HashMap–ctrlB 小細節&#xff1a;快捷鍵ctrlf12–跳出目錄結構 藍色圓圈&#xff1a;class 證明是類名粉…

RT-Thread學習筆記(四)

RT-Thread學習筆記 線程間同步信號量信號量的使用和管理動態創建信號量靜態創建信號量獲取信號量信號量同步實列互斥量互斥量的使用和管理互斥量動態創建互斥量靜態創建互斥量獲取和釋放互斥量實例事件集事件集的使用和管理動態創建事件集靜態初始化事件集發送和接收事件事件集…

element ui el-col的高度不一致導致換行

問題&#xff1a;ell-col的高度不一致導致換行&#xff0c;刷新后審查el-col的高度一致 我這邊是el-col寫的span超過了24&#xff0c;自行換行&#xff0c;測試發現初次進入里面的高度渲染的不一致&#xff0c;有的是51px有的是51.5px 問題原因分析 Flex布局換行機制 Elemen…

現代化Android開發:Compose提示信息的最佳封裝方案

在 Android 開發中&#xff0c;良好的用戶反饋機制至關重要。Jetpack Compose 提供了現代化的 UI 構建方式&#xff0c;但提示信息(Toast/Snackbar)的管理往往顯得分散。本文將介紹如何優雅地封裝提示信息&#xff0c;提升代碼可維護性。 一、基礎封裝方案 1. 簡單 Snackbar …

【C++語法】類和對象(2)

4.類和對象&#xff08;2&#xff09; 文章目錄 4.類和對象&#xff08;2&#xff09;類的六個默認成員函數(1)構造函數&#xff1a;構造函數特點含有缺省參數的構造函數構造函數特點&#xff08;續&#xff09;注意事項構造函數補充 前面總結了有關對象概念&#xff0c;對比 C…

【自然語言處理與大模型】vLLM部署本地大模型②

舉例上一篇文章已經過去了幾個月&#xff0c;大模型領域風云變幻&#xff0c;之前的vLLM安裝稍有過時&#xff0c;這里補充一個快速安裝教程&#xff1a; # 第一步&#xff1a;創建虛擬環境并激活進入 conda create -n vllm-0.8.4 python3.10 -y conda activate vllm-0…

26 Arcgis軟件常用工具有哪些

一、畫圖改圖工具&#xff08;矢量編輯&#xff09;? ?挪位置工具&#xff08;移動工具&#xff09;? 干哈的&#xff1f;?選中要素?&#xff08;比如地塊、道路&#xff09;直接拖到新位置&#xff0c;或者用坐標?X/Y偏移?批量移動&#xff0c;適合“整體搬家”。 ?磁…

QNX/LINUX/Android系統動態配置動態庫.so文件日志打印級別的方法

背景 通常我們會在量產的產品上&#xff0c;配置軟件僅打印少量日志&#xff0c;以提升產品的運行性能。同時我們要考慮預留方法讓軟件能夠擁有能力可以在燒錄版本后能夠通過修改默寫配置&#xff0c;打印更多日志。因為量產后的軟件通常開啟熔斷與加密&#xff0c;不能夠輕松…

WebGL圖形編程實戰【4】:光影交織 × 逐片元光照與渲染技巧

現實世界中的物體被光線照射時&#xff0c;會反射一部分光。只有當反射光線進人你的眼睛時&#xff0c;你才能夠看到物體并辯認出它的顏色。 光源類型 平行光&#xff08;Directional Light&#xff09;&#xff1a;光線是相互平行的&#xff0c;平行光具有方向。平行光可以看…

【Hive入門】Hive基礎操作與SQL語法:DDL操作全面指南

目錄 1 Hive DDL操作概述 2 數據庫操作全流程 2.1 創建數據庫 2.2 查看數據庫 2.3 使用數據庫 2.4 修改數據庫 2.5 刪除數據庫 3 表操作全流程 3.1 創建表 3.2 查看表信息 3.3 修改表 3.4 刪除表 4 分區與分桶操作 4.1 分區操作流程 4.2 分桶操作 5 最佳實踐與…

YOLO數據處理

YOLO&#xff08;You Only Look Once&#xff09;的數據處理流程是為了解決目標檢測領域的核心挑戰&#xff0c;核心目標是為模型訓練和推理提供高效、規范化的數據輸入。其設計方法系統性地解決了以下關鍵問題&#xff0c;并對應發展了成熟的技術方案&#xff1a; 一、解決的問…

Ubuntu-Linux中vi / vim編輯文件,保存并退出

1.打開文件 vi / vim 文件名&#xff08;例&#xff1a; vim word.txt &#xff09;。 若權限不夠&#xff0c;則在前方添加 sudo &#xff08;例&#xff1a;sudo vim word.txt &#xff09;來增加權限&#xff1b; 2.進入文件&#xff0c;按 i 鍵進入編輯模式。 3.編輯結…

PCL繪制點云+法線

讀取的點云ASCII碼文件&#xff0c;每行6個數據&#xff0c;3維坐標3維法向 #include <iostream> #include <fstream> #include <vector> #include <string> #include <pcl/point_types.h> #include <pcl/point_cloud.h> #include <pc…

如何在學習通快速輸入答案(網頁版),其他學習平臺通用,手機上快速粘貼

目錄 1、網頁版&#xff08;全平臺通用&#xff09; 2、手機版&#xff08;學習通&#xff0c;其他平臺有可能使用&#xff09; 1、網頁版&#xff08;全平臺通用&#xff09; 1、首先CtrlC復制好答案 2、在學習通的作業里輸入1 3、對準1&#xff0c;然后鼠標右鍵 &#xff…

002 六自由度舵機機械臂——姿態解算理論

00 DH模型的核心概念 【全程干貨【六軸機械臂正逆解計算及仿真示例】】 如何實現機械臂的逆解計算-機器譜-robotway DH模型是機器人運動學建模的基礎方法&#xff0c;通過??四個參數??描述相鄰關節坐標系之間的變換關系。其核心思想是將復雜的空間位姿轉換分解為繞軸旋轉…

pymongo功能整理與基礎操作類

以下是 Python 與 PyMongo 的完整功能整理&#xff0c;涵蓋基礎操作、高級功能、性能優化及常見應用場景&#xff1a; 1. 安裝與連接 (1) 安裝 PyMongo pip install pymongo(2) 連接 MongoDB from pymongo import MongoClient# 基礎連接&#xff08;默認本地&#xff0c;端口…

Trae+DeepSeek學習Python開發MVC框架程序筆記(四):使用sqlite存儲查詢并驗證用戶名和密碼

繼續通過Trae向DeepSeek發問并修改程序&#xff0c;實現程序運行時生成數據庫&#xff0c;用戶在系統登錄頁面輸入用戶名和密碼后&#xff0c;控制器通過模型查詢用戶數據庫表來驗證用戶名和密碼&#xff0c;驗證通過后顯示登錄成功頁面&#xff0c;驗證失敗則顯示登錄失敗頁面…

如何識別金融欺詐行為并進行分析預警

金融行業以其高效便捷的服務深刻改變了人們的生活方式。然而,伴隨技術進步而來的,是金融欺詐行為的日益猖獗。從信用卡盜刷到復雜的龐氏騙局,再到網絡釣魚和洗錢活動,金融欺詐的形式層出不窮,其規模和影響也在不斷擴大。根據全球反欺詐組織(ACFE)的最新報告,僅2022年,…

紛析云:開源財務管理軟件的創新與價值

在企業數字化轉型中&#xff0c;紛析云作為一款優秀的開源財務管理軟件&#xff0c;正為企業財務管理帶來新變革&#xff0c;以下是其核心要點。 一、產品概述與技術架構 紛析云采用微服務架構&#xff0c;功能組件高內聚低耦合&#xff0c;可靈活擴展和定制。前端基于現代框…