【AI 加持下的 Python 编程实战 2_10】DIY 拓展:从扫雷小游戏开发再探问题分解与 AI 代码调试能力(中)

文章目录

  • DIY 实战:从扫雷小游戏开发再探问题分解能力
    • 3 问题分解实战(自顶向下)
      • 3.2 页面渲染逻辑
      • 3.3 事件绑定逻辑
    • 4 代码实现(自底向上)
      • 4.1 页面渲染部分
      • 4.2 事件绑定部分

写在前面
本篇将利用《Learn AI-assisted Python Programming》第七章介绍的问题分解方法,完成简版扫雷游戏的后续逻辑分解。由于篇幅过长,与 AI 相关的具体交互过程和小结复盘留到下篇介绍,敬请关注!

DIY 实战:从扫雷小游戏开发再探问题分解能力

3 问题分解实战(自顶向下)

3.2 页面渲染逻辑

(接 上篇)…… init() 的拆分就暂告一个段落了,如下图所示:

start
init
bindEvents
renderGameBoard
renderStatsInfo
generateMineCells

图 4 初步确定的 init() 函数拆分方案

3.3 事件绑定逻辑

虽然页面上划分了三个区域:难度选择区、地雷统计区、扫雷面板区,但实际需要绑定事件的只有两个,统计区的数据更新是和游戏面板同步的,因此只拆成两个子函数即可:

bindEvents
bindLevelButtonActions
bindCellAction

先看难度选择区的事件绑定逻辑 bindLevelButtonActions(),这个比较容易,通过切换一个标识类 active 控制按钮本身的样式,然后再触发页面初始化函数 start(currentLv) 即可。

重点是每个单元格的事件绑定,这是整个扫雷游戏最核心的部分,需要仔细讨论每一种可能出现的状态。注册事件首选 mousedown,这样可以很方便地利用 event.which 属性知晓鼠标点击的具体按键:

  • event.which1 表示按下了鼠标左键;
  • event.which2 表示按下了鼠标滚轮(一般很少用到);
  • event.which3 表示按下了鼠标右键;

由于状态较多,这里建议使用排除法,先把旁枝末节的情况排除掉,剩下的就是核心逻辑了:

  1. 首先是禁用鼠标右键菜单;
  2. 接着禁用鼠标滚轮操作;
  3. 如果该单元格已经点开了(即不是地雷,且已经用左键点过的安全单元格),就直接中止后续操作;
  4. 对于未考察的单元格,分两种情况:
    1. 如果按下的是鼠标右键,则通过标注地雷加上小旗图标,同时更新地雷统计数据;
    2. 如果按下的是鼠标左键,则又分三种情况:
      1. 如果已经标记为地雷,则中止操作;
      2. 如果是地雷,则公布所有地雷,禁用所有单元格点击,并提示游戏失败;
      3. 如果不是地雷,再分两种情况:
        1. 周围八个单元格存在地雷,则根据具体数量添加不同的样式类,标记出具体数字;
        2. 周围不存在地雷,则依次遍历每一个周边单元格,再次按当前按下的是左键(即 3.2 步)进行递归检索;

绘制流程图如下:

触发 mousedown 事件
禁用右键菜单
禁用滚轮点击
单元格已点开
中止操作
未考察单元格
按下的是右键
按下的是左键
1标注地雷/小旗图标
2更新地雷统计数据
已标记为地雷
是否为地雷
中止操作
公布所有地雷
禁用所有点击
提示游戏失败
周围有地雷
添加数字样式类
显示周围地雷数量
遍历周边单元格
递归执行

因此,事件绑定函数可以拆分成这几个部分:

为右键
为左键
有雷
无雷
bindEvents
bindLevelButtonActions
bindCellMousedownActions
handlePopupAndScrollWheel
判定左右键
handleRightClick
判定是否为地雷
showMinesAndCleanup
周围是否有雷
renderMineCount
searchAround递归调用

这样一来,事件绑定的问题分解就全部完成了。

