网站建设实训感想,微型营销网站制作,dw网页制作表格,私人软件开发公司有哪些文章目录 DIY 实战#xff1a;从扫雷小游戏开发再探问题分解能力3 问题分解实战#xff08;自顶向下#xff09;3.2 页面渲染逻辑3.3 事件绑定逻辑 4 代码实现#xff08;自底向上#xff09;4.1 页面渲染部分4.2 事件绑定部分 写在前面 本篇将利用《Learn AI-assisted Py… 文章目录 DIY 实战从扫雷小游戏开发再探问题分解能力3 问题分解实战自顶向下3.2 页面渲染逻辑3.3 事件绑定逻辑 4 代码实现自底向上4.1 页面渲染部分4.2 事件绑定部分 写在前面 本篇将利用《Learn AI-assisted Python Programming》第七章介绍的问题分解方法完成简版扫雷游戏的后续逻辑分解。由于篇幅过长与 AI 相关的具体交互过程和小结复盘留到下篇介绍敬请关注 DIY 实战从扫雷小游戏开发再探问题分解能力
3 问题分解实战自顶向下
3.2 页面渲染逻辑
接 上篇…… init() 的拆分就暂告一个段落了如下图所示 #mermaid-svg-ZyWvCouNgYVaBFb7 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-ZyWvCouNgYVaBFb7 .error-icon{fill:#552222;}#mermaid-svg-ZyWvCouNgYVaBFb7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZyWvCouNgYVaBFb7 .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-ZyWvCouNgYVaBFb7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZyWvCouNgYVaBFb7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZyWvCouNgYVaBFb7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZyWvCouNgYVaBFb7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZyWvCouNgYVaBFb7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZyWvCouNgYVaBFb7 .marker.cross{stroke:#333333;}#mermaid-svg-ZyWvCouNgYVaBFb7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZyWvCouNgYVaBFb7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZyWvCouNgYVaBFb7 .cluster-label text{fill:#333;}#mermaid-svg-ZyWvCouNgYVaBFb7 .cluster-label span{color:#333;}#mermaid-svg-ZyWvCouNgYVaBFb7 .label text,#mermaid-svg-ZyWvCouNgYVaBFb7 span{fill:#333;color:#333;}#mermaid-svg-ZyWvCouNgYVaBFb7 .node rect,#mermaid-svg-ZyWvCouNgYVaBFb7 .node circle,#mermaid-svg-ZyWvCouNgYVaBFb7 .node ellipse,#mermaid-svg-ZyWvCouNgYVaBFb7 .node polygon,#mermaid-svg-ZyWvCouNgYVaBFb7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZyWvCouNgYVaBFb7 .node .label{text-align:center;}#mermaid-svg-ZyWvCouNgYVaBFb7 .node.clickable{cursor:pointer;}#mermaid-svg-ZyWvCouNgYVaBFb7 .arrowheadPath{fill:#333333;}#mermaid-svg-ZyWvCouNgYVaBFb7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZyWvCouNgYVaBFb7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZyWvCouNgYVaBFb7 .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-ZyWvCouNgYVaBFb7 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-ZyWvCouNgYVaBFb7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZyWvCouNgYVaBFb7 .cluster text{fill:#333;}#mermaid-svg-ZyWvCouNgYVaBFb7 .cluster span{color:#333;}#mermaid-svg-ZyWvCouNgYVaBFb7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZyWvCouNgYVaBFb7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} start init bindEvents renderGameBoard renderStatsInfo generateMineCells 图 4 初步确定的 init() 函数拆分方案
3.3 事件绑定逻辑
虽然页面上划分了三个区域难度选择区、地雷统计区、扫雷面板区但实际需要绑定事件的只有两个统计区的数据更新是和游戏面板同步的因此只拆成两个子函数即可 #mermaid-svg-EP7ZhdJYzrjr6Av5 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-EP7ZhdJYzrjr6Av5 .error-icon{fill:#552222;}#mermaid-svg-EP7ZhdJYzrjr6Av5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-EP7ZhdJYzrjr6Av5 .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-EP7ZhdJYzrjr6Av5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-EP7ZhdJYzrjr6Av5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-EP7ZhdJYzrjr6Av5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-EP7ZhdJYzrjr6Av5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-EP7ZhdJYzrjr6Av5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-EP7ZhdJYzrjr6Av5 .marker.cross{stroke:#333333;}#mermaid-svg-EP7ZhdJYzrjr6Av5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-EP7ZhdJYzrjr6Av5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-EP7ZhdJYzrjr6Av5 .cluster-label text{fill:#333;}#mermaid-svg-EP7ZhdJYzrjr6Av5 .cluster-label span{color:#333;}#mermaid-svg-EP7ZhdJYzrjr6Av5 .label text,#mermaid-svg-EP7ZhdJYzrjr6Av5 span{fill:#333;color:#333;}#mermaid-svg-EP7ZhdJYzrjr6Av5 .node rect,#mermaid-svg-EP7ZhdJYzrjr6Av5 .node circle,#mermaid-svg-EP7ZhdJYzrjr6Av5 .node ellipse,#mermaid-svg-EP7ZhdJYzrjr6Av5 .node polygon,#mermaid-svg-EP7ZhdJYzrjr6Av5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-EP7ZhdJYzrjr6Av5 .node .label{text-align:center;}#mermaid-svg-EP7ZhdJYzrjr6Av5 .node.clickable{cursor:pointer;}#mermaid-svg-EP7ZhdJYzrjr6Av5 .arrowheadPath{fill:#333333;}#mermaid-svg-EP7ZhdJYzrjr6Av5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-EP7ZhdJYzrjr6Av5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-EP7ZhdJYzrjr6Av5 .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-EP7ZhdJYzrjr6Av5 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-EP7ZhdJYzrjr6Av5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-EP7ZhdJYzrjr6Av5 .cluster text{fill:#333;}#mermaid-svg-EP7ZhdJYzrjr6Av5 .cluster span{color:#333;}#mermaid-svg-EP7ZhdJYzrjr6Av5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-EP7ZhdJYzrjr6Av5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} bindEvents bindLevelButtonActions bindCellAction 先看难度选择区的事件绑定逻辑 bindLevelButtonActions()这个比较容易通过切换一个标识类 active 控制按钮本身的样式然后再触发页面初始化函数 start(currentLv) 即可。
重点是每个单元格的事件绑定这是整个扫雷游戏最核心的部分需要仔细讨论每一种可能出现的状态。注册事件首选 mousedown这样可以很方便地利用 event.which 属性知晓鼠标点击的具体按键
event.which 为 1 表示按下了鼠标左键event.which 为 2 表示按下了鼠标滚轮一般很少用到event.which 为 3 表示按下了鼠标右键
由于状态较多这里建议使用排除法先把旁枝末节的情况排除掉剩下的就是核心逻辑了
首先是禁用鼠标右键菜单接着禁用鼠标滚轮操作如果该单元格已经点开了即不是地雷且已经用左键点过的安全单元格就直接中止后续操作对于未考察的单元格分两种情况 如果按下的是鼠标右键则通过标注地雷加上小旗图标同时更新地雷统计数据如果按下的是鼠标左键则又分三种情况 如果已经标记为地雷则中止操作如果是地雷则公布所有地雷禁用所有单元格点击并提示游戏失败如果不是地雷再分两种情况 周围八个单元格存在地雷则根据具体数量添加不同的样式类标记出具体数字周围不存在地雷则依次遍历每一个周边单元格再次按当前按下的是左键即 3.2 步进行递归检索
绘制流程图如下 #mermaid-svg-29fCfKJlV9OxWc7a {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-29fCfKJlV9OxWc7a .error-icon{fill:#552222;}#mermaid-svg-29fCfKJlV9OxWc7a .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-29fCfKJlV9OxWc7a .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-29fCfKJlV9OxWc7a .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-29fCfKJlV9OxWc7a .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-29fCfKJlV9OxWc7a .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-29fCfKJlV9OxWc7a .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-29fCfKJlV9OxWc7a .marker{fill:#333333;stroke:#333333;}#mermaid-svg-29fCfKJlV9OxWc7a .marker.cross{stroke:#333333;}#mermaid-svg-29fCfKJlV9OxWc7a svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-29fCfKJlV9OxWc7a .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-29fCfKJlV9OxWc7a .cluster-label text{fill:#333;}#mermaid-svg-29fCfKJlV9OxWc7a .cluster-label span{color:#333;}#mermaid-svg-29fCfKJlV9OxWc7a .label text,#mermaid-svg-29fCfKJlV9OxWc7a span{fill:#333;color:#333;}#mermaid-svg-29fCfKJlV9OxWc7a .node rect,#mermaid-svg-29fCfKJlV9OxWc7a .node circle,#mermaid-svg-29fCfKJlV9OxWc7a .node ellipse,#mermaid-svg-29fCfKJlV9OxWc7a .node polygon,#mermaid-svg-29fCfKJlV9OxWc7a .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-29fCfKJlV9OxWc7a .node .label{text-align:center;}#mermaid-svg-29fCfKJlV9OxWc7a .node.clickable{cursor:pointer;}#mermaid-svg-29fCfKJlV9OxWc7a .arrowheadPath{fill:#333333;}#mermaid-svg-29fCfKJlV9OxWc7a .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-29fCfKJlV9OxWc7a .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-29fCfKJlV9OxWc7a .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-29fCfKJlV9OxWc7a .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-29fCfKJlV9OxWc7a .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-29fCfKJlV9OxWc7a .cluster text{fill:#333;}#mermaid-svg-29fCfKJlV9OxWc7a .cluster span{color:#333;}#mermaid-svg-29fCfKJlV9OxWc7a div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-29fCfKJlV9OxWc7a :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是 否 否 是 否 是 是 否 是 否 触发 mousedown 事件 禁用右键菜单 禁用滚轮点击 单元格已点开 中止操作 未考察单元格 按下的是右键 按下的是左键 1标注地雷/小旗图标2更新地雷统计数据 已标记为地雷 是否为地雷 中止操作 公布所有地雷禁用所有点击提示游戏失败 周围有地雷 添加数字样式类显示周围地雷数量 遍历周边单元格递归执行 因此事件绑定函数可以拆分成这几个部分 #mermaid-svg-pXKffiWTeFrxsNZi {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-pXKffiWTeFrxsNZi .error-icon{fill:#552222;}#mermaid-svg-pXKffiWTeFrxsNZi .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pXKffiWTeFrxsNZi .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-pXKffiWTeFrxsNZi .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pXKffiWTeFrxsNZi .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pXKffiWTeFrxsNZi .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pXKffiWTeFrxsNZi .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pXKffiWTeFrxsNZi .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pXKffiWTeFrxsNZi .marker.cross{stroke:#333333;}#mermaid-svg-pXKffiWTeFrxsNZi svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pXKffiWTeFrxsNZi .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-pXKffiWTeFrxsNZi .cluster-label text{fill:#333;}#mermaid-svg-pXKffiWTeFrxsNZi .cluster-label span{color:#333;}#mermaid-svg-pXKffiWTeFrxsNZi .label text,#mermaid-svg-pXKffiWTeFrxsNZi span{fill:#333;color:#333;}#mermaid-svg-pXKffiWTeFrxsNZi .node rect,#mermaid-svg-pXKffiWTeFrxsNZi .node circle,#mermaid-svg-pXKffiWTeFrxsNZi .node ellipse,#mermaid-svg-pXKffiWTeFrxsNZi .node polygon,#mermaid-svg-pXKffiWTeFrxsNZi .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pXKffiWTeFrxsNZi .node .label{text-align:center;}#mermaid-svg-pXKffiWTeFrxsNZi .node.clickable{cursor:pointer;}#mermaid-svg-pXKffiWTeFrxsNZi .arrowheadPath{fill:#333333;}#mermaid-svg-pXKffiWTeFrxsNZi .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-pXKffiWTeFrxsNZi .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-pXKffiWTeFrxsNZi .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-pXKffiWTeFrxsNZi .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-pXKffiWTeFrxsNZi .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-pXKffiWTeFrxsNZi .cluster text{fill:#333;}#mermaid-svg-pXKffiWTeFrxsNZi .cluster span{color:#333;}#mermaid-svg-pXKffiWTeFrxsNZi div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-pXKffiWTeFrxsNZi :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 为右键 为左键 是 否 有雷 无雷 bindEvents bindLevelButtonActions bindCellMousedownActions handlePopupAndScrollWheel 判定左右键 handleRightClick 判定是否为地雷 showMinesAndCleanup 周围是否有雷 renderMineCount searchAround递归调用 这样一来事件绑定的问题分解就全部完成了。
4 代码实现自底向上
终于来到激动人心的代码实现环节了根据刚才的分解情况按照自底向上依次实现各个叶子级功能点
4.1 页面渲染部分
先是页面渲染的三个子函数 #mermaid-svg-ueVEiKx46BwfEJw3 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-ueVEiKx46BwfEJw3 .error-icon{fill:#552222;}#mermaid-svg-ueVEiKx46BwfEJw3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ueVEiKx46BwfEJw3 .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-ueVEiKx46BwfEJw3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ueVEiKx46BwfEJw3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ueVEiKx46BwfEJw3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ueVEiKx46BwfEJw3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ueVEiKx46BwfEJw3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ueVEiKx46BwfEJw3 .marker.cross{stroke:#333333;}#mermaid-svg-ueVEiKx46BwfEJw3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ueVEiKx46BwfEJw3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ueVEiKx46BwfEJw3 .cluster-label text{fill:#333;}#mermaid-svg-ueVEiKx46BwfEJw3 .cluster-label span{color:#333;}#mermaid-svg-ueVEiKx46BwfEJw3 .label text,#mermaid-svg-ueVEiKx46BwfEJw3 span{fill:#333;color:#333;}#mermaid-svg-ueVEiKx46BwfEJw3 .node rect,#mermaid-svg-ueVEiKx46BwfEJw3 .node circle,#mermaid-svg-ueVEiKx46BwfEJw3 .node ellipse,#mermaid-svg-ueVEiKx46BwfEJw3 .node polygon,#mermaid-svg-ueVEiKx46BwfEJw3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ueVEiKx46BwfEJw3 .node .label{text-align:center;}#mermaid-svg-ueVEiKx46BwfEJw3 .node.clickable{cursor:pointer;}#mermaid-svg-ueVEiKx46BwfEJw3 .arrowheadPath{fill:#333333;}#mermaid-svg-ueVEiKx46BwfEJw3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ueVEiKx46BwfEJw3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ueVEiKx46BwfEJw3 .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-ueVEiKx46BwfEJw3 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-ueVEiKx46BwfEJw3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ueVEiKx46BwfEJw3 .cluster text{fill:#333;}#mermaid-svg-ueVEiKx46BwfEJw3 .cluster span{color:#333;}#mermaid-svg-ueVEiKx46BwfEJw3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ueVEiKx46BwfEJw3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 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);
}注意 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 {Arraynumber} 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];
}$$ 是一个简化后的工具函数从 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;
}上述代码有两个地方需要注意 地雷的乱序算法使用随机值实现() Math.random() - 0.5 快速生成 [1, n] 的正整数数组 /*** Generates an array of numbers from 1 to size (inclusive).* param {number} size The size of the range.* returns {Arraynumber} An array of numbers from 0 to size.*/
export function range(size) {return [...Array(size).keys()].map(n n 1);
}紧接着填充状态值 mineCount 和 neighbors
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, i1]、列数范围是 [j-1, j1]。现在考虑边框则需要用 Math.max 和 Math.min 限制一下。这个写法其实是 Copilot 根据我的注释自动生成的。可见 Copilot 在小范围内对这样非常确定的需求理解得很到位我们只需要略微检查一下边界条件的取值就行了。
4.2 事件绑定部分
再来回顾一下事件绑定逻辑的总结构 #mermaid-svg-OEhDMXjNKoBQdwnO {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-OEhDMXjNKoBQdwnO .error-icon{fill:#552222;}#mermaid-svg-OEhDMXjNKoBQdwnO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OEhDMXjNKoBQdwnO .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-OEhDMXjNKoBQdwnO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OEhDMXjNKoBQdwnO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OEhDMXjNKoBQdwnO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OEhDMXjNKoBQdwnO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OEhDMXjNKoBQdwnO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OEhDMXjNKoBQdwnO .marker.cross{stroke:#333333;}#mermaid-svg-OEhDMXjNKoBQdwnO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OEhDMXjNKoBQdwnO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-OEhDMXjNKoBQdwnO .cluster-label text{fill:#333;}#mermaid-svg-OEhDMXjNKoBQdwnO .cluster-label span{color:#333;}#mermaid-svg-OEhDMXjNKoBQdwnO .label text,#mermaid-svg-OEhDMXjNKoBQdwnO span{fill:#333;color:#333;}#mermaid-svg-OEhDMXjNKoBQdwnO .node rect,#mermaid-svg-OEhDMXjNKoBQdwnO .node circle,#mermaid-svg-OEhDMXjNKoBQdwnO .node ellipse,#mermaid-svg-OEhDMXjNKoBQdwnO .node polygon,#mermaid-svg-OEhDMXjNKoBQdwnO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-OEhDMXjNKoBQdwnO .node .label{text-align:center;}#mermaid-svg-OEhDMXjNKoBQdwnO .node.clickable{cursor:pointer;}#mermaid-svg-OEhDMXjNKoBQdwnO .arrowheadPath{fill:#333333;}#mermaid-svg-OEhDMXjNKoBQdwnO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-OEhDMXjNKoBQdwnO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-OEhDMXjNKoBQdwnO .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-OEhDMXjNKoBQdwnO .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-OEhDMXjNKoBQdwnO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-OEhDMXjNKoBQdwnO .cluster text{fill:#333;}#mermaid-svg-OEhDMXjNKoBQdwnO .cluster span{color:#333;}#mermaid-svg-OEhDMXjNKoBQdwnO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-OEhDMXjNKoBQdwnO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 为右键 为左键 是 否 有雷 无雷 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 的交互过程也梳理一下结果又写了这么多内容只有放到下一篇继续了。
未完待续