扫雷是训练"数据结构思维"的好游戏:9×9 棋盘随机埋 10 颗雷,翻开格子,数字表示周围 8 格有几颗雷。本文用二维 std::vector 建模,用 std::queue 做 BFS 实现"空白自动扩散"。
玩法介绍
棋盘上 # 未翻开,数字是周围雷数,. 是空白格。输入 行 列(0~8)翻开:踩雷失败;空白格自动扩散到数字边界。翻开 71 格(81-10)即获胜。
第 1 步:数据模型
用两个同尺寸的二维 std::vector 保存"雷与数字"和"是否已翻开"。-1 是地雷,0~8 是周围雷数:
#include <iostream>
#include <vector>
#include <queue>
#include <random>
const int ROWS = 9, COLS = 9, MINES = 10;
std::vector<std::vector<int>> board(ROWS, std::vector<int>(COLS, 0));
std::vector<std::vector<bool>> shown(ROWS, std::vector<bool>(COLS, false));
// board: -1 表示地雷,其他为周围雷数;shown: 是否已翻开
第 2 步:随机布雷
用 std::mt19937 生成随机坐标,在空位埋雷直到埋满 MINES 颗。坐标可能重复,所以用 while 不断尝试:
void placeMines() {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<int> rr(0, ROWS - 1);
std::uniform_int_distribution<int> cc(0, COLS - 1);
int placed = 0;
while (placed < MINES) {
int r = rr(gen), c = cc(gen);
if (board[r][c] != -1) {
board[r][c] = -1;
++placed;
}
}
}
第 3 步:计算周围雷数
方向数组 dr[]、dc[] 表示 8 个邻居的偏移,遍历非雷格数邻居里的雷数。"方向数组 + 越界检查"在网格游戏里很通用:
const int dr[8] = {-1, -1, -1, 0, 0, 1, 1, 1};
const int dc[8] = {-1, 0, 1, -1, 1, -1, 0, 1};
void countNumbers() {
for (int r = 0; r < ROWS; ++r)
for (int c = 0; c < COLS; ++c) {
if (board[r][c] == -1) continue;
int cnt = 0;
for (int k = 0; k < 8; ++k) {
int nr = r + dr[k], nc = c + dc[k];
if (nr >= 0 && nr < ROWS && nc >= 0 && nc < COLS
&& board[nr][nc] == -1)
++cnt;
}
board[r][c] = cnt;
}
}
第 4 步:用 BFS 翻开空白区
点开空白格要把整片空白一起翻开,这是典型的广度优先搜索(BFS):std::queue 保存待处理格,空白格把 8 个邻居入队,数字格停下。用 std::pair 打包坐标:
void reveal(int sr, int sc) {
std::queue<std::pair<int, int>> q;
q.push({sr, sc});
while (!q.empty()) {
auto [r, c] = q.front();
q.pop();
if (r < 0 || r >= ROWS || c < 0 || c >= COLS) continue;
if (shown[r][c] || board[r][c] == -1) continue;
shown[r][c] = true;
if (board[r][c] != 0) continue; // 数字格不扩散
for (int k = 0; k < 8; ++k)
q.push({r + dr[k], c + dc[k]});
}
}
第 5 步:绘制、主循环与编译运行
绘制函数把格子映射成字符;主循环负责读坐标、判雷、翻开、统计。把第 1~4 步的代码与下面的函数存进 minesweeper.cpp:
void draw() {
std::cout << " ";
for (int c = 0; c < COLS; ++c) std::cout << c << " ";
std::cout << "\n";
for (int r = 0; r < ROWS; ++r) {
std::cout << r << " ";
for (int c = 0; c < COLS; ++c) {
if (!shown[r][c]) std::cout << "# ";
else if (board[r][c] == -1) std::cout << "* ";
else if (board[r][c] == 0) std::cout << ". ";
else std::cout << board[r][c] << " ";
}
std::cout << "\n";
}
}
int main() {
placeMines();
countNumbers();
int opened = 0;
while (opened < ROWS * COLS - MINES) {
draw();
int r, c;
std::cout << "输入坐标(行 列): ";
std::cin >> r >> c;
if (r < 0 || r >= ROWS || c < 0 || c >= COLS) continue;
if (board[r][c] == -1) {
std::cout << "踩雷了!游戏结束。\n";
return 0;
}
reveal(r, c);
opened = 0;
for (int i = 0; i < ROWS; ++i)
for (int j = 0; j < COLS; ++j)
if (shown[i][j]) ++opened;
}
std::cout << "恭喜,排除所有地雷!\n";
return 0;
}
g++ -std=c++17 -o minesweeper minesweeper.cpp
./minesweeper
注意:第一次翻开就踩雷是可能的,经典扫雷会保证第一步安全,这是留给你的改造点。
💡 改进方向:① 首次踩雷则重新布雷,保证第一步安全;② 支持标记地雷;③ 棋盘大小、雷数可配置;④ 用 std::chrono 计时。