(点击这里快速跳转扫雷网页小游戏)
扫雷小游戏规则不难,但是背后的逻辑拆解起来也不算很简单。正好借用这个在blog内插入小游戏的契机,从头拆解一下JS script的逻辑。
jsclass Minesweeper {}
jsconstructor(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(),设置游戏网格和界面
}
jsinit() {
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 方法,显示前三名游戏成绩
}
jsstartTimer() { // 开始计时
// 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 的值显示在页面上
}
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}`;
// 更新剩余雷数的显示:总雷数减去已标记的旗帜数量
}
jshandleCellClick(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);
}
}
}
jstoggleFlag(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}`;
// 更新剩余雷数显示
}
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;
}
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方法,递归翻开相邻单元格
}
}
}
}
}
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,表示游戏胜利
}
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方法,重新初始化游戏
}
jsrecordScore() {
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方法,显示最新的前三名游戏成绩
}
jsdisplayTopScores() {
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元素添加到成绩列表中
});
}
jshandleMouseDown(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方法,翻开周围的安全单元格
}
}
jscountFlags(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的值
}
}