2024-06-19
Learn coding
00

目录

1. 定义Minesweeper类:所有的游戏逻辑都要封装在这个类里
2. 构造函数construct:初始化游戏参数
3. 初始化函数init:初始化游戏状态及界面,以及创建游戏格子
4. 开始计时startTimer与更新计时updateTimerDisplay
5. 设置雷placeMines:随机放置雷,且要保证首次点击不是雷
6. 点击格子handleCellClick:点击不同状态的格子会做出不同回应
7. 翻转旗帜标记toggleFlag方法:未标记的格子标为旗帜,已标记为旗帜的格子恢复原样
8. 计算雷数countMines方法:计算指定单元格周围的地雷数量
9. 翻开单元格revealAdjacent方法:翻开指定单元格周围的安全单元格
10. checkWin方法:检查是否所有安全单元格都被翻开以判断游戏胜利情况
11. 游戏结束gameOver方法:处理游戏结束时的逻辑
12. 记录成绩recordScore方法:记录游戏成绩并更新排行榜
13. 显示成绩displayTopScores方法:显示前三名的游戏成绩
14. 鼠标按键处理方法handleMouseDown, handleMouseUp, handleDoubleClick
15. 计算旗帜数量countFlags方法:计算指定单元格周围的旗帜数量

(点击这里快速跳转扫雷网页小游戏)

扫雷小游戏规则不难,但是背后的逻辑拆解起来也不算很简单。正好借用这个在blog内插入小游戏的契机,从头拆解一下JS script的逻辑。

1. 定义Minesweeper类:所有的游戏逻辑都要封装在这个类里

js
class Minesweeper {}

2. 构造函数construct:初始化游戏参数

