前言
如果聊起有什么让大家印象深刻的童年游戏,那么《 扫雷 》的地位应该是不可撼动的。这个搭载在 Windows 系统菜单里的小游戏在当时可谓大红大紫,算的上是我们实打实的童年回忆了。
- 或许我们在一遍又一遍玩扫雷的同时,都会回忆起小学夏天的那节电脑课上,穿着鞋套,在硕大的白色电脑上,瞒着老师偷偷玩一把扫雷的简单快乐吧~
- 如今我们用经典的C语言复刻扫雷,还能否勾起你对童年的回忆呢?
一、游戏规则
扫雷游戏规则:玩家需要尽快找出雷区中的所有不是地雷的方块,而不许踩到地雷。
众所周知。点开其中一个小方格之后,数字是几,就说明它周围的八个方位就有几个雷
比如下图中就表示红框内存在2个雷
二、效果演示
🍑1、初级版
🍑2、升级版
三、游戏设计思路
在介绍过三子棋的实现后,要实现扫雷是不是就会感觉就简单一些了呢?其实二者有很多相似的地方,扫雷游戏的实现主要是对下面两个模块的实现:
一、游戏交互界面
- 创建游戏菜单
- 创建游戏逻辑主体
二、扫雷游戏实现
- 创建扫雷棋盘
- 初始化扫雷棋盘
- 布置雷
- 打印扫雷棋盘
- 用户排查雷
- 判断输赢
这里准备了一张图,便大家更直观的理清游戏实现原理与游戏实现的逻辑 :
四、游戏交互界面
🍑1、创建游戏菜单
📝代码展示:
void menu() { printf("* * * * * * * * * * * * * * * * * *\n"); printf("* * * * * * 1.扫雷游戏 * * * * * *\n"); printf("* * * * * * 0.退出游戏 * * * * * *\n"); printf("* * * * * * * * * * * * * * * * * *\n"); }
🍑2、创建游戏逻辑主体
📝代码展示:
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; }
1、srand((unsigned int)time(NULL)) 表示随机数生成函数通过时间戳生成随机数,为下面的布置雷做准备。
2、do-while() 因为游戏至少可以进入一次,至少让玩家输入一次玩或不玩,所以用do-while()循环–循环代码至少执行一次。
观察上面这段代码,其中我们使用了一个game()函数来实现扫雷游戏,但是这个函数还未定义,下面将实现对game()函数的定义。
五、扫雷游戏实现(初级版)
🍑1、游戏框架搭建
📝代码展示
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); }
🍑2、游戏内函数实现
🌳(1)创建雷盘
📝代码展示:
#define ROW 9 #define COL 9 #define ROWS ROW+2 #define COLS COL+2 char mine[ROWS][COLS] = { 0 };//存放布置好的雷的信息 char show[ROWS][COLS] = { 0 };//存放排查出的雷的信息
问题1:这里为什么创建两个棋盘?
假设这里只创建一个棋盘,那么这一个棋盘上就要放置3种信息。分别为雷、非雷、排查出周围雷的信息。虽然这种方法也没太大的问题,但是一个棋盘上放置3种信息会给之后打印棋盘时造成麻烦,不易打印。
所以我们创建两个二维数组,一个数组存放布置好雷的信息;另一个数组存放排查出的雷的信息。这样创建不仅方便之后的打印,也可以将问题分解从而让问题层次更加分明,解题思路更加清晰。
问题2:为什么使用宏常量?
其实这个问题在前面的三子棋游戏中也有介绍。在这里使用宏常量主要是为了方便程序的修改,增加了程序的可塑性。
就像我这里通过宏定义将行列通设定为9,也就是9*9的雷盘。假如后面我想要玩12*12的雷盘,我只需要将宏定义中的9改为12即可,这样就省去了在程序中大量修改的精力,使代码可塑性更高。
问题3:为什么二位数组的行列选择ROWS、COLS
而不是ROW、COL
?
通过扫雷游戏规则我们已知,如果一个位置不是雷,我们要排查它周围8个坐标是不是雷,对于9*9的棋盘,当在排查边界时可能会出现越界的情况。因此我们创建11*11的棋盘这样就很好的解决了这个问题。
那么怎么让用户看到9*9的棋盘呀?其实用户看到的只是程序员想让你看到的,后面我们只需要打印11*11中的9*9就ok了。
🌳(2)初始化雷盘
📝代码展示:
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set) { int i = 0; int j = 0; for (i = 0; i < rows; i++) { for (j = 0; j < cols; j++) { board[i][j] = set; } } }
这里就是简单使用两个for循环遍历整个棋盘从而实现棋盘初始化,这里就不在过多赘述。
补充:这里是将
mine[ROWS][COLS]
中的内容初始化为‘0’
,show[ROWS][COLS]
中内容初始化为‘*’
效果展示:
🌳(3)打印扫雷棋盘
📝代码展示:
//打印棋盘 DisPlayBoard(char board[ROWS][COLS], int row, int col) { int i = 1; int j = 1; printf("------扫雷游戏------\n"); for (i = 1; i <= row; i++) { for (j = 1; j <= col; j++) { printf("%c ",board[i][j]); } printf("\n"); } }
效果展示:
简单观察不难发现,上述棋盘使用户查找坐标时变得困难,不易于用户输入排雷坐标 ,因此我们可以通过下面简单优化,为棋盘标号👇:
📝优化后代码:
//打印棋盘 DisPlayBoard(char board[ROWS][COLS], int row, int col) { int i = 1; int j = 1; printf("------扫雷游戏------\n"); //打印列号 for (i = 0; i <= col; i++) { printf("%d ",i); } printf("\n"); for (i = 1; i <= row; i++) {//打印行号 printf("%d ",i); for (j = 1; j <= col; j++) { printf("%c ",board[i][j]); } printf("\n"); } }
优化后效果展示:
这样以来雷盘坐标是不是就变得更加直观清楚了 !😊
🌳(4)布置雷
📝代码展示:
#define EASY_COUNT 10
void SetMine(char mine[ROWS][COLS], int row, int col) { //布置10个雷 int count = EASY_COUNT; while (count) { //生产随机的下标 int x = rand()%row+1; int y = rand()%col+1; if (mine[x][y] == '0') { mine[x][y] = '1'; count--; } } }
1、这里使用宏定义布置雷的个数,便于修改,增加代码的可塑性。
2、rand()
函数与srand((unsigned int)time(NULL))
函数配合生成随机数,rand()%row+1
与rand()%col+1
保证了生成了横纵坐标在合理范围之内。
补充:这里规定雷为'1'
🌳(5)扫雷并判断输赢
1.排雷原理
在扫雷的时候我们还需要定义一个函数,用来返回排查坐标周围地雷的个数。
下面用一张图来解析排查原理:
假设排查坐标为(x,y),我们可以如右图,依次返回其周围8个坐标下对应的值,由于我们上面规定,雷为‘1’,非雷为‘0’,则字符相加减对应ASCLL码值相加减。
例如:(x,y)周围有1个雷,则7*'0'+'1'-8*'0'即表示‘1’的ASCLL码值减‘0’的ASCLL码值,返回整数1,即周围有1个雷
方案一:逐个遍历
📝代码展示:
//返回排查坐标周围雷的数量 int get_mine_count(char mine[ROWS][COLS], int x, int y) { return mine[x - 1][y] + mine[x - 1][y - 1] + mine[x][y - 1] + mine[x + 1][y - 1] + mine[x + 1][y] + mine[x + 1][y + 1] + mine[x][y + 1] + mine[x - 1][y + 1] - 8 * '0'; }
方案二:循环遍历
📝代码展示:
int get_mine_count2(char mine[ROWS][COLS], int x, int y) { int i = 0; int count = 0; for (i = -1; i <= 1; i++) { int j = 0; for (j = -1; j <= 1; j++) { if (mine[x + i][y + j] == '1') count++; } } return count; }
2.扫雷流程+判断输赢
设计思路:
1.输入排查的坐标
2.判断坐标合法性
(1)合法 -程序执行下一步
(2)不合法 -重新输入
3.检查坐标处是不是雷
(1)是雷 - 很遗憾炸死了 - 游戏结束
(2)不是雷 - 统计坐标周围有几个雷 - 存储排查雷的信息到show数组,游戏继续
程序执行过程如下图:
📝代码展示:
注意:
row*col- EASY_COUNT
表示所有非雷数量,所有非雷坐标全部排查则游戏正常结束,玩家胜利。
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col) { //思路: //1. 输入排查的坐标 //2、判断坐标合法性 //3. 检查坐标处是不是雷 // (1) 是雷 - 很遗憾炸死了 - 游戏结束 // (2) 不是雷 - 统计坐标周围有几个雷 - 存储排查雷的信息到show数组,游戏继续 int x = 0; int y = 0; int win = 0; while (win<row*col- EASY_COUNT) { printf("请输入要排查的坐标:>"); scanf("%d%d", &x, &y);//x--(1,9) y--(1,9) //判断坐标的合法性 if (x >= 1 && x <= row && y >= 1 && y <= col) { if (mine[x][y] == '1') { printf("很遗憾,你被炸死了\n"); DisplayBoard(mine, row, col); break; } else { //不是雷情况下,统计x,y坐标周围有几个雷 int count = get_mine_count(mine, x, y); show[x][y] = count+'0'; //显示排查出的信息 DisplayBoard(show, row, col); win++; } } else { printf("坐标不合法,请重新输入\n"); } } if (win == row * col - EASY_COUNT) { printf("恭喜你,排雷成功\n"); DisplayBoard(mine, row, col);//向玩家展示一下布雷棋盘 } }
到这里初级扫雷游戏就搭建完成了,我们来试玩一把:
六、游戏缺陷
1.上述代码每次排雷只展开一个坐标,排雷效率较低.
2.没有标记功能,缺少趣味性.
七、扫雷升级版
显然,初级扫雷虽然能实现扫雷游戏的基本功能,但是缺少展开功能和标记功能,扫雷仿佛没了灵魂,那么我们应该如何画龙点睛,让扫雷如虎添翼,更上一层呢?下面我们就针对游戏缺陷进一步改进!
🍑1、展开功能
如果没有思路,我们可以先观察一下游戏中的展开过程:
聪明的你们一定会发现:当排雷坐标周围处地雷数量为0
时,棋盘会向周围展开,并且将周围雷的数量显示到屏幕上,直到坐标周围地雷数不为0
。
显然,这是一个递归问题,当排雷坐标周围地雷数为0
,棋盘会继续遍历周围个坐标,直到坐标周围出现雷,递归停止。
📝代码展示:
static void Digit_boom(char show[ROWS][COLS], char mine[ROWS][COLS], int x, int y, int row, int col, int* win) { if (x >= 1 && x <= row && y >= 1 && y <= col)//判断坐标合法性 { int ret = Around_num2(mine, x, y);//接受坐标周围雷的数量 if (ret == 0)//递归条件--周围雷数为0 { (*win)++;//每排查一个坐标,排查次数加1,为判断输赢做准备 show[x][y] = '0';//显示周围雷数 int i = 0; int j = 0; //用两个循环遍历周围8个坐标 for (i = -1; i <= 1; i++) { for (j = -1; j <= 1; j++) { if (show[x + i][y + j] == '*')//递归的坐标必须是未排查过的坐标, //防止死递归 { Digit_boom(show, mine, x + i, y + j, row, col,win); } } } } else { //条件不满足退出递归 (*win)++;//排查坐标,次数加1 show[x][y] = ret + '0';//显示周围雷数 } } }
1、递归参数win这里为传址调用,作用为计数器,统计排雷次数,方便之后判断输赢。
2、注意此递归有两个条件:
条件一:if (ret == 0)为大前提,只有周围坐标为0才可能进入递归
条件二:if(show[x+i][y+j]=='*') ,为小前提,进入递归的坐标必须是未排查过的坐标,否则可能重复排查坐标,出现死递归
效果展示:
🍑2、标记功能
标记功能,作用是把玩家确定的雷坐标标记出来,或者是把不确定的雷坐标标记出来。它的作用只是做记号,相比于递归展开,标记功能显然更容易实现。
由于是做标记,我们需要再引入一个标志符号,为了不与雷盘符号*
冲突,易于玩家区分,这里使用#
作为标记符号。
📝代码展示:(标记雷)
//标记雷 static void flag(char show[ROWS][COLS], int row, int col) { while (1) { int x = 0; int y = 0; printf("请输入标记坐标(输入:0 0退出):>"); scanf("%d %d",&x,&y); if (x >= 1 && x <= row && y >= 0 && y <= col) { if (show[x][y] != '#') { show[x][y] = '#'; break; } else { printf("该坐标已经标记,请重新选择\n"); } } else { printf("坐标越界,请重新输入\n"); } if (x == y && x == 0) break; } }
📝代码展示:(取消标记)
static void cancel_flag(char show[ROWS][COLS], int row, int col) { while (1) { int x = 0; int y = 0; printf("请输入取消标记坐标(输入:0 0退出):>"); scanf("%d %d", &x, &y); if (x >= 1 && x <= row && y >= 0 && y <= col) { if (show[x][y] == '#')//比较用双等号!!! { show[x][y] = '*'; break; } else { printf("该坐标还未标记,请重新选择\n"); } } else { printf("坐标越界,请重新输入\n"); } if (x == y && x == 0) break; } }
效果展示 :
🍑3、搭建游戏菜单
当我们添加了这些功能后,为了能够有更好的游戏体验我们可以为这些功能添加一个菜单,如下图:
📝代码展示:
static void menu2() { printf("┌-----------------------------┐\n"); printf("├**********1.排雷*************┤\n"); printf("├**********2.标记*************┤\n"); printf("├**********3.取消标记*********┤\n"); printf("└-----------------------------┘\n"); }
🍑4、排雷函数封装
📝在实现了上述功能后,我们可以重新封装排雷函数FindMine()
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col) { int choice = -1; int x = 0; int y = 0; int win = 0; while (win < col * row - Easy_Count) { menu2(); printf("请选择:>"); scanf("%d", &choice); //标记 if (2 == choice) { flag(show, ROW, COL); DisplayBoard(show, ROW, COL); } //取消标记 else if (3 == choice) { cancel_flag(show, ROW, COL); DisplayBoard(show, ROW, COL); } //排雷 else if(1==choice) { printf("请输入排查坐标:>"); scanf("%d%d", &x, &y); if (x >= 0 && x <= row && y >= 0 && y <= col) { if (mine[x][y] == '1') { DisplayBoard(mine, ROW, COL); printf("很遗憾,你被炸死了!\n"); break; } else if(show[x][y]=='*') { Digit_boom(show, mine, x, y, row, col, &win); DisplayBoard(show, row, col); } else { printf("该坐标已排查,请重新选择\n"); } } else { printf("输入坐标错误,请重新输入\n"); } } else { printf("选择错误,请重新选择\n"); } } if (win == col * row - Easy_Count)//非雷坐标全部排查完毕 { DisplayBoard(mine, ROW, COL); printf("恭喜你,你赢了咯!\n"); } }
最终效果展示:
为了让扫雷界面更简洁,在每次打印雷盘前增加了清屏操作:
system("cls");
七、完整代码
🍑1、游戏交互主体-test.c
#define _CRT_SECURE_NO_WARNINGS 1 #include"game.h" //游戏菜单 menu() { printf("* * * * * * * * * * * * * * * * * *\n"); printf("* * * * * * 1.扫雷游戏 * * * * * *\n"); printf("* * * * * * 0.退出游戏 * * * * * *\n"); printf("* * * * * * * * * * * * * * * * * *\n"); } //扫雷游戏 void game() { //创建扫雷棋盘 char mine[ROWS][COLS]; char show[ROWS][COLS]; //初始化扫雷棋盘 Init(mine, ROWS, COLS, '0'); Init(show, ROWS, COLS, '*'); //布置雷 SetMine(mine, ROW, COL); //打印扫雷棋盘 DisplayBoard(show, ROW, COL); //开始扫雷并判断输赢 FindMine(mine, show, ROW, COL); } //交互界面 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; }
🍑2、声明部分-game.h
#pragma once //包含头文件 #include <stdio.h> #include <time.h> #include<string.h> #include<windows.h> #include<stdlib.h> //宏定义 #define ROW 9 #define COL 9 #define ROWS ROW+2 #define COLS COL+2 #define Easy_Count 10 //初始化雷盘 void Init(char board[ROWS][COLS],int rows,int cols,char x); //打印雷盘 void DisplayBoard(char board[ROWS][COLS],int row,int col); //布置雷 void SetMine(char mine[ROWS][COLS],int row,int col); //排雷并判断输赢 void FindMine(char mine[ROWS][COLS],char show[ROWS][COLS],int row,int col);
🍑3、函数定义-game.c
#define _CRT_SECURE_NO_WARNINGS 1 #include"game.h" //初始化雷盘 void Init(char board[ROWS][COLS], int rows, int cols, char x) { int i = 0; for (i=0;i<rows;i++) { int j = 0; for (j=0;j<cols;j++) { board[i][j] = x; } } } //打印雷盘 void DisplayBoard(char board[ROWS][COLS], int row, int col) { system("cls"); printf("-----扫雷游戏-----\n"); int i = 0; int j = 0; for (j = 0; j <= col; j++)//打印列号 { printf("%d ",j); } printf("\n"); for (i = 1; i <= row; i++) { printf("%d ",i);//打印行 for (j = 1; j <=col; j++) { printf("%c ",board[i][j]); } printf("\n"); } } //布置雷 void SetMine(char mine[ROWS][COLS], int row, int col) { int count = Easy_Count; while (count) { int i = rand() % row + 1; int j = rand() % col + 1; if (mine[i][j] == '0') { mine[i][j] = '1'; count--; } } } //显示周围雷数 //方案一 static int Around_num1(char mine[ROWS][COLS], int x, int y) { return mine[x - 1][y] + mine[x - 1][y - 1] + mine[x - 1][y + 1] + mine[x][y - 1] + mine[x][y + 1] + mine[x + 1][y] + mine[x + 1][y - 1] + mine[x + 1][y + 1]-8*'0'; } //方案二 static int Around_num2(char mine[ROWS][COLS], int x, int y) { int i = 0; int count = 0; for (i = -1; i <= 1; i++) { int j = 0; for (j = -1; j <= 1; j++) { if (mine[x + i][y + j] == '1') count++; } } return count; } //标记雷 static void flag(char show[ROWS][COLS], int row, int col) { while (1) { int x = 0; int y = 0; printf("请输入标记坐标(输入:0 0退出):>"); scanf("%d %d",&x,&y); if (x >= 1 && x <= row && y >= 0 && y <= col) { if (show[x][y] != '#') { show[x][y] = '#'; break; } else { printf("该坐标已经标记,请重新选择\n"); } } else { printf("坐标越界,请重新输入\n"); } if (x == y && x == 0) break; } } //取消标记 static void cancel_flag(char show[ROWS][COLS], int row, int col) { while (1) { int x = 0; int y = 0; printf("请输入取消标记坐标(输入:0 0退出):>"); scanf("%d %d", &x, &y); if (x >= 1 && x <= row && y >= 0 && y <= col) { if (show[x][y] == '#')//比较用双等号!!! { show[x][y] = '*'; break; } else { printf("该坐标还未标记,请重新选择\n"); } } else { printf("坐标越界,请重新输入\n"); } if (x == y && x == 0) break; } } //如果排雷坐标周围雷数为零,递归展开 static void Digit_boom(char show[ROWS][COLS], char mine[ROWS][COLS], int x, int y, int row, int col, int* win) { if (x >= 1 && x <= row && y >= 1 && y <= col)//判断坐标合法性 { int ret = Around_num2(mine, x, y);//接受坐标周围雷的数量 if (ret == 0)//递归条件--周围雷数为0 { (*win)++;//每排查一个坐标,排查次数加1,为判断输赢做准备 show[x][y] = '0'; int i = 0; int j = 0; //用两个循环遍历周围8个坐标 for (i = -1; i <= 1; i++) { for (j = -1; j <= 1; j++) { if (show[x + i][y + j] == '*')//递归的坐标必须是未排查过的坐标,防止死递归 { Digit_boom(show, mine, x + i, y + j, row, col,win); } } } } else { //条件不满足退出递归 (*win)++;//排查坐标,次数加1 show[x][y] = ret + '0'; } } } //扫雷菜单 static void menu2() { printf("┌-----------------------------┐\n"); printf("├**********1.排雷*************┤\n"); printf("├**********2.标记*************┤\n"); printf("├**********3.取消标记*********┤\n"); printf("└-----------------------------┘\n"); } //扫雷并判断输赢 void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col) { int choice = -1; int x = 0; int y = 0; int win = 0; while (win < col * row - Easy_Count) { menu2(); printf("请选择:>"); scanf("%d", &choice); if (2 == choice) { flag(show, ROW, COL); DisplayBoard(show, ROW, COL); } else if (3 == choice) { cancel_flag(show, ROW, COL); DisplayBoard(show, ROW, COL); } else if(1==choice) { printf("请输入排查坐标:>"); scanf("%d%d", &x, &y); if (x >= 0 && x <= row && y >= 0 && y <= col) { if (mine[x][y] == '1') { DisplayBoard(mine, ROW, COL); printf("很遗憾,你被炸死了!\n"); break; } else if(show[x][y]=='*') { Digit_boom(show, mine, x, y, row, col, &win); DisplayBoard(show, row, col); } else { printf("该坐标已排查,请重新选择\n"); } } else { printf("输入坐标错误,请重新输入\n"); } } else { printf("选择错误,请重新选择\n"); } } if (win == col * row - Easy_Count) { DisplayBoard(mine, ROW, COL); printf("恭喜你,你赢了咯!\n"); } }
总结
由于作者水平有限,如笔下有误,敬请留言。
如果本文对您有所帮助,请给博主点赞👍关注🙏哦,笔者会持续更新干货教程,期待与君共勉!