使用 JavaScript 构建扫雷游戏:一步步教程

使用 javascript 构建扫雷游戏:一步步教程

本文将指导你使用 JavaScript 构建一个简单的扫雷游戏。我们将从数据结构设计开始,逐步实现游戏初始化、渲染、用户交互、结束条件判断以及错误处理等关键功能。通过本教程,你将掌握使用 JavaScript 构建命令行界面 (CLI) 游戏的基本方法,并了解如何优化游戏性能。

1. 数据结构设计

首先,我们需要设计一个合适的数据结构来表示游戏状态。扫雷游戏的每个单元格可以有以下几种状态:

是否是雷 (isMine): boolean 类型,表示该单元格是否包含地雷。状态 (state): 字符串类型,可以是 “unopened” (未打开), “opened” (已打开), 或 “flagged” (已标记)。

因此,每个单元格可以用一个对象来表示:

{   isMine: boolean,   state: "unopened" | "opened" | "flagged"}

整个游戏状态可以用一个二维数组来表示,数组中的每个元素都是上述的单元格对象。

2. 初始化游戏状态

游戏初始化包括生成二维数组,并为每个单元格设置初始状态。所有单元格初始状态都为 “unopened”,isMine 的值则根据随机算法确定。

立即学习“Java免费学习笔记(深入)”;

