扫雷是训练"数据结构思维"的好游戏: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 计时。