扫雷作为Windows系统自带的经典游戏,相信是很多人的童年回忆。这个看似简单的9x9方格游戏,背后蕴含着精妙的逻辑设计。游戏规则其实很简单:玩家需要在一个布满隐藏地雷的棋盘上,通过点击方格来揭开安全区域。如果点中地雷,游戏立即结束;如果揭开的是安全区域,则会显示周围8个方格中存在的地雷数量。
在设计C语言版扫雷时,我们需要考虑几个核心要素:首先是棋盘的表示,这里我们使用二维数组来存储;其次是地雷的随机分布,需要用到随机数生成函数;最后是游戏逻辑的实现,包括判断胜负、计算周围地雷数等。为了让游戏体验更接近原版,我们还需要设计一个简单的图形界面来显示棋盘状态。
初学者可能会觉得实现一个完整的扫雷游戏很复杂,但如果我们把它拆解成几个独立的功能模块,就会发现每个部分其实都很简单。这就是结构化编程的魅力所在——把复杂问题分解为多个简单的小问题。
在正式开始编码前,我们先要规划好项目结构。与把所有代码都堆在一个文件里不同,我建议采用多文件组织方式。这样做有几个明显好处:首先是代码更清晰,不同功能的实现被分开存放;其次是便于团队协作,多人可以同时开发不同模块;最重要的是调试更方便,当某个功能出现问题时,可以快速定位到对应的源文件。
在我们的扫雷项目中,我们创建三个主要文件:
game.h这个头文件是整个项目的枢纽,它定义了游戏所需的各种常量和函数接口。这里我们使用宏定义了棋盘大小(9x9)和地雷数量(10个)。考虑到在计算周围地雷数时需要检查棋盘边缘的格子,我们实际创建的数组比显示区域大一圈,这就是为什么会有ROWS和COLS这两个额外定义的宏。
c复制#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define EASY_COUNT 10
游戏开始前,我们需要初始化两个棋盘:一个用于存储实际的地雷分布(mine数组),另一个用于显示给玩家(show数组)。初始化函数InitBoard接收三个参数:棋盘数组、行列数和初始值。通过双重循环,我们可以快速将整个数组填充为指定字符。
显示棋盘时,为了提升用户体验,我们添加了行列号标记。这样玩家在输入坐标时就能准确定位。DisplayBoard函数负责这个功能,它只显示中间9x9的区域,边缘的缓冲区域对玩家不可见。
c复制void DisplayBoard(char board[ROWS][COLS], int row, int col) {
printf("------------扫雷游戏------------\n");
// 打印列号
for(int i=0; i<=col; i++) {
printf("%d ", i);
}
printf("\n");
// 打印每行内容
for(int i=1; i<=row; i++) {
printf("%d ", i); // 行号
for(int j=1; j<=col; j++) {
printf("%c ", board[i][j]);
}
printf("\n");
}
printf("------------扫雷游戏------------\n");
}
地雷的随机分布是游戏的关键。我们使用rand()函数生成随机坐标,然后在指定位置放置地雷。这里有几个需要注意的细节:首先,要确保生成的坐标在有效范围内(1-9);其次,要避免在同一个位置重复放置地雷;最后,放置完成后要记得减少剩余地雷计数。
c复制void SetMine(char board[ROWS][COLS], int row, int col) {
int count = EASY_COUNT;
while(count > 0) {
int x = rand()%row + 1;
int y = rand()%col + 1;
if(board[x][y] == '0') {
board[x][y] = '1';
count--;
}
}
}
游戏的核心逻辑在FindMine函数中实现。玩家每次输入坐标后,程序需要做以下检查:
如果坐标有效且未揭开,程序会计算周围8个格子中的地雷数量,并在show数组中显示这个数字。GetMineCount函数通过简单的字符运算实现这一功能。
c复制int GetMineCount(char mine[ROWS][COLS], int x, int y) {
return (mine[x-1][y-1] + mine[x-1][y] + mine[x-1][y+1] +
mine[x][y-1] + mine[x][y+1] +
mine[x+1][y-1] + mine[x+1][y] + mine[x+1][y+1] - 8*'0');
}
游戏通过一个while循环持续运行,直到玩家踩雷或成功标记所有安全区域。每次循环都会检查已揭开的格子数,当这个数等于总格子数减去地雷数时,玩家获胜。为了提高代码可读性,我们将游戏流程封装在game()函数中,主程序只需要调用这个函数即可。
c复制void game() {
char mine[ROWS][COLS] = {0}; // 存储地雷
char show[ROWS][COLS] = {0}; // 显示给玩家
// 初始化棋盘
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
// 布置地雷
SetMine(mine, ROW, COL);
// 显示初始棋盘
DisplayBoard(show, ROW, COL);
// 开始排雷
FindMine(mine, show, ROW, COL);
}
为了让游戏更完整,我们添加了一个简单的菜单系统。玩家可以选择开始游戏或退出。这个菜单通过do-while循环实现,确保至少显示一次,switch-case结构处理用户选择。
c复制void menu() {
printf("**********************\n");
printf("****** 1. 开始游戏 ******\n");
printf("****** 0. 退出游戏 ******\n");
printf("**********************\n");
}
int main() {
srand((unsigned int)time(NULL)); // 随机数种子
int input = 0;
do {
menu();
printf("请选择:>");
scanf("%d", &input);
switch(input) {
case 1:
game();
break;
case 0:
printf("游戏结束\n");
break;
default:
printf("选择错误,请重新输入\n");
break;
}
} while(input);
return 0;
}
良好的用户体验离不开完善的错误处理。在我们的实现中,对用户输入做了多重验证:
当检测到无效输入时,程序会给出明确提示并要求重新输入,而不是直接崩溃。这种防御性编程在实际项目中非常重要。
虽然这个基础版本已经可以正常运行,但还有优化空间。比如,目前的GetMineCount函数每次都要计算8个相邻格子,可以考虑在游戏初始化时就预先计算好每个格子的地雷数并存储起来。另外,可以添加递归展开空白区域的算法,当玩家点击一个周围没有地雷的格子时,自动展开所有相邻的空白区域。
掌握了基础版本后,你可以尝试添加更多功能:
这些扩展不仅能提升游戏体验,也是很好的编程练习。我在实际开发中发现,从简单版本开始,逐步添加功能,是最有效的学习方式。