js
constructor(rootSelector, width, height, mineCount) { this.root = document.querySelector(rootSelector); // 使用 document.querySelector 方法选择用于显示游戏网格的根元素,并将其赋值给 this.root this.timerDisplay = document.querySelector('#timer'); // 选择用于显示计时器的元素,并将其赋值给 this.timerDisplay this.minesLeft = document.querySelector('#minesLeft'); // 选择用于显示剩余雷数的元素,并将其赋值给 this.minesLeft this.scoreList = document.querySelector('#scoreList'); // 选择用于显示游戏成绩列表的元素,并将其赋值给 this.scoreList this.width = width; // 设置游戏网格的列数,并将其赋值给 this.width this.height = height; // 设置游戏网格的行数,并将其赋值给 this.height this.totalMines = mineCount; // 设置游戏中的总雷数,并将其赋值给 this.totalMines this.cells = []; // 初始化存储单元格元素的二维数组 this.cells,用于在游戏网格中存储每个单元格元素 this.firstClick = true; // 标记是否是第一次点击,用于确保首次点击时不会有雷 this.timer = null; // 初始化计时器变量 this.timer,用于存储计时器的引用 this.timeElapsed = 0; // 初始化已用时间 this.timeElapsed,用于记录游戏已经进行的时间 this.gameOverFlag = false; // 标记游戏是否已经结束 this.gameOverFlag this.gameRecords = JSON.parse(localStorage.getItem('minesweeperScores')) || []; // 从本地存储中加载游戏记录 this.gameRecords // 使用 localStorage.getItem('minesweeperScores') 获取存储的游戏成绩数据,并使用 JSON.parse 将其解析为数组 // 如果本地存储中没有记录,则初始化为空数组 this.init(); // 调用初始化方法 this.init(),设置游戏网格和界面 }

3. 初始化函数init:初始化游戏状态及界面,以及创建游戏格子

js
init() { clearInterval(this.timer); // 清除之前的计时器,确保在重新开始游戏时没有残留的计时器在运行 this.timer = null; // 重置计时器变量,将其设置为 null,表示当前没有计时器在运行 this.timeElapsed = 0; // 重置已用时间,将其设置为 0,表示游戏时间从零开始计时 this.gameOverFlag = false; // 重置游戏结束标志,将其设置为 false,表示游戏尚未结束 this.updateTimerDisplay(); // 调用 updateTimerDisplay 方法,更新计时器显示,初始显示为 0 this.root.innerHTML = ''; // 清空游戏根元素的HTML内容,准备重新生成游戏网格 this.root.style.gridTemplateColumns = `repeat(${this.width}, 40px)`; // 设置游戏网格的列样式,根据游戏的列数,动态生成CSS样式 this.cells = Array.from({ length: this.height }, () => Array(this.width).fill(null)); // 初始化存储单元格元素的二维数组,大小为height x width,初始值为null this.firstClick = true; // 重置首次点击标志,将其设置为 true,表示尚未进行首次点击(不会是雷) this.flagCount = 0; // 重置旗帜计数,将其设置为 0,表示尚未放置任何旗帜 this.minesLeft.textContent = `Mines Left: ${this.totalMines}`; // 更新剩余雷数显示,初始显示为总雷数 // 用循环创建游戏格子 for (let y = 0; y < this.height; y++) { for (let x = 0; x < this.width; x++) { const cell = document.createElement('div'); // 创建一个新的 div 元素作为单元格 cell.dataset.status = 'hidden'; // 设置单元格的初始状态为隐藏(未翻开) cell.className = 'cell'; cell.addEventListener('click', () => { // 添加点击事件监听器,当点击时触发 if (this.firstClick) { // 如果是首次点击,开始计时并放置雷 this.startTimer(); // 调用 startTimer 方法开始计时 this.placeMines(x, y); // 调用 placeMines 方法放置雷,确保首次点击位置没有雷 this.firstClick = false; } this.handleCellClick(x, y); // 调用handleCellClick方法处理单元格点击逻辑 }); cell.addEventListener('contextmenu', (e) => { // 添加右键点击事件监听器,当右键点击时触发 e.preventDefault(); // 阻止默认的右键点击行为(显示右键菜单) this.toggleFlag(x, y); // 调用 toggleFlag 方法切换单元格的旗帜状态 }); cell.addEventListener('mousedown', (e) => this.handleMouseDown(e, x, y)); // 添加鼠标按下事件监听器,当单元格被按下时触发,处理同时按下左右键的逻辑 cell.addEventListener('mouseup', (e) => this.handleMouseUp(e, x, y)); // 添加鼠标松开事件监听器,当单元格松开时触发,处理鼠标事件 this.cells[y][x] = cell; // 将单元格元素存储在 cells 数组中,对应其在网格中的位置 this.root.appendChild(cell); // 将单元格元素添加到游戏根元素中,生成实际的游戏网格 } } this.displayTopScores(); // 调用 displayTopScores 方法,显示前三名游戏成绩 }

4. 开始计时startTimer与更新计时updateTimerDisplay

js
startTimer() { // 开始计时 // this.timer 保存 setInterval 返回的 ID,以便后续可以使用 clearInterval 清除计时器 this.timer = setInterval(() => { // 使用 setInterval 函数,每 1000 毫秒执行一次回调函数 this.timeElapsed++; // 每次回调执行时,将 this.timeElapsed 递增1,表示已用时间增加一秒 this.updateTimerDisplay(); // 调用 updateTimerDisplay 方法更新计时器显示 }, 1000); } updateTimerDisplay() { // 更新计时显示 this.timerDisplay.textContent = `Time: ${this.timeElapsed}`; // 将 this.timeElapsed 的值显示在页面上 }

5. 设置雷placeMines:随机放置雷,且要保证首次点击不是雷

js
placeMines(firstX, firstY) { let minesPlaced = 0; // 初始化 minesPlaced 变量,用于记录已放置的雷数 while (minesPlaced < this.totalMines) { const x = Math.floor(Math.random() * this.width); // 随机生成雷的x坐标 const y = Math.floor(Math.random() * this.height); // 随机生成雷的y坐标 if ((x !== firstX || y !== firstY) && !this.cells[y][x].dataset.mine) { // 确保雷不在首次点击的位置并且当前位置没有地雷 this.cells[y][x].dataset.mine = 'true'; // 表示该位置是雷 minesPlaced++; // 已放置雷数+1 } } this.minesLeft.textContent = `Mines Left: ${this.totalMines - this.flagCount}`; // 更新剩余雷数的显示:总雷数减去已标记的旗帜数量 }

6. 点击格子handleCellClick:点击不同状态的格子会做出不同回应

js
handleCellClick(x, y) { if (this.gameOverFlag) return; // 游戏已结束:不做任何处理 const cell = this.cells[y][x]; if (cell.dataset.status === 'revealed' || cell.dataset.flagged === 'true') return; // 已被翻开和标记为旗帜的格子不做任何处理 if (cell.dataset.mine === 'true') { // 点击放置了雷的格子,游戏结束 cell.classList.add('mine'); this.gameOver(false); } else { cell.dataset.status = 'revealed'; cell.classList.add('revealed'); const mines = this.countMines(x, y); cell.textContent = mines > 0 ? mines : ''; // 对于不是雷的格子,自动计算周围区域的雷数量并显示 if (mines === 0) { this.revealAdjacent(x, y); // 如果周围没有雷,调用 revealAdjacent 方法翻开周围的非雷格 } if (this.checkWin()) { // 检查翻开的格子状态以判定玩家是否获胜 this.gameOver(true); } } }

7. 翻转旗帜标记toggleFlag方法:未标记的格子标为旗帜,已标记为旗帜的格子恢复原样

js
toggleFlag(x, y) {// 检查 this.gameOverFlag 是否为 true,如果是,说明游戏已经结束,直接返回,不做任何处理 if (this.gameOverFlag) return; const cell = this.cells[y][x]; // 获取点击的单元格元素 if (cell.dataset.status === 'revealed') return; // 检查单元格是否已经被翻开,如果是,直接返回,不做任何处理 if (cell.dataset.flagged === 'true') { // 检查单元格是否已经被标记为旗帜 cell.dataset.flagged = 'false'; // 如果是,则将单元格标记为未标记状态 cell.textContent = ''; // 清空单元格内容 cell.classList.remove('flag'); // 移除单元格的旗帜样式 this.flagCount--; // 减少旗帜计数 } else { cell.dataset.flagged = 'true'; // 将单元格标记为旗帜状态 cell.textContent = '🚩'; // 在单元格中显示旗帜符号 cell.classList.add('flag'); // 添加单元格的旗帜样式 this.flagCount++; // 增加旗帜计数 } this.minesLeft.textContent = `Mines Left: ${this.totalMines - this.flagCount}`; // 更新剩余雷数显示 }

8. 计算雷数countMines方法:计算指定单元格周围的地雷数量

js
countMines(x, y) { let count = 0; // 初始化地雷计数器 count 为 0 for (let dy = -1; dy <= 1; dy++) { // 使用嵌套循环遍历单元格周围的所有单元格(包括自身),外层循环遍历y方向 for (let dx = -1; dx <= 1; dx++) { // 内层循环遍历x方向 const nx = x + dx, ny = y + dy; // 计算相邻单元格的坐标 if (nx >= 0 && nx < this.width && ny >= 0 && ny < this.height && this.cells[ny][nx].dataset.mine === 'true') { // 检查相邻单元格是否在网格范围内,并且是否有雷 count++; // 如果有雷,count加1 } } } return count; }

9. 翻开单元格revealAdjacent方法:翻开指定单元格周围的安全单元格

js
revealAdjacent(x, y) { for (let dy = -1; dy <= 1; dy++) { // 使用嵌套循环遍历单元格周围的所有单元格(包括自身),外层循环遍历y方向 for (let dx = -1; dx <= 1; dx++) { // 内层循环遍历x方向 const nx = x + dx, ny = y + dy; // 计算相邻单元格的坐标 if (nx >= 0 && nx < this.width && ny >= 0 && ny < this.height) { // 检查相邻单元格是否在网格范围内 const adjCell = this.cells[ny][nx]; // 获取相邻单元格元素 if (!adjCell.dataset.flagged && adjCell.dataset.status !== 'revealed') { // 检查相邻单元格是否没有被标记为旗帜,并且没有被翻开 this.handleCellClick(nx, ny); // 调用handleCellClick方法,递归翻开相邻单元格 } } } } }

10. checkWin方法:检查是否所有安全单元格都被翻开以判断游戏胜利情况

js
checkWin() { for (let y = 0; y < this.height; y++) { // 外层循环遍历网格的行 for (let x = 0; x < this.width; x++) { // 内层循环遍历网格的列 const cell = this.cells[y][x]; // 获取当前单元格 if (cell.dataset.status === 'hidden' && cell.dataset.mine !== 'true') { // 检查当前单元格是否是未翻开的安全单元格 return false; // 如果找到一个未翻开的安全单元格,返回false,表示游戏尚未胜利 } } } return true; // 如果所有安全单元格都被翻开,返回true,表示游戏胜利 }

11. 游戏结束gameOver方法:处理游戏结束时的逻辑

js
gameOver(win) { clearInterval(this.timer); // 清除计时器,停止计时 this.gameOverFlag = true; // 设置游戏结束标志,防止进一步的游戏操作 if (win) { // 判断传入参数win是否为true,表示玩家是否获胜 alert('You win!'); this.recordScore(); // 调用第12步recordScore方法,记录玩家的游戏成绩 } else { alert('Game Over! Restarting...'); } this.init(); // 调用第3步init方法,重新初始化游戏 }

12. 记录成绩recordScore方法:记录游戏成绩并更新排行榜

js
recordScore() { this.gameRecords.push(this.timeElapsed); // 将当前游戏的已用时间添加到游戏成绩记录数组gameRecords中 this.gameRecords.sort((a, b) => a - b); // 对游戏成绩记录数组进行排序,按从小到大的顺序排列 if (this.gameRecords.length > 3) { this.gameRecords.pop(); // 检查游戏成绩记录数组的长度,如果超过 3,则删除多余的成绩 } localStorage.setItem('minesweeperScores', JSON.stringify(this.gameRecords)); // 将更新后的游戏成绩记录数组保存到本地存储中 this.displayTopScores(); // 调用第13步displayTopScores方法,显示最新的前三名游戏成绩 }

13. 显示成绩displayTopScores方法:显示前三名的游戏成绩

js
displayTopScores() { this.scoreList.innerHTML = ''; // 清空成绩列表的HTML内容 this.gameRecords.forEach((score, index) => { // 遍历游戏成绩记录数组gameRecords const li = document.createElement('li'); // 创建一个新的li元素 li.textContent = `${index + 1}. ${score} seconds`; // 设置li元素的文本内容,显示排名和成绩 this.scoreList.appendChild(li); // 将li元素添加到成绩列表中 }); }

14. 鼠标按键处理方法handleMouseDown, handleMouseUp, handleDoubleClick

js
handleMouseDown(event, x, y) { // 处理鼠标按下事件 if (event.buttons === 3) { // 检查event.buttons是否为 3,表示左右键同时按下 this.handleDoubleClick(x, y); // 调用handleDoubleClick方法,处理同时按下左右键的逻辑 } } handleMouseUp(event, x, y) { // 处理鼠标松开事件 if (event.buttons === 0) { // 检查event.buttons是否为 0,表示没有按键按下 this.root.oncontextmenu = (e) => { e.preventDefault(); }; // 阻止右键点击时显示默认上下文菜单 } } handleDoubleClick(x, y) { // 处理双击事件,即同时按下左右键 if (this.gameOverFlag) return; // 检查this.gameOverFlag是否为 true,如果是,说明游戏已经结束,直接返回,不做任何处理 const cell = this.cells[y][x]; // 获取双击的单元格元素 if (cell.dataset.status !== 'revealed') return; // 检查单元格是否已被翻开,如果没有,直接返回,不做任何处理 const mines = parseInt(cell.textContent, 10); // 获取单元格中显示的雷数 if (mines > 0 && this.countFlags(x, y) === mines) { // 检查周围雷数大于0且周围旗帜数量等于地雷数量 this.revealAdjacent(x, y); // 调用第9步revealAdjacent方法,翻开周围的安全单元格 } }

15. 计算旗帜数量countFlags方法:计算指定单元格周围的旗帜数量

js
countFlags(x, y) { let flags = 0; // 初始化旗帜计数器flags为0 for (let dy = -1; dy <= 1; dy++) { // 使用嵌套循环遍历单元格周围的所有单元格(包括自身),外层循环遍历y方向 for (let dx = -1; dx <= 1; dx++) { // 内层循环遍历x方向 const nx = x + dx, ny = y + dy; // 计算相邻单元格的坐标 if (nx >= 0 && nx < this.width && ny >= 0 && ny < this.height && this.cells[ny][nx].dataset.flagged === 'true') { // 检查相邻单元格是否在网格范围内,并且是否被标记为旗帜 flags++; // 如果有旗帜,旗帜计数器flags加 1 } } } return flags; // 返回旗帜计数器flags的值 } }
首页

PERSONAL SKILL

Proficient in Python, R, SQL, MS Office; English CET-6, can be used for meeting communication and reporting; Basic knowledge of CSS, html, JS.

WORKING EXPERIENCE

Click here to view my CV.