4 代码实现(自底向上)

终于来到激动人心的代码实现环节了!根据刚才的分解情况,按照自底向上依次实现各个叶子级功能点:

4.1 页面渲染部分

先是页面渲染的三个子函数:

init
renderGameBoard
renderStatsInfo
generateMineCells

对应代码:

let mineFound = 0;
function init(lv) {// 1. create table elementsconst doms = renderGameBoard(lv);// 2. render stats info$('#mineCount').innerHTML = lv.mine;$('#mineFound').innerHTML = mineFound;// 3. create mine arrayconst mines = generateMineCells(lv, doms);return mines;
}

先实现页面单元格的动态渲染函数 renderGameBoard(lv)

function renderGameBoard({ row, col }) {const table = $('.gameBoard');table.innerHTML = ''; // reset table contentconst fragment = document.createDocumentFragment();for (let i = 1; i <= row; i++) {const tr = document.createElement('tr');for (let j = 1; j <= col; j++) {const td = document.createElement('td');td.dataset.id = `${i},${j}`;td.classList.add('cell');tr.appendChild(td);}fragment.appendChild(tr);}table.appendChild(fragment);return $$('.cell');
}

注意:

  1. td.dataset.id 设置的是单元格 ID 坐标,它和状态矩阵总编号之间转换关系可以写入 utils.js 工具模块:

    /*** Converts a 2D array index to a 1D array index.* @param {string} ij  The string representation of the 2D index, e.g., "1,2".* @param {number} col The number of columns in the 2D array.* @returns {number} The 1D index corresponding to the 2D index.*/
    export function getId(ij, col) {const [i, j] = ij.split(',').map(n => parseInt(n, 10));return (i - 1) * col + j;
    }/*** Converts a 1D array index to a 2D array index.* @param {number} id The 1D index.* @param {number} col The number of columns in the 2D array.* @returns {Array<number>} An array containing the row and column indices.*/
    export function getIJ(id, col) {const j = id % col === 0 ? col : id % col;const i = (id - j) / col + 1;return [i, j];
    }
    
  2. $$ 是一个简化后的工具函数,从 utils.js 模块导入:

    /*** Selects all elements matching a CSS selector.* @param {string} selector The CSS selector to match elements.* @returns {NodeList} A NodeList of elements matching the selector.*/
    export const $$ = document.querySelectorAll.bind(document);
    

接着实现地雷统计指标的初始化 renderStatsInfo。由于只有两句话,因此不单独创建新的子函数:

// 2. render stats info
$('#mineCount').innerHTML = lv.mine;
$('#mineFound').innerHTML = mineFound;

然后是渲染部分的最后一项 generateMineCells(lv, doms)

function generateMineCells(lv, doms) {// 1. create mine cellsconst mines = initMineCells(lv);// 2. populate neighboring idspopulateNeighboringIds(mines, lv, doms);return mines;
}

这里之所以又分出两个子函数,是因为实现过程中发现可以将部分点击事件的逻辑(例如计算周边区域的地雷数)分摊到状态矩阵的初始化来处理,没必要在每次按下鼠标时再算。因此,对于每个状态矩阵的元素而言,还应该有个新增属性 neighbors,用于存放周边元素 ID 的子数组。于是 initMineCells 负责生成状态矩阵,populateNeighboringIds 负责填充每个状态元素的初始状态值(当前位置的周边地雷数、紧邻单元格的 ID 数组):

function initMineCells({row, col, mine}) {const size = row * col; // total number of cellsconst mines = range(size).sort(() => Math.random() - 0.5).slice(-mine);// console.log(mines);const mineCells = range(size).map(id => {const isMine = mines.includes(id),mineCount = isMine ? 9 : 0,neighbors = isMine ? null : []; // neighbors of the cellreturn {id,isMine,mineCount,neighbors, // neighbors of the cellchecked: false, // whether the cell is checked or notflagged: false // whether the cell is flagged or not};});return mineCells;
}

