【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.mzph.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;可灵活扩展和定制。前端基于现代框…