const generateGrid = (gridSize) => {  let grid = [];  for (let i = 0; i < gridSize; i++) {    grid.push([]);    for (let j = 0; j  Math.random() < 0.5; // 更简洁的随机布尔值生成方式let grid = generateGrid(9); // 创建一个 9x9 的棋盘

注意: isMine() 函数使用 Math.random()

3. 渲染游戏状态

渲染函数将游戏状态的二维数组转换为人类可读的字符串,以便在控制台中显示。

const render = (grid) => {  let output = "";  for (let i = 0; i < grid.length; i++) {    for (let j = 0; j < grid[i].length; j++) {      const cell = grid[i][j];      if (cell.state === "unopened") {        output += ". "; // 未打开的单元格      } else if (cell.state === "flagged") {        output += "F "; // 已标记的单元格      } else if (cell.isMine) {        output += "* "; // 地雷      } else {        // TODO: 计算并显示周围地雷的数量        output += "  "; // 已打开的单元格 (无雷)      }    }    output += "n"; // 换行  }  return output;};console.log(render(grid));

注意: render 函数需要根据单元格的状态来显示不同的字符。未打开的单元格可以用 “.” 表示,已标记的单元格可以用 “F” 表示,地雷可以用 “*” 表示。对于已打开的单元格,你需要计算并显示其周围的地雷数量 (将在后续步骤中实现)。

4. 用户交互

用户可以执行以下两个操作:

打开单元格 (open): 改变单元格的状态为 “opened”。如果该单元格是地雷,则游戏结束。如果该单元格周围没有地雷,则需要递归地打开周围的单元格。标记单元格 (flag): 改变单元格的状态为 “flagged” 或 “unopened” (如果已经标记)。

const open = (grid, row, column) => {  if (row = grid.length || column = grid[0].length) {    return; // 越界检查  }  const cell = grid[row][column];  if (cell.state !== "unopened") {    return; // 已经打开或标记的单元格  }  cell.state = "opened";  if (cell.isMine) {    return "lose"; // 踩到地雷,游戏结束  }  // TODO: 如果周围没有地雷,递归打开周围的单元格  return false;};const flag = (grid, row, column) => {  if (row = grid.length || column = grid[0].length) {    return; // 越界检查  }  const cell = grid[row][column];  if (cell.state === "unopened") {    cell.state = "flagged";  } else if (cell.state === "flagged") {    cell.state = "unopened";  }};

注意: open 函数需要进行越界检查,并判断单元格是否已经打开或标记。如果打开的单元格是地雷,则返回 “lose”,表示游戏结束。如果周围没有地雷,则需要递归地打开周围的单元格 (这部分逻辑将在后续步骤中实现)。 flag 函数用于在 “unopened” 和 “flagged” 状态之间切换。

5. 结束条件判断

游戏结束的条件有两种:

失败 (lose): 用户打开了一个包含地雷的单元格。胜利 (win): 用户打开了所有非地雷的单元格。

const checkEnd = (grid) => {  let allNonMineCellsOpened = true;  for (let i = 0; i < grid.length; i++) {    for (let j = 0; j < grid[i].length; j++) {      const cell = grid[i][j];      if (!cell.isMine && cell.state !== "opened") {        allNonMineCellsOpened = false;        break;      }    }    if (!allNonMineCellsOpened) {      break;    }  }  if (allNonMineCellsOpened) {    return "win";  }  return false; // 游戏未结束};

注意: checkEnd 函数需要遍历整个棋盘,判断是否所有非地雷的单元格都已打开。如果是,则返回 “win”,表示游戏胜利。

6. 主函数

主函数将所有组件整合在一起,实现游戏循环。

const readline = require('readline').createInterface({  input: process.stdin,  output: process.stdout,});const main = async () => {  // 提示用户输入棋盘大小  let gridSize = 9; // 默认棋盘大小  // generate grid  let grid = generateGrid(gridSize);  console.log(render(grid));  let endState = false;  while (!endState) {    // 提示用户输入操作    const action = await new Promise((resolve) => {      readline.question(`Enter action (open/flag row column): `, input => {        resolve(input);      });    });    // 解析用户输入    const [command, rowStr, columnStr] = action.split(" ");    const row = parseInt(rowStr);    const column = parseInt(columnStr);    // 执行操作    if (command === "open") {      endState = open(grid, row, column);      if (endState === "lose") {        break; // 踩到雷,跳出循环      }    } else if (command === "flag") {      flag(grid, row, column);    }    console.log(render(grid));    endState = checkEnd(grid);  }  if (endState === "win") {    console.log("You win!");  } else {    console.log("You lose!");  }  readline.close();};main();

注意: 主函数使用 readline 模块来获取用户输入。用户需要输入操作类型 (open/flag) 以及行和列的坐标。然后,主函数调用相应的函数来执行操作,并渲染游戏状态。循环直到游戏结束 (win/lose)。

7. 错误处理

在游戏开发过程中,需要考虑各种可能的错误情况,并进行处理。例如:

用户输入无效的坐标 (超出范围)。用户尝试打开已经打开的单元格。用户输入无效的命令。

在 open 和 flag 函数中添加了越界检查。 可以在主函数中添加输入验证,确保用户输入的坐标是有效的数字。

8. 优化

周围地雷计数: 实现一个函数,计算每个单元格周围的地雷数量,并在 render 函数中显示。递归打开: 在 open 函数中,如果打开的单元格周围没有地雷,则递归地打开周围的单元格。性能优化: 避免在 checkEnd 函数中进行 O(N^2) 的检查。可以使用额外的变量来跟踪游戏状态,并在 open 函数中更新这些变量。

9. 总结

通过本教程,你学习了如何使用 JavaScript 构建一个简单的扫雷游戏。 你了解了数据结构设计、游戏初始化、渲染、用户交互、结束条件判断以及错误处理等关键步骤。你还学习了如何优化游戏性能。 这个简单的CLI扫雷游戏为你提供了一个良好的起点,你可以继续扩展它的功能,例如添加图形界面、不同的难度级别等等。

以上就是使用 JavaScript 构建扫雷游戏:一步步教程的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1517369.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年12月20日 10:43:09
下一篇 2025年12月20日 10:43:14

相关推荐

  • JavaScript控制台扫雷游戏开发教程

    本教程详细指导如何使用纯JavaScript在VS Code控制台中构建一个功能完整的扫雷游戏。内容涵盖从核心数据结构设计、游戏状态初始化与渲染,到处理用户输入、实现游戏逻辑(开格、标记)、判断胜负条件,以及构建主游戏循环的完整开发流程,并提供错误处理和性能优化的建议。 在javascript环境中…

    2025年12月20日
    000
  • JS如何实现布隆过滤器?布隆过滤器的应用

    布隆过滤器通过位数组和多个哈希函数判断元素是否存在,可高效实现“可能存在”或“肯定不存在”的查询,适用于网页爬虫去重、缓存穿透预防等场景,其核心步骤包括创建位数组、设计哈希函数、添加与查询元素;位数组大小和哈希函数数量需根据预期元素数和误判率计算,公式为m = -(n ln(p)) / (ln(2)…

    2025年12月20日
    000
  • js如何操作麦克风

    在javascript中操作麦克风需通过getusermedia api获取用户授权,该api是实现访问麦克风的核心;2. 首先检查浏览器支持情况并请求权限,使用navigator.mediadevices.getusermedia({ audio: true })获取音频流,成功后通过promis…

    2025年12月20日 好文分享
    000
  • js怎么判断字符串是否包含子串

    判断字符串是否包含子串最推荐使用includes(),因其语义清晰且直接返回布尔值;2. 若需获取子串位置或兼容旧浏览器,则选用indexof(),通过返回值是否为-1判断存在性;3. 对于复杂模式匹配或不区分大小写的查找,应使用正则表达式,其中test()方法适合布尔判断,match()可返回匹配…

    2025年12月20日
    000
  • js 怎样获取地理位置

    使用javascript获取地理位置的核心是调用浏览器的geolocation api,通过navigator.geolocation.getcurrentposition()方法实现,需处理用户授权拒绝、定位不准确及信息安全等问题;首先检查浏览器是否支持该api,若支持则调用getcurrentp…

    2025年12月20日
    000
  • js 怎样合并两个对象

    在javascript中合并对象最推荐的方式是使用展开语法或object.assign()方法,1. 展开语法通过{…obj1, …obj2}创建新对象,不修改原对象,符合不可变性原则;2. object.assign()通过object.assign(target, sou…

    2025年12月20日
    000
  • js怎么判断一个变量是否是数组

    判断一个变量是否为数组最推荐的方法是使用 array.isarray(),因为它准确、可靠且能正确处理跨iframe等不同执行环境下的数组判断;2. typeof 不能用于判断数组,因为它对所有对象(包括数组、普通对象、null)都返回”object”,无法区分具体类型;3.…

    2025年12月20日 好文分享
    000
  • JS如何实现虚拟滚动?长列表的优化

    虚拟滚动通过只渲染可视区域内的列表项并动态更新偏移量,避免渲染全部数据,从而解决长列表导致的DOM过多、内存占用高和滚动卡顿问题,提升页面性能与用户体验。 当你有一个需要展示大量数据的列表时,比如几千上万条记录,直接把它们一股脑儿地渲染到页面上,浏览器大概率会“罢工”——卡顿、内存占用飙升,用户体验…

    2025年12月20日
    000
  • JS如何实现Hooks?Hooks的规则

    Hooks 的核心实现原理是利用闭包和调用顺序,React 为每个组件维护一个按顺序存储状态的“槽位”数组,每次渲染时按顺序读取或更新状态,确保状态与 Hook 调用一一对应。 Hooks 在 JavaScript,特别是 React 框架中,实现的核心在于利用闭包和组件内部的一个“隐秘”状态存储机…

    2025年12月20日
    000
  • JS如何实现弹幕功能

    js实现弹幕功能的核心答案是通过动态创建dom元素并结合css动画或requestanimationframe实现横向移动,同时进行元素回收与性能优化;具体而言,首先构建一个相对定位的容器用于承载弹幕,接着定义绝对定位的弹幕样式并利用transform实现高效动画,然后在javascript中创建元…

    2025年12月20日
    000
  • 什么是useReducer?Reducer的模式

    useReducer 是 useState 的高级形式,适用于复杂状态逻辑管理。它通过 reducer 函数将状态更新逻辑与组件分离,接收当前状态和 action,返回新状态,确保逻辑清晰、可预测。使用步骤包括:定义初始状态、创建纯函数 reducer、调用 useReducer 获取 state …

    2025年12月20日
    000
  • JS如何实现通知提醒

    JavaScript实现通知提醒的核心是使用Notification API发送系统级通知或创建自定义DOM元素实现页面内提示。首先需通过Notification.requestPermission()请求用户授权,获准后即可调用new Notification()显示系统通知,适用于新消息、任务完…

    2025年12月20日
    000
  • 深入理解ESM:解决JavaScript模块导入“未提供导出”错误

    本文旨在解决JavaScript ES模块中常见的SyntaxError: The requested module ‘X’ does not provide an export named ‘Y’错误。该错误通常源于对默认导出与命名导出的混淆,以及对…

    2025年12月20日
    000
  • javascript数组怎么实现虚拟滚动

    虚拟滚动通过只渲染可视区域内的数据来提升性能,其核心是根据滚动位置动态计算需渲染的数据范围。1. 计算可视区域数据范围:基于scrolltop、itemheight和visibleheight,得出startindex = math.floor(scrolltop / itemheight),end…

    2025年12月20日 好文分享
    000
  • Promise与异步迭代器的配合

    异步迭代器配合promise,使处理异步数据流变得直观清晰。其核心在于next()方法返回promise,解析后产出value和done;使用for await…of循环可同步风格消费异步数据;常见实现方式是async function*,内部用await等待异步操作、yield产出值;…

    2025年12月20日 好文分享
    000
  • JS如何实现useRef?Ref的持久化

    useRef能持久化是因为它返回的对象在组件实例的生命周期内始终保持同一引用,React通过将该对象绑定到组件的内部节点(如Fiber节点)实现跨渲染的持久存储,每次调用useRef都返回同一实例,确保.current值在多次渲染间不变且修改不触发重渲染。 useRef 在JavaScript(尤其…

    2025年12月20日
    000
  • js 如何移除DOM节点

    移除dom节点主要有三种方式:使用 remove() 方法、removechild() 方法和 innerhtml = ”;2. 推荐优先使用 remove(),因其语法简洁且无需获取父节点;3. removechild() 兼容性更好,适用于需要兼容旧浏览器或需返回被移除节点的场景;4…

    2025年12月20日
    000
  • 什么是无障碍?ARIA属性的应用

    无障碍的核心是让所有人平等使用数字产品,ARIA通过为自定义组件添加语义(如角色、状态、属性)弥补HTML不足,但应优先使用原生语义标签,并配合键盘交互与焦点管理,结合实际测试确保残障用户可感知、操作内容,实现技术向善。 无障碍,简单来说,就是让每个人,无论身体能力如何,都能平等地获取和使用信息、产…

    2025年12月20日
    000
  • js如何实现数组映射

    在javascript中,实现数组映射的核心方式是使用内置的 map() 方法。1. map() 方法通过接受一个回调函数,为原数组的每个元素生成新值,最终返回一个新数组,不修改原始数组,体现了函数式编程的不变性原则;2. 相较于 foreach() 和 for 循环,map() 更适合“一对一”数…

    2025年12月20日
    000
  • JS如何实现任务调度

    JavaScript任务调度依赖事件循环机制,通过setTimeout、setInterval、requestAnimationFrame、Web Workers及自定义队列等手段控制任务执行。事件循环管理宏任务(如setTimeout)与微任务(如Promise)的执行顺序,确保异步操作按规则运行…

    2025年12月20日
    000

发表回复

登录后才能评论
关注微信