初学者必经:C语言简单实现扫雷游戏
扫雷游戏
《扫雷》(MineSweeper) 是一款大众类的益智小游戏,于1992年发行。游戏目标是在最短的时间内根据点击格子出现的数字找出所有非雷格子,同时避免踩雷,踩到一个雷即全盘皆输。
下面我将会附上代码并且提供思路。
那么今天我们就用C语言和控制台简单实现一下这款经典的小游戏吧!
后面会提供Github网址,以便下载整个源码;
实现思路
那么在正式写代码之前,我们应该先来构思一下整个游戏的思路。
首先是游戏的开始,我们需要有一个菜单,然后选对选项,就可以进入游戏。
那么一个扫雷游戏的组成,需要有一个棋盘,然后我们选中坐标:要么是雷,要么是不是雷,并且这个位置应该显示周围雷的个数,以判断周围雷的个数,进行决策。所以我们可以考虑到,我们其实应该有两个棋盘,一个存放哪个位置是雷哪个位置不是雷,另一个棋盘我们应该存放未排雷的状态,以及排雷后显示周围雷的个数。所以我们需要有两个棋盘。
但是,我们还需要考虑一点的是,既然我们要计算周围雷的个数,那么当该位置处于棋盘的最左边或者最右边时候怎么办?数组会越界!我们要判断吗?算了吧,我们干脆用比棋盘大一圈的棋盘来存放吧!
其次,就是游戏的进行:输入坐标,若是雷则直接结束游戏,如果不是雷的位置则展开,并且计算周围雷的个数,最后打印棋盘。直到判断赢为止。
最后,那么输赢又怎样来实现呢?
这些具体的设计我们就后面详细来讲。
代码实现
宏定义部分
既然是棋盘实现,并且存储字符的话,那么用char类型的二维数组再适合不过了。既然是二维数组,我们可以利用宏定义来增强程序后续的可修改性。
EASY_COUNT是雷的个数,这里说明一下。
#define ROW 9 // 实际遍历的行
#define COL 9 // / 实际遍历的列
#define ROWS ROW + 2 // 棋盘行
#define COLS COL + 2 // 棋盘列
#define EASY_COUNT 10 // 简单难度类的个数
进入游戏之前
菜单
void mnue() {
printf("###############################################\n");
printf("################ 1.start ################\n");
printf("################ 0.exit ################\n");
printf("###############################################\n");
}
没得说,打印界面而已。
主界面
void test() {
// 初始化种子
srand((unsigned int)time(NULL));
int input = 0;
do {
mnue(); // 打印菜单
printf("请输入选项:\n");
scanf("%d", &input);
switch (input) {
// 进入游戏
case 1: game(); break;
case 0: printf("退出游戏, 期待下次游玩\n"); break;
default:printf("选项错误, 请重新输入数字:\n"); break;
}
} while (input);
}
控制游戏的进入以及退出部分。
游戏部分
那么进入游戏之后我们就要按照上面我们所说的初始化棋盘,并且为雷盘随机设置雷,准备工作做好了就可以进入我们游戏的核心了gameCore(后面讲)
void game() {
// 两个棋盘,一个是雷盘,一个是展示盘
char mine[ROWS][COLS] = { 0 }; // 雷盘
char show[ROWS][COLS] = { 0 }; // 给玩家看的展示盘
// 棋盘分别初始化
initBoard(mine, ROWS, COLS, 'O');
initBoard(show, ROWS, COLS, '#');
// 设置雷
setBoom(mine, ROW, COL);
// 游戏的核心部分
gameCore(mine, show, ROW, COL);
}
这里的实现思路是初始化两个棋盘,分别为全O和全#,然后为雷盘,随机设置雷,最后进入游戏的核心部分。
棋盘的显示:
显示棋盘的函数
void display(char board[][COLS], int row, int col) {
for (int i = 0; i <= col; i++) printf("%d ", i);
putchar('\n'); // 打印行标
for (int i = 0; i <= row<<1; i++) printf("_");
putchar('\n'); // 打印分割下划线
for (int i = 1; i <= row; i++) {
printf("%d|", i); // 打印列的分割
for (int j = 1; j <= col; j++) {
printf("%c ", board[i][j]); // 打印列标
}
putchar('\n');
}
}
初始化:initBoard函数
这个函数比较简单,就不说明了
void initBoard(char board[][COLS], int rows, int cols, char ch) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
board[i][j] = ch;
}
}
}
设置雷:setBoom函数
随机设置雷的位置
void setBoom(char mine[][COLS], int row, int col) {
for (int i = 0; i < EASY_COUNT;) {
int x = rand() % row + 1; // 获得一个[1, row]范围的数
int y = rand() % col + 1;
if (mine[x][y] != '*') {
mine[x][y] = '*';
i++; // 只有当雷添加上后,i才++
}
}
}
游戏的核心!gameCore函数
gameCore函数是游戏的最核心部分
那么这个部分还是需要说一说的,首先,我们需要两个变量存放玩家输入的坐标(即x,y),然后判断输入坐标的合理性以及此处有没有排过雷,如果都没有问题,即本次输入的坐标合理且没有被排过雷,此时就可以进入我们的空白展开算法,根据我们输入的坐标去展开我们的棋盘。
相反如果发现(x,y)位置是雷,则直接退出游戏,返回到打印菜单部分。
至于判断输赢我们会在后面讲解。
void gameCore(char mine[][COLS], char show[][COLS], int row, int col) {
int x = 0, y = 0;
// 判读
while (isWin < ROW * COL - EASY_COUNT) {
// 游戏还未赢
// 作弊(显示当前的雷盘)(测试用)
//display(mine, ROW, COL);
//printf("\n\n\n"); // 用于分割
display(show, ROW, COL);
printf("请输入您要排雷的坐标:\n");
scanf_s("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col) {
// 坐标合法
if (mine[x][y] == '*') {
// 踩雷
system("cls");
printf("您输入了坐标:(%d, %d)。\n很遗憾,您踩到了雷,请重新开始\n",x,y);
display(mine, row, col);
printf("\n\n\n");
break; // 结束游戏
}
else if (show[x][y] == '#') {
// 本次坐标不是雷,并且没有被排过雷
system("cls");
// 空白展开算法,用于处理点开后的算法
unfold(mine, show, x, y);
}
else {
// 排过雷
printf("当前输入的坐标已经排过雷了,请重新输入\n");
}
}
else {
// 输入的坐标超过当前棋盘
printf("当前输入的坐标不合法,请您重新输入\n");
}
}
if (isWin == ROW * COL - EASY_COUNT) {
// 赢了
printf("恭喜您!!!成功排除所有雷\n");
display(mine, ROW, COL);
}
}
空白展开算法:unfold函数
这个函数是用来判断此次(x, y)坐标需需不需要被展开的,其思路是如果本次(x,y)周围没有雷,则分别遍历周围8个坐标,然后递归进入函数再次进行判断。若周围有雷,则直接调用wakeOutBoom函数计算周围雷的个数,并赋予给展示盘(show)即可。
void unfold(char mine[][COLS], char show[][COLS], int x, int y) {
int count = 0;
isWin++;
if (!(count = wakeOutBoom(mine, x , y ))) {
show[x][y] = ' ';
for (int i = x - 1; i <= x + 1; i++) {
for (int j = y - 1; j <= y + 1; j++) {
if ((i > 0 && i <= ROW) && (j > 0 && j <= COL) && (mine[i][j] != '*' && show[i][j] == '#') ) {
unfold(mine, show, i, j);
}
}
}
}
else {
show[x][y] = '0' + count;
}
}
输赢的判断
首先我们声明了一个全局变量,用于判断输赢
并且在每次调用unfold函数时isWin至少要+1。
那么为什么每次调用unfold函数时都要+1,其思路是这样的,每次调用unfold函数时,其坐标为(x,y)的位置,不是显示空格,就是显示数字,所以至少+1,然后如若符合条件则再递归进入unfold函数,以此类推。所以我就把isWin++;放在了unfold函数进入的部分。
int isWin = 0; // 声明全局变量(用于判断赢)
我们再回顾gameCore函数中的一个循环的判断条件,这条语句的意思是非雷的个数 小于棋盘总数-雷的个数,此时说明游戏还没有结束,我们应该继续游戏,当排雷的个数>=棋盘总数-雷的个数时,说明雷已经排完了。
while (isWin < ROW * COL - EASY_COUNT)
当退出循环后,我们打印恭喜语句,并且打印原来的雷盘展示给玩家看
if (isWin == ROW * COL - EASY_COUNT) {
// 赢了
printf("恭喜您!!!成功排除所有雷\n");
display(mine, ROW, COL);
}
此时扫雷游戏也就结束了,然后我们就回到打印菜单的哪个部分,可以选择再来一遍或者退出即可。
结束与总结
扫雷游戏整体来说较为简单,逻辑较为简单,了解其思路即可简单实现。
如果有上面代码有任何问题,或者改进方案,可一起讨论,改进,在这里先谢谢各位大佬了。
上述代码的GitHub链接:https://github.com/WakingHours-GitHub/MineSweeper