
构建扫雷游戏的第一步是定义其核心数据结构。一个扫雷棋盘本质上是一个二维网格,每个单元格(cell)都拥有特定的状态和属性。为了清晰地表示这些信息,我们应将每个单元格设计为一个对象,包含以下关键属性:
因此,游戏状态可以由一个包含这些单元格对象的二维数组来表示。
/**
* @typedef {Object} Cell
* @property {boolean} isMine - 单元格是否为地雷
* @property {"unopened" | "opened" | "flagged"} state - 单元格的当前状态
* @property {number} [adjacentMines] - 周围地雷数量 (可选, 仅在非地雷且打开时有意义)
*/
/**
* 游戏网格,由Cell对象组成的二维数组
* @type {Cell[][]}
*/
let gameGrid;在设计好数据结构后,我们需要初始化游戏网格。这包括创建指定大小的二维数组,并为每个单元格赋予初始属性。
首先,实现一个函数来生成一个指定大小的空二维数组,作为网格的容器。
/**
* 生成一个指定大小的空二维数组作为网格骨架。
* @param {number} gridSize - 网格的边长(例如,9表示9x9的网格)。
* @returns {Array<Array<any>>} - 初始化的二维数组。
*/
const generateEmptyGrid = (gridSize) => {
let grid = [];
for (let i = 0; i < gridSize; i++) {
grid.push([]);
for (let j = 0; j < gridSize; j++) {
grid[i][j] = null; // 暂时用null占位
}
}
return grid;
};接下来,我们需要一个函数来随机决定一个单元格是否为地雷。使用 Math.random() 可以生成一个介于0(包含)和1(不包含)之间的浮点数。通过将其与一个阈值(例如0.2,表示20%的概率是地雷)进行比较,我们可以得到一个布尔值。
立即学习“Java免费学习笔记(深入)”;
/**
* 随机决定一个单元格是否为地雷。
* @param {number} mineProbability - 单元格是地雷的概率 (0到1之间)。
* @returns {boolean} - 如果为true则为地雷,否则不是。
*/
const isMine = (mineProbability = 0.2) => Math.random() < mineProbability;现在,结合上述函数,我们可以初始化整个游戏网格。在遍历网格时,为每个单元格创建一个 Cell 对象,并设置其初始状态。
/**
* 初始化游戏网格,包括布雷和设置初始状态。
* @param {number} gridSize - 网格的边长。
* @param {number} mineProbability - 单元格是地雷的概率。
* @returns {Cell[][]} - 初始化的游戏网格。
*/
const initializeGrid = (gridSize, mineProbability = 0.2) => {
let grid = generateEmptyGrid(gridSize);
for (let r = 0; r < gridSize; r++) {
for (let c = 0; c < gridSize; c++) {
grid[r][c] = {
isMine: isMine(mineProbability),
state: "unopened",
adjacentMines: 0 // 初始设置为0,后续计算
};
}
}
// 在所有地雷位置确定后,计算每个非地雷单元格的相邻地雷数
calculateAdjacentMines(grid);
return grid;
};
/**
* 计算并设置每个非地雷单元格的相邻地雷数量。
* @param {Cell[][]} grid - 游戏网格。
*/
const calculateAdjacentMines = (grid) => {
const gridSize = grid.length;
for (let r = 0; r < gridSize; r++) {
for (let c = 0; c < gridSize; c++) {
if (!grid[r][c].isMine) {
let count = 0;
// 检查周围8个方向
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
if (dr === 0 && dc === 0) continue; // 跳过自身
const nr = r + dr;
const nc = c + dc;
// 检查边界
if (nr >= 0 && nr < gridSize && nc >= 0 && nc < gridSize) {
if (grid[nr][nc].isMine) {
count++;
}
}
}
}
grid[r][c].adjacentMines = count;
}
}
}
};为了在控制台显示游戏状态,我们需要一个 render 函数,它将游戏网格转换为可读的字符串。不同的单元格状态应该用不同的字符表示。
/**
* 将游戏网格渲染为控制台可打印的字符串。
* @param {Cell[][]} grid - 游戏网格。
* @param {boolean} [revealMines=false] - 是否显示所有地雷(例如游戏结束时)。
* @returns {string} - 渲染后的游戏板字符串。
*/
const renderGrid = (grid, revealMines = false) => {
let output = "";
const gridSize = grid.length;
// 打印列索引
output += " ";
for (let c = 0; c < gridSize; c++) {
output += ` ${c}`;
}
output += "\n";
output += " " + "-".repeat(gridSize * 2) + "\n";
for (let r = 0; r < gridSize; r++) {
output += `${r}|`; // 打印行索引
for (let c = 0; c < gridSize; c++) {
const cell = grid[r][c];
let char = " "; // 默认字符
if (revealMines && cell.isMine) {
char = "X"; // 游戏结束时显示所有地雷
} else if (cell.state === "unopened") {
char = "#"; // 未打开
} else if (cell.state === "flagged") {
char = "F"; // 已标记
} else if (cell.state === "opened") {
if (cell.isMine) {
char = "X"; // 踩到地雷
} else if (cell.adjacentMines === 0) {
char = " "; // 空白区域
} else {
char = cell.adjacentMines.toString(); // 显示数字
}
}
output += ` ${char}`;
}
output += "\n";
}
return output;
};扫雷游戏主要有两种用户操作:打开单元格和标记/取消标记单元格。我们需要实现对应的函数来处理这些操作,并更新游戏状态。
打开单元格的逻辑相对复杂,特别是当打开一个周围地雷数为0的单元格时,需要递归地打开其周围的空单元格,直到遇到有数字的单元格。
/**
* 打开指定坐标的单元格。
* @param {Cell[][]} grid - 游戏网格。
* @param {number} r - 行索引。
* @param {number} c - 列索引。
* @returns {boolean} - 如果打开的是地雷,返回true(游戏失败),否则返回false。
*/
const openCell = (grid, r, c) => {
const gridSize = grid.length;
// 边界检查和状态检查
if (r < 0 || r >= gridSize || c < 0 || c >= gridSize) return false;
const cell = grid[r][c];
if (cell.state === "opened" || cell.state === "flagged") return false;
cell.state = "opened";
if (cell.isMine) {
return true; // 踩到地雷,游戏失败
}
// 如果打开的是空单元格(adjacentMines为0),则递归打开周围的单元格
if (cell.adjacentMines === 0) {
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
if (dr === 0 && dc === 0) continue;
openCell(grid, r + dr, c + dc); // 递归调用
}
}
}
return false; // 未踩到地雷
};标记单元格用于玩家怀疑某个位置有地雷,防止误触。再次标记则取消标记。
/**
* 标记或取消标记指定坐标的单元格。
* @param {Cell[][]} grid - 游戏网格。
* @param {number} r - 行索引。
* @param {number} c - 列索引。
*/
const flagCell = (grid, r, c) => {
const gridSize = grid.length;
if (r < 0 || r >= gridSize || c < 0 || c >= gridSize) return; // 边界检查
const cell = grid[r][c];
if (cell.state === "opened") return; // 已打开的单元格不能标记
if (cell.state === "unopened") {
cell.state = "flagged";
} else if (cell.state === "flagged") {
cell.state = "unopened";
}
};游戏需要判断何时结束,以及是胜利还是失败。
/**
* 检查游戏是否结束,并返回游戏状态。
* @param {Cell[][]} grid - 游戏网格。
* @returns {"win" | "lose" | false} - 游戏状态,如果未结束则返回false。
*/
const checkEndCondition = (grid) => {
const gridSize = grid.length;
let unopenedNonMines = 0;
let totalMines = 0;
for (let r = 0; r < gridSize; r++) {
for (let c = 0; c < gridSize; c++) {
const cell = grid[r][c];
if (cell.isMine) {
totalMines++;
// 如果踩到地雷,直接判定为失败
if (cell.state === "opened") {
return "lose";
}
} else {
if (cell.state === "unopened" || cell.state === "flagged") {
unopenedNonMines++;
}
}
}
}
// 如果所有非地雷单元格都被打开,则游戏胜利
if (unopenedNonMines === 0) {
return "win";
}
return false; // 游戏尚未结束
};主游戏循环是连接所有组件的核心。它负责初始化游戏、渲染棋盘、接收用户输入、处理动作、检查游戏状态并循环直到游戏结束。我们将使用Node.js的 readline 模块来获取控制台输入。
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
/**
* 异步提问函数,封装rl.question。
* @param {string} query - 提示用户的问题。
* @returns {Promise<string>} - 用户输入的答案。
*/
const askQuestion = (query) => new Promise(resolve => rl.question(query, resolve));
/**
* 游戏主函数。
*/
const main = async () => {
console.log("欢迎来到控制台扫雷游戏!");
let gridSize = 0;
while (gridSize < 3 || gridSize > 20 || isNaN(gridSize)) {
const sizeInput = await askQuestion("请输入网格大小 (例如: 9 表示 9x9): ");
gridSize = parseInt(sizeInput);
if (gridSize < 3 || gridSize > 20 || isNaN(gridSize)) {
console.log("无效的网格大小,请输入一个3到20之间的数字。");
}
}
let gameGrid = initializeGrid(gridSize, 0.15); // 15% 的地雷概率
let endState = false;
while (!endState) {
console.clear(); // 清空控制台
console.log(renderGrid(gameGrid));
let actionInput = await askQuestion("请输入操作 (例如: 'o 1 2' 打开(1,2) 或 'f 0 0' 标记(0,0)): ");
const parts = actionInput.trim().split(' ');
const action = parts[0].toLowerCase();
const row = parseInt(parts[1]);
const col = parseInt(parts[2]);
// 输入验证
if (isNaN(row) || isNaN(col) || row < 0 || row >= gridSize || col < 0 || col >= gridSize) {
console.log("无效的坐标或输入格式,请重试。");
await new Promise(resolve => setTimeout(resolve, 1500)); // 暂停1.5秒
continue;
}
if (action === "o") {
const isMineHit = openCell(gameGrid, row, col);
if (isMineHit) {
endState = "lose";
}
} else if (action === "f") {
flagCell(gameGrid, row, col);
} else {
console.log("无效的操作,请使用 'o' (打开) 或 'f' (标记)。");
await new Promise(resolve => setTimeout(resolve, 1500));
}
if (!endState) { // 如果还没有因为踩雷而结束,则检查其他结束条件
endState = checkEndCondition(gameGrid);
}
}
console.clear();
console.log(renderGrid(gameGrid, true)); // 游戏结束时显示所有地雷
if (endState === "win") {
console.log("\n恭喜你,你赢了!?");
} else if (endState === "lose") {
console.log("\n很遗憾,你踩到地雷了!游戏结束。?");
}
rl.close();
};
main();在实际开发中,考虑用户可能进行的各种无效操作至关重要。上述代码已包含一些基本的输入验证,但仍可进一步增强:
当前实现是一个功能完备的基础扫雷游戏,但仍有许多优化空间:
通过以上步骤和建议,您应该能够构建一个功能完善的控制台扫雷游戏,并为进一步的优化和功能扩展打下坚实的基础。
以上就是使用JavaScript构建控制台版扫雷游戏教程的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号