扫雷的规则看似简单:在棋盘上避开所有地雷,翻开所有安全格就算获胜。把它做成C语言控制台程序时,最直接的做法是用一个二维数组表示棋盘。这个数组不仅存储每个格子是否有雷,还要记录该格子周围8个邻接格中的雷数。程序运行后先布雷,再根据玩家输入的坐标翻开格子,反复循环直到踩雷或完成目标。

一、用二维数组描述雷区
控制台版扫雷通常不依赖图形库,一切都靠字符显示。程序里至少需要两张表:一张是玩家能看到的棋盘board,初始时全部显示为星号;另一张是真实雷区mine,用来保存地雷位置和每个格子的数字。把它们都定义成二维字符数组后,后续的布雷、计算、展开操作都会围绕行列下标进行。
用char而不是int存储雷区,主要是为了方便控制台打印。地雷可以用字符'M'表示,普通格子初始化为字符'0'。当计算出某个格子周围有3颗雷时,直接把'0' + 3赋给数组,打印出来就是字符'3'。这种小技巧可以减少类型转换,也让代码更贴近控制台展示需求。初始化时还需要一个visited数组记录格子是否已经被翻开过,避免递归展开时重复处理。
布雷阶段使用rand()生成随机行列,但要先调用srand(time(NULL))设置随机种子,否则每次运行程序都会生成相同布局。如果随机到的位置已经有雷,就重新再生成,直到放满预设的雷数。为了调试方便,也可以把雷区打印出来观察,但正式游戏中显然不能提前暴露。
二、核心算法:邻接计数与空白展开
计算一个格子周围有多少雷,是扫雷的基础。标准做法是用双层循环遍历以该格子为中心的3乘3区域,跳过自身,然后判断邻居是否越界以及是否为地雷。这个逻辑写成countMines函数最清晰,输入行和列,返回整型雷数。边界判断尤其重要:如果当前格子在棋盘边缘,它的邻居只有5个,而不是8个;如果不做越界判断,数组下标会访问到负数或超过长度,轻则结果错误,重则程序崩溃。
当玩家翻开的格子周围雷数为0时,扫雷会自动展开相连的安全区域。这个功能非常适合用递归实现。递归函数expand先判断坐标是否越界、是否已经翻开过,通过检查后把该格标记为已访问。如果邻接雷数大于0,只显示数字并返回;如果等于0,就继续递归处理周围8个格子。递归展开会让空白区域像水波一样扩散,直到被数字格子边界挡住。需要注意的是,递归深度在最坏情况下可能接近整个棋盘大小,不过9乘9的规模完全在C语言函数调用栈承受范围内。
展开逻辑还有一个细节:不能翻开地雷。递归过程中遇到数字格子就停止,所以正常情况下不会把雷翻开。但如果布雷和展开的顺序不对,或者countMines函数写错,可能会出现数字边界错误。建议先在一个5乘5的小棋盘上测试,打印出雷区和展开后的棋盘进行人工核对。
三、控制台交互与胜负判定
控制台游戏的主循环很简单:打印棋盘、读取玩家输入、处理坐标、判断状态。玩家输入习惯从1开始编号,而C语言数组从0开始,所以读取行列后要分别减1。输入坐标后还要先检查是否超出棋盘范围,防止用户输入0或负数导致数组越界。这个细节很容易被忽略,但一个健壮的程序必须处理非法输入。
胜负判定分为两种情况。如果玩家输入的坐标对应mine数组中的'M',游戏立即结束并提示踩雷。另一种胜利条件是用一个全局变量opened统计已翻开的格子数量。每次expand函数翻开一个新格子,opened就加1。当opened等于总格子数减去雷数时,说明所有安全格都已经翻开,程序输出胜利信息。这个判断放在每次展开之后执行,比扫描整个棋盘判断剩余星号更高效。
主循环里还有一个细节:如果玩家输入的是已经翻开的格子,直接跳过即可,不要再次统计opened,否则胜利条件可能提前满足。当前实现中expand函数内部会先检查visited,因此已经翻开过的格子不会重复增加计数。对于重复输入同一个安全坐标,程序不会有副作用。
四、完整代码与改进方向
下面给出一个可运行的9乘9、10颗雷的控制台扫雷程序。所有函数都使用全局数组,适合初学者理解和修改。真正编译运行时,可以在Windows的Dev-C++或Linux的GCC环境中直接使用。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ROW 9
#define COL 9
#define MINES 10
char board[ROW][COL];
char mine[ROW][COL];
int visited[ROW][COL];
int opened = 0;
void initArrays() {
for (int i = 0; i < ROW; i++) {
for (int j = 0; j < COL; j++) {
board[i][j] = '*';
mine[i][j] = '0';
visited[i][j] = 0;
}
}
opened = 0;
}
void placeMines() {
int placed = 0;
while (placed < MINES) {
int r = rand() % ROW;
int c = rand() % COL;
if (mine[r][c] != 'M') {
mine[r][c] = 'M';
placed++;
}
}
}
int countMines(int r, int c) {
int count = 0;
for (int dr = -1; dr <= 1; dr++) {
for (int dc = -1; dc <= 1; dc++) {
if (dr == 0 && dc == 0) continue;
int nr = r + dr;
int nc = c + dc;
if (nr >= 0 && nr < ROW && nc >= 0 && nc < COL) {
if (mine[nr][nc] == 'M') count++;
}
}
}
return count;
}
void expand(int r, int c) {
if (r < 0 || r >= ROW || c < 0 || c >= COL) return;
if (visited[r][c]) return;
visited[r][c] = 1;
opened++;
int n = countMines(r, c);
if (n > 0) {
board[r][c] = '0' + n;
return;
} else {
board[r][c] = ' ';
for (int dr = -1; dr <= 1; dr++) {
for (int dc = -1; dc <= 1; dc++) {
if (dr == 0 && dc == 0) continue;
expand(r + dr, c + dc);
}
}
}
}
void printBoard() {
printf(" ");
for (int j = 0; j < COL; j++) printf("%d ", j + 1);
printf("\n");
for (int i = 0; i < ROW; i++) {
printf("%d ", i + 1);
for (int j = 0; j < COL; j++) {
printf("%c ", board[i][j]);
}
printf("\n");
}
}
int main() {
srand((unsigned)time(NULL));
initArrays();
placeMines();
int gameover = 0;
while (!gameover) {
printBoard();
printf("请输入坐标(行 列): ");
int r, c;
scanf("%d %d", &r, &c);
r--;
c--;
if (r < 0 || r >= ROW || c < 0 || c >= COL) {
printf("坐标超出范围,请重新输入。\n");
continue;
}
if (mine[r][c] == 'M') {
printf("踩到地雷,游戏结束。\n");
gameover = 1;
} else {
if (!visited[r][c]) {
expand(r, c);
if (opened == ROW * COL - MINES) {
printBoard();
printf("恭喜你,扫雷成功!\n");
gameover = 1;
}
}
}
}
return 0;
}
这段代码可以直接作为课程设计的基础版本。后续可以考虑几个改进方向:增加标记地雷的功能,让玩家能手动标注疑似地雷;在第一次点击时保证不踩雷,很多经典扫雷都采用这种友好设计;还可以把棋盘大小和雷数做成可配置项,通过宏定义或配置文件读取。更进一步的优化是用图形库替换控制台界面,这样鼠标操作和后期维护都会更直观。
如果发现递归展开后一些数字格显示不正常,优先检查countMines函数的边界条件。很多问题都出在边缘格子遍历邻居时越界,只要确保nr和nc满足大于等于0且小于对应的行列上限即可。另一个常见错误是opened忘记在初始化时清零,导致第二局游戏判断提前结束。