上述代码有两个地方需要注意:

  1. 地雷的乱序算法:使用随机值实现:() => Math.random() - 0.5

  2. 快速生成 [1, n] 的正整数数组:

    /*** Generates an array of numbers from 1 to size (inclusive).* @param {number} size The size of the range.* @returns  {Array<number>} An array of numbers from 0 to size.*/
    export function range(size) {return [...Array(size).keys()].map(n => n + 1);
    }
    

紧接着填充状态值 mineCountneighbors

function populateNeighboringIds(mineCells, {col, row}, doms) {const safeCells = mineCells.filter(({ isMine }) => !isMine);// 1. Get neighbor ids for each cellsafeCells.forEach(cell => {const [i, j] = getIJ(cell.id, col);for (let r = Math.max(1, i - 1), rows = Math.min(row, i + 1); r <= rows; r++) {for (let c = Math.max(1, j - 1), cols = Math.min(col, j + 1); c <= cols; c++) {if (r === i && c === j) continue;const neighborId = getId(`${r},${c}`, col);cell.neighbors.push(neighborId);}}});// 2. Calculate total number of neighboring minessafeCells.forEach(cell => {// get neighbor ids for each cellconst mineCount = cell.neighbors.reduce((acc, neighborId) => {const {isMine} = mineCells[neighborId - 1];return acc + (isMine ? 1 : 0);}, 0);cell.mineCount = mineCount;});
}

注意,这里出现了第一个比较繁琐的逻辑(L6–L7):判定周边单元格的上、下、左、右边界。如果当前单元格坐标为 (i, j),不考虑雷区边框的情况下,其周边单元格的行号范围是 [i-1, i+1]、列数范围是 [j-1, j+1]。现在考虑边框,则需要用 Math.maxMath.min 限制一下。这个写法其实是 Copilot 根据我的注释自动生成的。可见 Copilot 在小范围内对这样非常确定的需求理解得很到位,我们只需要略微检查一下边界条件的取值就行了。

4.2 事件绑定部分

再来回顾一下事件绑定逻辑的总结构:

为右键
为左键
有雷
无雷
bindEvents
bindLevelButtonActions
bindCellMousedownActions
handlePopupAndScrollWheel
判定左右键
which = 3
handleRightClick
判定是否为地雷
cell.isMine = true
showMinesAndCleanup
周围是否有雷
mineCount = 0
renderMineCount
searchAround
递归调用

由于层次过深,盲目按照自底向上的思路实现子函数可行性不大,因为还没有对每个函数的参数及返回值做进一步确认。因此这里还是自顶向下实现。

先来看最外层:

function bindEvents(lv, mines) {// when selecting a levelbindLevelButtonActions();// when clicking on the game board$('.gameBoard').onmousedown = (ev) => {ev.preventDefault();if(ev.target.classList.contains('gameBoard')) {// 禁用 table 元素上的右键菜单ev.target.oncontextmenu = (e) => {e.preventDefault();};}};// when clicking on cellsbindCellMousedownActions(lv, mines);
}

之所以中间多了一部分,是因为实测时发现单元格之间还存在少量间隔,如果不小心在这些地方点击右键,仍然会出现上下文菜单,因此特地做了补救。

接着先来实现难度选择区的事件绑定:

