⭐博客主页:️CS semi主页
⭐欢迎关注:点赞收藏+留言
⭐系列专栏:C语言初阶
⭐代码仓库:C Advanced
家人们更新不易,你们的点赞和关注对我而言十分重要,友友们麻烦多多点赞+关注,你们的支持是我创作最大的动力,欢迎友友们私信提问,家人们不要忘记点赞收藏+关注哦!!!
扫雷
前言
1.规则和玩法
扫雷是一款大众类的益智小游戏,目标是在最短的时间内根据点击格子出现的数字找出所有非雷格子,同时避免踩雷,踩到一个雷即全盘皆输。其胜利条件不是将所有地雷插上旗子,而是点开所有不是地雷的格子。
2.对读者说的话
利用C语言制作扫雷对于初学者是比较复杂的,但利用所学的初阶C语言可以制作一个可以玩的很开心的扫雷游戏,如果大家对于扫雷代码感到有点吃不消,下面我放一个我之前自己创作的三子棋的入门教程,能帮助大家更好地理解C语言扫雷制作的方法。
三子棋教程
ps:这篇博客用的是9*9的棋盘和10颗雷,如果想改变棋盘和雷的数量都可以在宏定义上更改!
一、基本思路分析
1.创建一个菜单(menu),供玩家选择玩还是退出
2.制作一个switch语句,控制游戏的开始与退出
3.进入游戏,创建两个二维数组,一个存放布置好的雷的信息,另一个存放排查出的雷的信息(边界要加2)
4.开始游戏
5.初始化棋盘
6.打印棋盘
7.电脑随机布置雷
8.电脑排查周围的雷并打印在棋盘上(简易版)
9.递归实现排开一片区域
10.判断是否排雷成功
二、实现步骤(详细解释)
1.创建文件
如图:大家需要创建一个头文件(game.h)和两个源文件(game.c)、(test.c),这样分工程的制作一个项目,能够更加清晰明了,也能够及时发现错误的地方。头文件game.h内存放的是该项目中的头文件,在另外两个文件中可以输入#include“game.h”,这样更加方便又清晰;源文件test.c中放的是游戏开始或者退出的代码;源文件内game.c实现的是让游戏运行的代码,是整个工程当中最核心的部分也是最重要的部分,经过这么分析,大家应该更加明白设计这么多个文件的重要性了吧!
如下两图:使用#include"game.h",使得代码更加简洁明了。
2.创建菜单
在菜单创建这里比较灵活,大家可以根据自己的习惯创立一个菜单,我用的菜单比较简单,代码如下:
test.c
void menu() { //打印菜单
printf("****************************\n");
printf("***冒险者,请选择是否冒险***\n");
printf("**********1.游戏开始********\n");
printf("**********0.退出游戏********\n");
}
3.控制游戏开始或退出
先上代码再解释:
test.c
void test() {
int input = 0;
//srand((unsigned int)time(NULL)); //随机生成的种子
printf("扫雷游戏\n");
menu();
do {
printf("请输入:>");
scanf("%d", &input);
switch (input) {
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (input); //input为非零则为真,在程序里面执行;若为零则为假,跳出程序
}
利用一个switch语句,用户输入1或者0来控制扫雷游戏的进行或者退出,大家可能会想了,为什么我把一个随机生成的种子给注释了?大家不要着急,这个随机种子的设置是为了后续电脑随机布置雷所使用的。
4.开始游戏
test.c
void game() {
printf("开始游戏\n");
//1.需要准备两个二维数组,一个数组用来存放布置好的雷的信息,另一个数组用来存放排查出雷的信息
//2.很奇怪的点,如果排查在边界怎么办,很简单,为了防止越界,那就行增加两行,列增加两列
char mine[ROWS][COLS] = { 0 }; //存放布置好的雷的信息
char show[ROWS][COLS] = { 0 }; //存放排查出的雷的信息
}
大家可能看不懂ROWS和COLS,大家往下继续看,接下来我会跟大家解释这两个“诡异”的字母。
5.初始化键盘
上面的程序都是在源文件test.c与头文件game.h中写的,那最核心的game.c程序呢?加下来我们将花费大多数时间来写源文件game.c内的代码。
这里需要所有地方的代码:game.h 、 test.c 和 game.c
game.h
#define ROW 9 //行的行数
#define COL 9 //列的列数
#define ROWS ROW+2 //行数+2,防止越界
#define COLS COL+2 //列数+2,防止越界
//#define MineCount 10 //雷的个数
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
利用宏定义定义一个9行9列的棋盘,为了防止排查雷的时候越界,故行和列都增加2。如下图:以99的棋盘为例,如果雷在99的边界,排查雷的时候是排查雷的周围8个地方,发现会越界唉,所以需要行和列增加两个单位来防止越界。
game.c
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set) {//传入‘0’或者‘*’表示对棋盘的初始化
int i = 0;
for (i = 0; i < rows; i++) {
int j = 0;
for (j = 0; j < cols; j++) {
board[i][j] = set; //定义board[ROWS][COLS]是为了不跟show和mine搞混
}
}
}
test.c
//初始化棋盘
InitBoard(mine, ROWS, COLS, '0'); //'0'表示这个地方是没有雷的
InitBoard(show, ROWS, COLS, '*'); //‘*’表示埋雷,把所有地方埋掉
初始化这个棋盘,定义‘0’为没有雷,*’表示埋雷,把整个棋盘都用**表示,呈现在大家的控制台中。
6.打印棋盘
game.h
//打印棋盘
void PrintBoard(char board[ROWS][COLS], int row, int col);
geme.c
//打印棋盘
void PrintBoard(char board[ROWS][COLS], int row, int col) {
int i = 0;
int j = 0;
printf("~~~~~~~~~~~扫雷~~~~~~~~~~~~~~\n");
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");
}
printf("~~~~~~~~~~~扫雷~~~~~~~~~~~~~~\n");
}
想要埋雷,先要隐藏雷,在这99的草皮中,光秃秃的地皮肯定不好埋,玩家一看这边被挖过了就知道这边有雷,不会过去,但是,倘若你在这片区域放上草皮,隐藏一下,玩家就不能一眼看出哪里藏雷,就需要根据推理来判断哪里有雷,所以让这块99的区域都打印成”*“。
test.c
//打印棋盘
/*PrintBoard(mine, ROW, COL);*/
PrintBoard(show, ROW, COL);
大家可能会好奇,为什么有个注释行?原因其实很简单,我们把注释行放开,发现原来控制台会出现两个9*9的方格,你可以进行比对了呀!检查检查计算机有没有按照你的心意先初始化不赋地方为雷。
7.随机布置雷
game.h
//布置雷
void MineSet(char mine[ROWS][COLS], int row, int col);//传入mine[ROWS][COLS]方便看出是传入的是雷的信息
game.c
//布置雷
void MineSet(char mine[ROWS][COLS], int row, int col) {
int count = MineCount; //十个雷
while (count) {
//下标的随机生成
int x = rand() % row + 1; //模row(9)生成随机数0到8后再加一为1到9
int y = rand() % col + 1; //同上
//布置雷
while (mine[x][y] == '0') {
mine[x][y] = '1'; //雷表示'1',没有雷的地方赋个'1',代表加一个雷
count--; //打印一个雷减一个雷,直到减到0
}
}
}
test.c
//布置雷(随机布置)
MineSet(mine, ROW, COL); //布置雷是布置在9*9的方格当中,所以传参传的是ROW和COL
/*PrintBoard(mine, ROW, COL);*/
注释行的作用为打印出雷的具体位置,大家可以看一看计算机有没有出错,以及了解一下雷的具体位置。如下图,数一数,果然是十个,计算机没有骗我!
void test() {
//int input = 0;
srand((unsigned int)time(NULL)); //随机生成的种子
//printf("扫雷游戏\n");
//menu();
/*do {
printf("请输入:>");
scanf("%d", &input);
switch (input) {
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (input);*/ //input为非零则为真,在程序里面执行;若为零则为假,跳出程序
}
这里的随机函数种子就存在很大的作用了,电脑随机生成十个雷。
8.排查雷(简易版)
这个只是比较简易版的排雷,也就是只有比较简单的排查周围八个位置有没有雷,而真正的排雷应该是点开一个区域,如果为0,周围一片没有雷的地方都应该展开,所以,在第9个模块我将会详细跟大家解释如果利用递归来实现排一大片的雷!
game.h
//排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
game.c
大家可能对于周围八个区域空间的雷的信息很头大,这怎么做呀?其实不难,大家想想为什么之前我定义了一个’0’和’1’,这边就起到很重要的作用了,大家看,‘1’的ASCII码为49,‘0’的ASCII码为48,要是想找到周围的雷,是不是只需要将这八个加起来并减去8*‘0’就能知道周围有几个雷了。原来那么简单!动起手来!
//排查雷
int get_mine_count(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'); //x,y坐标周围八个坐标的代表是字符'0'或者'1',字符0代表的ASCII码数为48,‘1’代表的ASCII码数为49,只要是减去8*‘0’则可以显示出周围有几个雷
}
test.c
下面这个代码其实要和最后一步一起用才可以,所以,现在这里放一下给大家一个预热!
//排查雷
FindMine(mine, show, ROW, COL);
9.排查雷(扩展版)
正式版的终于来了,御用的思想主要是递归(这里只放上game.c的代码,其余与第8个模块一致)
先解析一下,如图,判断紫色方框周围八个位置是否存在雷,发现是有的,但是正下方是没有雷的,此时需要递归去判断褐色方框周围存不存在雷,再进行判断,直到排不出附近那一大片位置的雷,也就是这片区域没有雷,形成一个半包的样子。
game.c
//排查雷
/*int get_mine_count(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'); //x,y坐标周围八个坐标的代表是字符'0'或者'1',字符0代表的ASCII码数为48,‘1’代表的ASCII码数为49,只要是减去8*‘0’则可以显示出周围有几个雷
}*/
//判断周围有没有雷
void Broadboom(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y) {
//判断坐标是否越界
if (x == 0 || y == 0 || x == ROWS - 1 || y == COLS - 1) {
return 0;
}
//判断是否已经被排除
if (show[x][y] != '*') {
return 0;
}
int count = get_mine_count(mine, x, y);
if (count > 0) {
show[x][y] = count + '0';
return 0;
}
//递归实现排雷
else if (count == 0) {
show[x][y] = '0';
Broadboom(mine, show, x - 1, y);
Broadboom(mine, show, x - 1, y - 1);
Broadboom(mine, show, x, y - 1);
Broadboom(mine, show, x + 1, y - 1);
Broadboom(mine, show, x + 1, y);
Broadboom(mine, show, x + 1, y + 1);
Broadboom(mine, show, x, y + 1);
Broadboom(mine, show, x - 1, y + 1);
}
}
那肯定是要先判断这个坐标有没有越界和是否已经被排查过了,引进一个count等于上面排查的雷的数量,如果它大于0,那很简单呀,只需要显示这一个位置旁边雷的个数就好了,但如果是0怎么办?其实很简单,运用一个递归就能解决了!往它其他八个位置递归,再判断这个坐标的周围八个位置的再周围的八个位置是不是有雷,大家可能会疑惑,那判断过的地方呢?哈哈,我早有想到,递归进去的还是这个函数,继续判断是否越界和是否已经被排查过,让计算机一直运作,我们就看最终出现在我们眼前的黑窗口就好了!
10.判断输赢
game.h
与第8个模块相同
//判断输赢
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
game.c
//判断游戏输赢
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col) {
int x = 0;
int y = 0;
int win = 0; //判断成功条件
while (win < (row * col - MineCount)) { //81-10=71 排查出71个地方不是雷就可以跳出循环了
printf("请输入要排查的坐标>");
scanf("%d %d", &x, &y);
if ((x >= 1 && x <= row) && (y >= 1 && y <= col)) { //判断坐标是否合法
//缺少一个讨论的问题,没有做出来已经被排查的雷,如果这个地方被排查过了,就不需要再排查了
if (show[x][y] != '*') {
printf("该地方已经被排查过了!\n");
continue;
}
if (mine[x][y] == '1') { //踩雷成功
printf("很遗憾,你踩到雷了\n");
PrintBoard(mine, ROW, COL);
break;
}
else {
Broadboom(mine, show, x, y);
/*show[x][y] = get_mine_count(mine, x, y) + '0';*/
int n = get_mine_count(mine, x, y); //重新定义一个函数传进去找周围雷的个数
show[x][y] = n + '0'; //ASCII码值为n+'0',则为周围几个雷
PrintBoard(show, ROW, COL);
win++;
}
}
else {
printf("输入坐标错误,请重新输入\n");
}
}
if (win == (row * col - MineCount)) {
printf("恭喜你,排雷成功\n");
}
}
test.c
与第8个模块代码一样
//排查雷
FindMine(mine, show, ROW, COL);
代码汇总
game.h
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#define ROW 9 //行的行数
#define COL 9 //列的列数
#define ROWS ROW+2 //行数+2,防止越界
#define COLS COL+2 //列数+2,防止越界
#define MineCount 10 //雷的个数
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
//打印棋盘
void PrintBoard(char board[ROWS][COLS], int row, int col);
//布置雷
void MineSet(char mine[ROWS][COLS], int row, int col);
//排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
game.c
#include"game.h"
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set) {
int i = 0;
for (i = 0; i < rows; i++) {
int j = 0;
for (j = 0; j < cols; j++) {
board[i][j] = set;
}
}
}
//打印棋盘
void PrintBoard(char board[ROWS][COLS], int row, int col) {
int i = 0;
int j = 0;
printf("~~~~~~~~~~~扫雷~~~~~~~~~~~~~~\n");
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");
}
printf("~~~~~~~~~~~扫雷~~~~~~~~~~~~~~\n");
}
//布置雷
void MineSet(char mine[ROWS][COLS], int row, int col) {
int count = MineCount; //十个雷
while (count) {
//下标的随机生成
int x = rand() % row + 1; //模row(9)生成随机数0到8后再加一为1到9
int y = rand() % col + 1; //同上
//布置雷
while (mine[x][y] == '0') {
mine[x][y] = '1'; //雷表示'1',没有雷的地方赋个'1',代表加一个雷
count--; //打印一个雷减一个雷,直到减到0
}
}
}
//排查雷
int get_mine_count(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'); //x,y坐标周围八个坐标的代表是字符'0'或者'1',字符0代表的ASCII码数为48,‘1’代表的ASCII码数为49,只要是减去8*‘0’则可以显示出周围有几个雷
}
//判断周围有没有雷
void Broadboom(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y) {
//判断坐标是否越界
if (x == 0 || y == 0 || x == ROWS - 1 || y == COLS - 1) {
return 0;
}
//判断是否已经被排除
if (show[x][y] != '*') {
return 0;
}
int count = get_mine_count(mine, x, y);
if (count > 0) {
show[x][y] = count + '0';
return 0;
}
//递归实现排雷
else if (count == 0) {
show[x][y] = '0';
Broadboom(mine, show, x - 1, y);
Broadboom(mine, show, x - 1, y - 1);
Broadboom(mine, show, x, y - 1);
Broadboom(mine, show, x + 1, y - 1);
Broadboom(mine, show, x + 1, y);
Broadboom(mine, show, x + 1, y + 1);
Broadboom(mine, show, x, y + 1);
Broadboom(mine, show, x - 1, y + 1);
}
}
//判断游戏输赢
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col) {
int x = 0;
int y = 0;
int win = 0; //判断成功条件
while (win < (row * col - MineCount)) { //81-10=71 排查出71个地方不是雷就可以跳出循环了
printf("请输入要排查的坐标>");
scanf("%d %d", &x, &y);
if ((x >= 1 && x <= row) && (y >= 1 && y <= col)) { //判断坐标是否合法
//缺少一个讨论的问题,没有做出来已经被排查的雷,如果这个地方被排查过了,就不需要再排查了
if (show[x][y] != '*') {
printf("该地方已经被排查过了!\n");
continue;
}
if (mine[x][y] == '1') { //踩雷成功
printf("很遗憾,你踩到雷了\n");
PrintBoard(mine, ROW, COL);
break;
}
else {
Broadboom(mine, show, x, y);
/*show[x][y] = get_mine_count(mine, x, y) + '0';*/
int n = get_mine_count(mine, x, y); //重新定义一个函数传进去找周围雷的个数
show[x][y] = n + '0'; //ASCII码值为n+'0',则为周围几个雷
PrintBoard(show, ROW, COL);
win++;
}
}
else {
printf("输入坐标错误,请重新输入\n");
}
}
if (win == (row * col - MineCount)) {
printf("恭喜你,排雷成功\n");
}
}
test.c
#include"game.h"
void game() {
printf("开始游戏\n");
//1.需要准备两个二维数组,一个数组用来存放布置好的雷的信息,另一个数组用来存放排查出雷的信息
//2.很奇怪的点,如果排查在边界怎么办,很简单,为了防止越界,那就行增加两行,列增加两列
char mine[ROWS][COLS] = { 0 }; //存放布置好的雷的信息
char show[ROWS][COLS] = { 0 }; //存放排查出的雷的信息
//初始化棋盘
InitBoard(mine, ROWS, COLS, '0'); //'0'表示这个地方是没有雷的
InitBoard(show, ROWS, COLS, '*'); //‘*’表示埋雷,把所有地方埋掉
//打印棋盘
/*PrintBoard(mine, ROW, COL);*/
PrintBoard(show, ROW, COL);
//布置雷(随机布置)
MineSet(mine, ROW, COL); //布置雷是布置在9*9的方格当中,所以传参传的是ROW和COL
/*PrintBoard(mine, ROW, COL);*/
//排查雷
FindMine(mine, show, ROW, COL);
}
void menu() { //打印菜单
printf("****************************\n");
printf("***冒险者,请选择是否冒险***\n");
printf("**********1.游戏开始********\n");
printf("**********0.退出游戏********\n");
}
void test() {
int input = 0;
srand((unsigned int)time(NULL)); //随机生成的种子
printf("扫雷游戏\n");
menu();
do {
printf("请输入:>");
scanf("%d", &input);
switch (input) {
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (input); //input为非零则为真,在程序里面执行;若为零则为假,跳出程序
}
int main() {
test();
return 0;
}
总结
用初阶C语言写扫雷小游戏这个童年回忆小游戏确实有点难,但大家要吃的下这份苦来,一步一步来,按照这篇博客慢慢来终能学会写这个程序。扫雷小游戏运用了很多函数、数组和一部分递归来解决,只要基础够扎实就能写出这个程序,加油!!!
客官,都读到这里了,赏脸给个三连呗~~~