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

news2025/4/25 18:58:15

文章目录

  • 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 elements
    const doms = renderGameBoard(lv);

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

    // 3. create mine array
    const mines = generateMineCells(lv, doms);

    return mines;
}

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

function renderGameBoard({ row, col }) {
    const table = $('.gameBoard');
    table.innerHTML = ''; // reset table content
    const 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 cells
    const mines = initMineCells(lv);
    
    // 2. populate neighboring ids
    populateNeighboringIds(mines, lv, doms);
    
    return mines;
}

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

function initMineCells({row, col, mine}) {
    const size = row * col; // total number of cells
    const 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 cell
            return {
                id,
                isMine,
                mineCount,
                neighbors, // neighbors of the cell
                checked: false, // whether the cell is checked or not
                flagged: 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 cell
    safeCells.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 mines
    safeCells.forEach(cell => {
        // get neighbor ids for each cell
        const 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 level
    bindLevelButtonActions();

    // 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 cells
    bindCellMousedownActions(lv, mines);
}

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

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

function bindLevelButtonActions() {
    const btns = Array.from($$('.level [data-level]'));
    btns.forEach((btn, _, arr) => {
        btn.onclick = ({ target }) => {
            // toggle active class
            arr.forEach(bt => (target !== bt) ?
                bt.classList.remove('active') :
                bt.classList.add('active'));
            // reset mines found
            mineFound = 0;
            currentLv = getCurrentLevel(target.dataset.level);
            // reload game
            start(currentLv);
        };
    });

    // when clicking on the restart button
    $('.restart').onclick = (ev) => {
        ev.preventDefault();
        mineFound = 0; // reset mine found count
        start(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 flagged
                console.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 cell
    curDom.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.coloradmin.cn/o/2342637.html

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

相关文章

如何在 Odoo 18 中配置自动化动作

如何在 Odoo 18 中配置自动化动作 Odoo是一款多功能的业务管理平台&#xff0c;旨在帮助各种规模的企业更高效地处理日常运营。凭借其涵盖销售、库存、客户关系管理&#xff08;CRM&#xff09;、会计和人力资源等领域的多样化模块&#xff0c;Odoo 简化了业务流程&#xff0c…

node.js 实战——(Http 知识点学习)

HTTP 又称为超文本传输协议 是一种基于TCP/IP的应用层通信协议&#xff1b;这个协议详细规定了 浏览器 和万维网 服务器 之间互相通信的规则。协议中主要规定了两个方面的内容&#xff1a; 客户端&#xff1a;用来向服务器发送数据&#xff0c;可以被称之为请求报文服务端&am…

新市场环境下新能源汽车电流传感技术发展前瞻

新能源革命重构产业格局 在全球碳中和战略驱动下&#xff0c;新能源汽车产业正经历结构性变革。国际清洁交通委员会&#xff08;ICCT&#xff09;最新报告显示&#xff0c;2023年全球新能源汽车渗透率突破18%&#xff0c;中国市场以42%的市占率持续领跑。这种产业变革正沿着&q…

fastjson使用parseObject转换成JSONObject出现将字符特殊字符解析解决

现象&#xff1a;将字符串的${TARGET_VALUE}转换成NULL字符串了问题代码&#xff1a; import com.alibaba.fastjson.JSON;JSONObject config JSON.parseObject(o.toString()); 解决方法&#xff1a; 1.更换fastjson版本 import com.alibaba.fastjson2.JSON;或者使用其他JS…

【安装neo4j-5.26.5社区版 完整过程】

1. 安装java 下载 JDK21-windows官网地址 配置环境变量 在底下的系统变量中新建系统变量&#xff0c;变量名为JAVA_HOME21&#xff0c;变量值为JDK文件夹路径&#xff0c;默认为&#xff1a; C:\Program Files\Java\jdk-21然后在用户变量的Path中&#xff0c;添加下面两个&am…

机器人项目管理新风口:如何高效推动智能机器人研发?

在2025年政府工作报告中&#xff0c;“智能机器人”首次被正式纳入国家发展战略关键词。从蛇年春晚的秧歌舞机器人惊艳亮相&#xff0c;到全球首个人形机器人马拉松的热议&#xff0c;智能机器人不仅成为科技前沿的焦点&#xff0c;也为产业升级注入了新动能。而在热潮背后&…

【Linux】网络基础和socket(4)

1.网络通信&#xff08;app\浏览器、小程序&#xff09; 2.网络通信三要素&#xff1a; IP&#xff1a;计算机在网络上唯一标识&#xff08;ipv4:4个字段&#xff0c;每段最大255 IPV6:16进制&#xff09; 端口&#xff1a;计算机应用或服务唯一标识 ssh提供远程安全连接…

大数据可能出现的bug之flume

一、vi /software/flume/conf/dir_to_logger.conf配置文件 问题的关键: Dir的D写成了小写 另一个终端里面的东西一直在监听状态下无法显示 原来是vi /software/flume/conf/dir_to_logger.conf里面的配置文件写错了 所以说不是没有source参数的第三行的原因 跟这个没关系 …

图解Mysql原理之全局锁,表级锁,行锁了解吗?

前言 大家好&#xff0c;我是程序蛇玩编程。 Mysql中的锁大家都用过吗&#xff0c;那全局锁&#xff0c;表锁&#xff0c;行锁哪个用的频率最多呢? 正文 全局锁: 全局锁就是对整个数据库实例加锁。 MySQL 提供了一个加全局读锁的方法&#xff0c;命令是 Flush tables wi…

Java集成【邮箱验证找回密码】功能

目录 1.添加依赖 2.选择一个自己的邮箱&#xff0c;作为发件人角色。 3.编写邮箱配置【配置发件人邮箱】 4.编写邮箱配置类 5.编写controller业务代码 6.演示效果 7.总结流程 8.注意 结语 一.发送邮箱验证码 1.添加依赖 <!--导入邮箱依赖--> <dependency&g…

HarmonyOS 5.0应用开发——MVVM模式的应用

【高心星出品】 文章目录 MVVM模式的应用ArkUI开发模式图架构设计原则案例运行效果项目结构功能特性开发环境model层viewmodel层view层 MVVM模式的应用 MVVM&#xff08;Model-View-ViewModel&#xff09;模式是一种广泛用于应用开发的架构模式&#xff0c;它有助于分离应用程…

程序员鱼皮最新项目-----AI超级智能体教程(一)

文章目录 1.前言1.什么是AI大模型2.什么是多模态3.阿里云百炼平台介绍3.1文本调试展示3.2阿里云和dashscope的关系3.3平台智能体应用3.4工作流的创建3.5智能体编排应用 1.前言 最近鱼皮大佬出了一套关于这个AI 的教程&#xff0c;关注鱼皮大佬很久了&#xff0c;鱼皮大佬确实在…

【AI模型学习】双流网络——更强大的网络设计

文章目录 一 背景1.1 背景1.2 研究目标 二 模型2.1 双流架构2.2 光流 三 实验四 思考4.1 多流架构4.2 fusion策略4.3 fusion的early与late 先简单聊了双流网络最初在视频中的起源&#xff0c;之后把重点放在 “多流结构"和"fusion” 上。 一 背景 1.1 背景 Two-Str…

HarmonyOS:一多能力介绍:一次开发,多端部署

概述 如果一个应用需要在多个设备上提供同样的内容&#xff0c;则需要适配不同的屏幕尺寸和硬件&#xff0c;开发成本较高。HarmonyOS 系统面向多终端提供了“一次开发&#xff0c;多端部署”&#xff08;后文中简称为“一多”&#xff09;的能力&#xff0c;可以基于一种设计…

“在中国,为中国” 英飞凌汽车业务正式发布中国本土化战略

3月28日&#xff0c;以“夯实电动化&#xff0c;推进智能化&#xff0c;实现高质量发展”为主题的2025中国电动汽车百人会论坛在北京举办。众多中外机构与行业上下游嘉宾就全球及中国汽车电动化的发展现状、面临的挑战与机遇&#xff0c;以及在技术创新、市场布局、供应链协同等…

Java技术体系的主要产品线详解

Java技术体系的主要产品线详解 Java Card&#xff1a;支持Java小程序&#xff08;Applets&#xff09;运行在小内存设备&#xff08;如智能卡&#xff09;上的平台。 Java ME&#xff08;Micro Edition&#xff09;&#xff1a;支持Java程序运行在移动终端&#xff08;手机、P…

‌机器学习快速入门--0算力起步实践篇

在学习人工智能的过程中&#xff0c;显卡是必不可少的工具&#xff0c;但它的成本较高且更新换代速度很快。那么&#xff0c;没有GPU的情况下如何学习人工智能呢&#xff1f;以下是针对普通电脑与有算力环境分离的学习规划方案&#xff0c;尤其适合前期无GPU/云计算资源的学习者…

源码篇 剖析 Vue2 双向绑定原理

前置操作 源码代码仓地址&#xff1a;https://github.com/vuejs/vue/tree/main 1.查看源码当前版本 当前版本为 v2.7.16 2.Clone 代码 在【Code】位置点击&#xff0c;复制 URL 用于 Clone 代码 3.执行 npm install 4.执行 npm run dev 前言 在 Vue 中最经典的问题就是双…

单例模式与消费者生产者模型,以及线程池的基本认识与模拟实现

前言 今天我们就来讲讲什么是单例模式与线程池的相关知识&#xff0c;这两个内容也是我们多线程中比较重要的内容。其次单例模式也是我们常见设计模式。 单例模式 那么什么是单例模式呢&#xff1f;上面说到的设计模式又是什么&#xff1f; 其实单例模式就是设计模式的一种。…

STM32配置系统时钟

1、STM32配置系统时钟的步骤 1、系统时钟配置步骤 先配置系统时钟&#xff0c;后面的总线才能使用时钟频率 2、外设时钟使能和失能 STM32为了低功耗&#xff0c;一开始是关闭了所有的外设的时钟&#xff0c;所以外设想要工作&#xff0c;首先就要打开时钟&#xff0c;所以后面…