function bindLevelButtonActions() {const btns = Array.from($$('.level [data-level]'));btns.forEach((btn, _, arr) => {btn.onclick = ({ target }) => {// toggle active classarr.forEach(bt => (target !== bt) ?bt.classList.remove('active') :bt.classList.add('active'));// reset mines foundmineFound = 0;currentLv = getCurrentLevel(target.dataset.level);// reload gamestart(currentLv);};});// when clicking on the restart button$('.restart').onclick = (ev) => {ev.preventDefault();mineFound = 0; // reset mine found countstart(currentLv);ev.target.classList.add('hidden');};
}

这里又增补了一个按钮:.restart。这是每局结束时才会出现的按钮,专门用于重新开始游戏。同理,也是实测时发现的细节。

最后是扫雷区的鼠标事件 bindCellMousedownActions

function bindCellMousedownActions(lv, mines) {// when clicking on cells$$('.cell').forEach(cell => {cell.onmousedown = ({ target, which }) => {// 禁用右键菜单target.oncontextmenu = e => e.preventDefault();// 禁用鼠标滚轮if(which === 2) return;const cellObj = findMineCellById(target.dataset.id, lv, mines);if(cellObj.checked) {// already checked or flaggedconsole.log('Already checked, abort');return; }if (which === 3) {// 右击:添加/删除地雷标记handleRightClick(target, lv, mines);return;}if (which === 1) {// 左击// 1. 如果已插旗,则不处理if (cellObj.flagged) {console.log('Already flagged, abort');return;}// 2. 踩雷,游戏结束:if (cellObj.isMine) {showMinesAndCleanup(target, mines, lv);// 提示重启游戏setTimeout(() => {$('.restart').classList.remove('hidden');alert('游戏结束!你踩到地雷了!');}, 0);return;}// 3. 若为安全区域,标记为已检查searchAround(cellObj, target, lv.col, mines);// 4. 查看是否胜利const allChecked = mines.filter(e => !e.isMine && !e.checked).length === 0;if (allChecked) {congratulateVictory(mines, lv);}}};});
}

根据问题拆分情况,这里又分出了 5 个具体的子函数,除了 findMineCellById 是临时新增外,其余都是游戏运行必不可少的核心逻辑:

// 1. 根据单元格坐标 id 获取对应的状态矩阵元素
function findMineCellById(id, {col}, mines) {const index = getId(id, col) - 1;return mines[index];
}// 2. 右键标记为地雷以及取消地雷标记的处理逻辑
function handleRightClick(target, lv, mines) {target.classList.toggle('mine');target.classList.toggle('ms-flag');const cellObj = findMineCellById(target.dataset.id, lv, mines);cellObj.flagged = !cellObj.flagged; // toggle flagged status// 更新地雷标记数if (cellObj.flagged) {$('#mineFound').innerHTML = (++mineFound);} else {$('#mineFound').innerHTML = (--mineFound);}
}// 3. 踩到地雷时的处理逻辑
function showMinesAndCleanup(target, mines, lv) {// 1. 标记当前踩雷的单元格target.classList.add('fail');// 2. 公布所有地雷showFinalResult(mines, lv);
}// 4. 游戏胜利的处理逻辑
function congratulateVictory(mines, lv) {showFinalResult(mines, lv);setTimeout(() => {alert('恭喜你,成功扫除所有地雷!');$('.restart').classList.remove('hidden');}, 0);
}
function showFinalResult(mines, lv) {// 1. 渲染出所有地雷renderAllMines(mines, lv.col);// 2. 标记所有单元格为已检查(防止误操作)mines.forEach(mine => mine.checked = true);// 3. 所有标记正确的单元格背景色变为绿色renderAllCorrectFlagged(mines, lv);
}// 5. 当前单元格及周边都没有地雷时的处理逻辑
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);}});}
}

这样就实现了所有的处理逻辑,完整代码及最终页面已经放到了 InsCode 上,感兴趣的朋友可以 Fork 到本地试试。

本来计划把后续和 Copilot 的交互过程也梳理一下,结果又写了这么多内容,只有放到下一篇继续了。

(未完待续)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/902835.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

微信小程序开发1------微信小程序中的消息提示框总结

微信小程序中的消息提示框主要分为以下几种&#xff1a; 1. wx.showToast(Object object) 功能&#xff1a; 显示消息提示框&#xff0c;一般用于显示操作结果、状态等。 特点&#xff1a; 提示框显示在屏幕中间&#xff0c;持续一段时间后自动消失&#xff08;默认1.5秒&…

AI 场景落地:API 接口服务 VS 本地部署,哪种更适合?

在当前 AI 技术迅猛发展的背景下&#xff0c;企业在实现 AI 场景落地时&#xff0c;面临着一个关键抉择&#xff1a;是选择各大厂商提供的 API 接口服务&#xff0c;还是进行本地化部署&#xff1f;这不仅关乎成本、性能和安全性&#xff0c;还涉及到技术架构、数据治理和长期战…

Android 加壳应用运行流程 与 生命周期类处理方案

版权归作者所有&#xff0c;如有转发&#xff0c;请注明文章出处&#xff1a;https://cyrus-studio.github.io/blog/ DexClassLoader DexClassLoader 可以加载任意路径下的 dex&#xff0c;或者 jar、apk、zip 文件&#xff08;包含classes.dex&#xff09;。常用于插件化、热…

c++进阶——类与继承

文章目录 继承继承的基本概念继承的基本定义继承方式继承的一些注意事项 继承类模板 基类和派生类之间的转换继承中的作用域派生类的默认成员函数默认构造函数拷贝构造赋值重载析构函数默认成员函数总结 不能被继承的类继承和友元继承与静态成员多继承及其菱形继承问题继承模型…

GAEA情感坐标背后的技术原理

基于GAEA的去中心化物理基础设施网络&#xff08;DePIN&#xff09;&#xff0c;用户有机会在GAEA平台上获得宝贵的数据共享积分。为了提升这些洞察的丰富性&#xff0c;用户必须花费一定数量的积分&#xff0c;将过去的网络数据与当前的情感数据绑定&#xff0c;从而产生一种新…

图形编辑器基于Paper.js教程27:对图像描摹的功能实现,以及参数调整

本篇文章来讲一下 图像描摹的功能的实现。 我们知道要雕刻图片可以通过分析图片的像素来生成相应的gcode进行雕刻&#xff0c;但如果你想要将图片转换为线稿进行雕刻&#xff0c;这个时候就要从图片中提取出 线稿。 例如下面的图片&#xff1a; 你想要获取到这个图片的线稿&…

人工智能与机器学习,谁是谁的子集 —— 再谈智能的边界与演进路径

人工智能&#xff08;Artificial Intelligence, AI&#xff09;作为当代最具影响力的前沿技术之一&#xff0c;常被大众简化为 “深度学习” 或 “大模型” 等标签。然而&#xff0c;这种简化认知往往掩盖了AI技术内部结构的复杂性与多样性。事实上&#xff0c;AI并非单一方法的…

Oracle_开启归档日志和重做日志

在Oracle中&#xff0c;类似于MySQL的binlog的机制是归档日志&#xff08;Archive Log&#xff09;和重做日志&#xff08;Redo Log&#xff09; 查询归档日志状态 SELECT log_mode FROM v$database; – 输出示例&#xff1a; – LOG_MODE – ARCHIVELOG (表示已开启) – NO…

IDEA编写flinkSQL(快速体验版本,--无需配置环境)

相关资料 文档内容链接地址datagen生成器https://nightlies.apache.org/flink/flink-docs-release-1.16/docs/connectors/table/datagen/print 生成器https://nightlies.apache.org/flink/flink-docs-release-1.16/docs/connectors/table/print/ 准备工作 优点就是下载个ide…

基于AI技术的高速公路交通引流系统设计与应用研究

基于AI技术的高速公路交通引流系统设计与应用研究 1. 研究背景与意义 1.1 交通系统演化脉络 1.1.1 发展阶段划分 机械化时代&#xff08;1950-1990&#xff09;&#xff1a;固定式信号控制信息化时代&#xff08;1991-2010&#xff09;&#xff1a;SCATS/SCOOT系统智能化时代…

NEGATIVE LABEL GUIDED OOD DETECTION WITH PRETRAINED VISION-LANGUAGE MODELS

1. 介绍: 这篇论文也是基于CLIP通过后处理的方法实现的OOD的检测,但是设计点在于,之前的方法是使用的ID的类别,这篇工作是通过添加一些在语义上非常不同于ID的类别的外分布类来做的OOD检测。 CLIP做OOD检测的这个系列里面我看的以及记录的第一篇就是MCM的方法,这也是确实是…

Linux 网络基础三 (数据链路层协议:以太网协议、ARP 协议)

一、以太网 两个不同局域网的主机传递数据并不是直接传递的&#xff0c;而是通过路由器 “一跳一跳” 的传递过去。 跨网络传输的本质&#xff1a;由无数个局域网&#xff08;子网&#xff09;转发的结果。 所以&#xff0c;要理解数据跨网络转发原理就要先理解一个局域网中数…

Azure Data Factory ETL设计与调度最佳实践

一、引言 在Azure Data Factory (ADF) 中&#xff0c;调度和设计ETL&#xff08;抽取、转换、加载&#xff09;过程需要综合考量多方面因素&#xff0c;以确保数据处理高效、可扩展、可靠且易于维护。以下将详细介绍相关关键考虑因素、最佳实践&#xff0c;并辅以具体示例说明…

非序列实现MEMS聚焦功能

zemax非序列模式下有MEMS,但是没有对应的代码。无法修改成自己需要的功能 以下是实现MEMS聚焦功能: #include <windows.h> #include <cmath> #include <stdio.h> #include <string.h> #include <algorithm> #undef max #undef min#define D…

android studio sdk unavailable和Android 安装时报错:SDK emulator directory is missing

md 网上说的都是更换proxy代理什么的&#xff0c;换网的&#xff0c;还有一些二其他乱七八糟的&#xff0c;根本没用&#xff0c;感觉很多就是解决不了问题&#xff0c;还贼多贼一致&#xff0c;同质化&#xff0c;感觉很坑人&#xff0c;让人觉得他们和我的一样的&#xff0c;…

三维重建模块VR,3DCursor,MPR与VR的坐标转换

MPR里的reslicecursor 的坐标与 vtkimage 坐标一致。 但三维窗格里的vtkvolume 的坐标是相对坐标&#xff0c;坐标值依然是MM单位。 用中心点的偏移量比较容易实现&#xff0c;交互中Reslicercursor中心点 距离 vtkimagedata 的中心点 的偏移量&#xff0c;用于vtkvolume即可…

Python Cookbook-6.9 快速复制对象

任务 为了使用 copy.copy&#xff0c;需要实现特殊方法__copy__。而且你的类的__init__比较耗时所以你希望能够绕过它并获得一个“空的”未初始化的类实例。 解决方案 下面的解决方案可同时适用于新风格和经典类: def empty_copy(obj):class Empty(obj.__class__):def __in…

kubernets集群的安装-node节点安装-(简单可用)-超详细

一、kubernetes 1、简介 kubernetes&#xff0c;简称K8s&#xff08;库伯内特&#xff09;&#xff0c;是用8代替名字中间的8个字符“ubernete”而成的缩写 云计算的三种主要服务模式——基础设施即服务&#xff08;IaaS&#xff09;、平台即服务&#xff08;PaaS&#xff0…

【Linux学习笔记】进程的fork创建 exit终止 wait等待

【Linux学习笔记】进程的fork创建 exit终止 wait等待 &#x1f525;个人主页&#xff1a;大白的编程日记 &#x1f525;专栏&#xff1a;Linux学习笔记 文章目录 【Linux学习笔记】进程的fork创建 exit终止 wait等待前言1.进程创建1.1 fork函数初识1.2fork函数返回值1.3写时拷…

鸿蒙应用开发证书考试的一点想法

一、介绍&#xff1a; 直接上图 二、体验后的想法&#xff1a; 1.知识点在指南API参考最佳实践里面找 2.没有明确说明考试不能查第1点的文档&#xff0c;但是考试只有1个小时&#xff0c;合理分配时间 3.切屏三次后自动提交要注意&#xff0c;每月3次机会下月又有3次机会&a…