童年回忆--扫雷(包括标记功能和递归展开)--万字讲解让你学会扫雷制作
- 魔王的介绍:😶🌫️一名双非本科大一小白。
- 魔王的目标:🤯努力赶上周围卷王的脚步。
- 魔王的主页:🔥🔥🔥大魔王.🔥🔥🔥
❤️🔥大魔王与你分享:人生人山人海人来人往,自己自尊自爱自由自在。
文章目录
- 一、前言
- 二、文件介绍
- 三、代码实现
- 1.创建基本的框架
- 2.#define定义常量
- 3.创建和初始化二维数组
- 4.埋雷
- 5.打印雷盘
- 6.玩家标记
- 7.玩家下棋
- 四、总代码如下
- `game.h`
- `game.c`
- `test.c`
- 效果展示图
- 五、总结
一、前言
小时候我们在家里刚买电脑的时候,网络还不是很发达(两三年纪吧),每次断网我们都除了电脑自带的那一点单机游戏外没什么玩的,其中最好玩的便是扫雷,那么你可以用代码实现扫雷吗?这里我们添加了两个功能:第一个是标记功能,如果确定为雷我们可以标记为!,如果不确定,我们可以标记为?,第二个功能是递归展开:我们知道如果我们我们所点的那个位置一周都不是雷,那么它会自动扩开,如果我们点的地方有雷,那么会显示出周围雷的个数,如果想尝试玩一下,点击扫雷可以跳转过去,那么接下来就学习以下如何代码实现吧!
二、文件介绍
test.c:测试文件
game.c:源文件,进行函数的定义
game.h:头文件(只进行声明)
我们用测试文件调用game.h声明、game.c文件中实现的函数,这就是三个文件的关系。
为了使三者联系起来,我们需要让两个源文件(.c)文件都包含头文件,这样他们中的函数就可以一起使用了。
三、代码实现
1.创建基本的框架
void menu()
{printf("************************\n");printf("******** 1.play ********\n");printf("******** 0.exit ********\n");printf("************************\n");
}
void test()
{int input = 0;do{menu();printf("请选择:> ");scanf("%d", &input);switch (input){case 1:printf("开始游戏\n");game();break;case 0:printf("退出游戏\n");break;default:printf("输入错误,请重新输入\n");}} while (input);}int main()
{test();return 0;
}
2.#define定义常量
因为我们在点击一个位置后如果附近有雷,那么就需要显示周围有几个雷,所以我们需要访问这个位置一周的内容,所以如果我们创建的表格大小和玩的时候打印出来的一样大,那么当访问边上那些位置周围雷的个数时,就会数组越界,所以我们如果要下99雷盘,我们需要让行和列都宽一个位置,也就是创建1111的雷盘,不过打印的话只打印中间的9*9格。所以为了方便我们先定义常量。
#define ROW 9#define COL 9#define ROWS ROW+2#define COLS COL+2#define mine_num 10
3.创建和初始化二维数组
和三子棋一样,我们要实现选中那个位置出现什么效果,只能是通过二维数组,让二位数组里的元素发生变化,在玩家视角就是自己操作了这个地方,我们需要创建两个二维数组,一个是布置雷区的二维数组,只在后台存在,并不显示出来,另一个则是给玩家展示的二维数组,至于为什么是两个,有很多原因,最容易说的就是:总不能把布置的雷展示给玩家吧,这样还怎么玩,不就开挂了。所以规定:让埋雷区初始化为字符0,展示区初始化为字符*,字符0是因为我们想用一个函数来初始化这两个雷盘,传的参数只需要最后传的字符不一样(一个是字符0,一个是字符*),其他都是一样的,所以为了方便之后的操作(两个雷盘间的联系),我们把两个数组都定义为char类型。
//初始化
void init_board(char board[ROWS][ROWS], int rows, int cols, char a)
{int i = 0;for (i = 0; i < rows; i++){int j = 0;for (j = 0; j < cols; j++){board[i][j] = a;}}
}
char mine[ROWS][COLS];
char show[ROWS][COLS];
//初始化雷区
init_board(mine, ROWS,COLS,'0');
//初始化展示区
init_board(show,ROWS,COLS, '*');
4.埋雷
在上篇智能三子棋博客中已经用到了随机数生成函数,点击智能三子棋可以跳转到该博客,里面有随机数的讲解。雷盘里我们初始化的是字符0,我们把埋雷的地方初始化为字符1,到计算的时候只需要让某个位置一周(8个)的数字字符相加减去7个字符0即可得到表示该位置有多少个雷的数字字符。
//放雷
void put_mine(char mine[ROWS][COLS], int rows, int cols)
{int num = 0;for (num = 0; num < mine_num; num++){while (1){int i = rand() % ROW + 1;int j = rand() % COL + 1;if (mine[i][j] == '0'){mine[i][j] = '1';break;}}}
}
//埋雷
put_mine(mine,ROWS, COLS);
5.打印雷盘
做完这些操作,我们就需要让玩家看到雷盘了,打印的时候因为行与列比较多,所以我们进行了排序,让玩家一看就知道自己要选择的地方的坐标。
//打印
void print_board(char board[ROWS][COLS], int rows, int cols)
{int i = 0;printf(" ");//为了对其每行,因为每行的开头加了下标,所以要先空出来下标所占的位置for (i = 1; i < cols-1; i++)//打印每列的下标{printf("%d ", i);}printf("\n");printf(" ");//为了对其每行,因为每行的开头加了下标,所以要先空出来下标所占的位置for (i = 1; i < cols-1; i++)//让每列的下标和其下面的元素隔开{printf("- ");}printf("\n");for (i = 1; i < rows-1; i++){printf("%d| ", i);//标记每行int j = 0;for (j = 1; j < cols-1; j++){printf("%c ", board[i][j]);}printf("\n");}
}
//打印雷盘
//print_board(mine, ROWS, COLS);//上帝视角,打印雷区
//printf("\n");
print_board(show, ROWS, COLS);
6.玩家标记
显示出展示区后玩家就开始下棋了,每次玩家在下棋前,我们都要问是否进行标记,然后标记一次后继续问是要标记还是要退出标记进行扫雷。标记的时候我们需要先判断玩家选择的位置是否在范围内(1~9),然后还要判断是否该位置已经被扫过了(如果没被扫过,该位置坐标只能是’*‘,’!‘,’?')
//玩家标记
void mark_board(char show[ROWS][COLS], int rows, int cols)
{int flag = 0;while (1){printf("选择标记方式:按1标记为雷(!),按2标记为不确定(?),按3取消标记(*),选择4进入排雷环节\n");printf("请玩家选择标记方式:> ");scanf("%d", &flag);if (flag == 1){int i = 0;int j = 0;printf("玩家选择标记位置:> ");scanf("%d %d", &i, &j);if (i > 0 && i <= ROW && j > 0 && j <= COL){if (show[i][j] != '*'&&show[i][j]!='?'){printf("该位置已经扫过或者已经被标记了该种符号,请重新选择\n");continue;}show[i][j] = '!';}}else if (flag == 2){int i = 0;int j = 0;printf("玩家选择:> ");scanf("%d %d", &i, &j);if (i > 0 && i <= ROW && j > 0 && j <= COL){if (show[i][j] != '*' && show[i][j] != '!'){printf("该位置已经扫过或者已经被标记了该种符号,请重新选择\n");continue;}show[i][j] = '?';}}else if (flag == 3)//取消标记,变为原来的*{int i = 0;int j = 0;printf("玩家选择:> ");scanf("%d %d", &i, &j);if (i > 0 && i <= ROW && j > 0 && j <= COL){if (show[i][j] == '?' || show[i][j] == '!'){show[i][j] = '*';}else{printf("该位置已经扫过或者已经被标记了该种符号,请重新选择\n");continue;}}}else if (flag == 4)return;elseprintf("选择错误,请重新选择\n");system("cls");print_board(show, ROWS, COLS);}
}
mark_board(show, ROWS, COLS);//每次输入前的标记环节
7.玩家下棋
因为玩家下棋和递归展开、判断周围雷个数以及输赢是连起来的,所以这个部分比较多。
- 当扫的雷的个数等于(玩家看到的整个雷盘的个数减去雷的个数)的时候,如果程序还没有结束(也就是这期间没被炸死),那么就说明玩家扫雷成功,游戏胜利。
- 玩家在进行选择排雷位置时,选择的位置需要合理(1~9),然后还需要没被扫过。
- 当玩家选择的位置一周都没有雷时,该位置由*变为空格(也就是说只有这个位置一周都没有雷,才变为空格,并且展开一周,否则变为周围雷的个数),并且周围全部展开(每展开一个都要让扫雷的个数+1,否则就不能胜利了),然后继续判断,直到附近有雷时,显示出雷的个数。
- 当玩家选择的坐标正好是雷时,游戏结束,扫雷失败。
- 我们第三个所提到的展开一片所用到的就是递归:当我们输入的坐标周围不是雷时,展开周围的全部然后再在周围的这八个里面依次判断是否符合,如果符合就变为空格并且继续展开一篇,如果周围有雷,就不再展开,该位置变为周围雷的个数。
- 上面的这个如果不经处理就会导致死循环,因为我们选的这个如果周围没雷,就会展开一片,那么这一片又与我们选的这个相邻,那么又会回去判断我们选择的这个位置,然后就这样重复,最终会导致内存崩了。所以我们在进行递归时加一个限制条件,它必须是!、?或*才可以,因为如果是数字字符或空格,说明这个地方已经判断过了,那么就不能再判断这个位置。
判断周围雷的个数时,因为我们最终想要的是一个数字字符形式,所以我们在让要判断的这个位置周围雷的个数时,我们需要让周围相加减去7个字符0,而不是8个。- 我们在表达某个位置一周的坐标的时候,采取的是遍历法,用两个for循环找到这个位置一周的坐标并判断他们是显示数字还是再展开一片。
- 递归展开是根据安 度 因的扫雷博客自己写出来的,点击安度因可以跳转过去。
//周围雷个数
char around_mine(char mine[ROWS][COLS], int i, int j)
{return mine[i][j - 1] + mine[i][j + 1] + mine[i + 1][j] + mine[i - 1][j] + mine[i - 1][j - 1] + mine[i + 1][j + 1] + mine[i - 1][j + 1] + mine[i + 1][j - 1] - 7 * '0';//一共是8个字符0,但因为我们要用到一个,所以减掉7个,用的那一个是因为我们想以字符数字的形式赋过去,因为数组是字符类型的
}//递归展开
void recursion(char mine[ROWS][COLS], char show[ROWS][COLS], int i, int j, int* p)
{int x = 0;int y = 0;for (x = -1; x <= 1; x++){for (y = -1; y <= 1; y++){if (x + i > 0 && x + i <= ROW && y + j > 0 && y + j <= COL)//防止越界{if (mine[i + x][j + y] == '0' && (show[i + x][j + y] == '*' || show[i + x][j + y] == '?' || show[i + x][j + y] == '!')){(*p)++;if (around_mine(mine, i + x, j + y) == '0'){show[i + x][j + y] = ' ';recursion(mine, show, i + x, j + y, p);}else{show[i + x][j + y] = around_mine(mine, i + x, j + y);//recursion(mine, show, i + x, j + y, p);}}elsecontinue;}}}
}//玩家选择坐标
int player_option(char mine[ROWS][COLS], char show[ROWS][COLS], int rows, int cols,int* p)
{int i = 0;int j = 0;printf("玩家选择:> ");scanf("%d %d", &i, &j);if (i > 0 && i <= ROW && j > 0 && j <= COL){if (mine[i][j] == '1'){return 0;}if (show[i][j] == '*'||show[i][j]=='!'||show[i][j]=='?'){(*p)++; show[i][j] = around_mine(mine, i, j);if (show[i][j] == '0'){show[i][j] = ' ';recursion(mine, show, i, j, p);//递归展开}/*elserecursion(mine, show, i, j, p);*/return 1;}else{printf("该位置已经排过,请重新选择位置\n");Sleep(1500);return 1;}}
}//玩家进行标记和排雷
int num = 0;
int ret = 0;
while (num<ROW*COL-mine_num)//当重复的次数为所有非雷个数后跳出循环,胜利
{mark_board(show, ROWS, COLS);//每次输入前的标记环节ret = player_option(mine, show, ROWS, COLS, &num);system("cls");print_board(show, ROWS, COLS);if (ret == 0){printf("很遗憾,你被炸死了,雷区如下:\n");print_board(mine, ROWS, COLS);//让玩家死后看到这盘扫雷游戏中雷都在哪里break;}//print_board(show, ROWS, COLS);
}
if (num == ROW * COL - mine_num)printf("恭喜你扫除了全部的雷!!!\n");
四、总代码如下
game.h
#pragma once#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <windows.h>#define ROW 9#define COL 9#define ROWS ROW+2#define COLS COL+2#define mine_num 10//初始化
void init_board(char board[ROWS][ROWS], int rows, int cols, char a);//埋雷
void put_mine(char mine[ROWS][COLS],int rows, int cols);//打印
void print_board(char board[ROWS][COLS], int rows, int cols);//玩家选择坐标
int player_option(char mine[ROWS][COLS], char show[ROWS][COLS], int rows, int cols);//玩家标记
void mark_board(char show[ROWS][COLS], int rows, int cols);
game.c
#define _CRT_SECURE_NO_WARNINGS 1#include "game.h"//初始化
void init_board(char board[ROWS][ROWS], int rows, int cols, char a)
{int i = 0;for (i = 0; i < rows; i++){int j = 0;for (j = 0; j < cols; j++){board[i][j] = a;}}
}//放雷
void put_mine(char mine[ROWS][COLS], int rows, int cols)
{int num = 0;for (num = 0; num < mine_num; num++){while (1){int i = rand() % ROW + 1;int j = rand() % COL + 1;if (mine[i][j] == '0'){mine[i][j] = '1';break;}}}
}//打印
void print_board(char board[ROWS][COLS], int rows, int cols)
{int i = 0;printf(" ");//为了对其每行,因为每行的开头加了下标,所以要先空出来下标所占的位置for (i = 1; i < cols-1; i++)//打印每列的下标{printf("%d ", i);}printf("\n");printf(" ");//为了对其每行,因为每行的开头加了下标,所以要先空出来下标所占的位置for (i = 1; i < cols-1; i++)//让每列的下标和其下面的符号隔开{printf("- ");}printf("\n");for (i = 1; i < rows-1; i++){printf("%d| ", i);//标记每行int j = 0;for (j = 1; j < cols-1; j++){printf("%c ", board[i][j]);}printf("\n");}
}//周围雷个数
char around_mine(char mine[ROWS][COLS], int i, int j)
{return mine[i][j - 1] + mine[i][j + 1] + mine[i + 1][j] + mine[i - 1][j] + mine[i - 1][j - 1] + mine[i + 1][j + 1] + mine[i - 1][j + 1] + mine[i + 1][j - 1] - 7 * '0';//一共是8个字符0,但因为我们要用到一个,所以减掉7个,用的那一个是因为我们想以字符数字的形式赋过去,因为数组是字符类型的
}//递归展开
void recursion(char mine[ROWS][COLS], char show[ROWS][COLS], int i, int j, int* p)
{int x = 0;int y = 0;for (x = -1; x <= 1; x++){for (y = -1; y <= 1; y++){if (x + i > 0 && x + i <= ROW && y + j > 0 && y + j <= COL)//防止越界{if (mine[i + x][j + y] == '0' && (show[i + x][j + y] == '*' || show[i + x][j + y] == '?' || show[i + x][j + y] == '!')){(*p)++;if (around_mine(mine, i + x, j + y) == '0'){show[i + x][j + y] = ' ';recursion(mine, show, i + x, j + y, p);}else{show[i + x][j + y] = around_mine(mine, i + x, j + y);//recursion(mine, show, i + x, j + y, p);}}elsecontinue;}}}
}//玩家选择坐标
int player_option(char mine[ROWS][COLS], char show[ROWS][COLS], int rows, int cols,int* p)
{int i = 0;int j = 0;printf("玩家选择:> ");scanf("%d %d", &i, &j);if (i > 0 && i <= ROW && j > 0 && j <= COL){if (mine[i][j] == '1'){return 0;}if (show[i][j] == '*'||show[i][j]=='!'||show[i][j]=='?'){(*p)++; show[i][j] = around_mine(mine, i, j);if (show[i][j] == '0'){show[i][j] = ' ';recursion(mine, show, i, j, p);//递归展开}/*elserecursion(mine, show, i, j, p);*/return 1;}else{printf("该位置已经排过,请重新选择位置\n");Sleep(1500);return 1;}}
}//玩家标记
void mark_board(char show[ROWS][COLS], int rows, int cols)
{int flag = 0;while (1){printf("选择标记方式:按1标记为雷(!),按2标记为不确定(?),按3取消标记(*),选择4进入排雷环节\n");printf("请玩家选择标记方式:> ");scanf("%d", &flag);if (flag == 1){int i = 0;int j = 0;printf("玩家选择标记位置:> ");scanf("%d %d", &i, &j);if (i > 0 && i <= ROW && j > 0 && j <= COL){if (show[i][j] != '*'&&show[i][j]!='?'){printf("该位置已经扫过或者已经被标记了该种符号,请重新选择\n");continue;}show[i][j] = '!';}}else if (flag == 2){int i = 0;int j = 0;printf("玩家选择:> ");scanf("%d %d", &i, &j);if (i > 0 && i <= ROW && j > 0 && j <= COL){if (show[i][j] != '*' && show[i][j] != '!'){printf("该位置已经扫过或者已经被标记了该种符号,请重新选择\n");continue;}show[i][j] = '?';}}else if (flag == 3){int i = 0;int j = 0;printf("玩家选择:> ");scanf("%d %d", &i, &j);if (i > 0 && i <= ROW && j > 0 && j <= COL){if (show[i][j] == '?' || show[i][j] == '!'){show[i][j] = '*';}else{printf("该位置已经扫过或者已经被标记了该种符号,请重新选择\n");continue;}}}else if (flag == 4)return;elseprintf("选择错误,请重新选择\n");system("cls");print_board(show, ROWS, COLS);}
}
test.c
#define _CRT_SECURE_NO_WARNINGS 1#include "game.h"void menu()
{printf("************************\n");printf("******** 1.play ********\n");printf("******** 0.exit ********\n");printf("************************\n");
}
void game()
{char mine[ROWS][COLS];char show[ROWS][COLS];//初始化雷区init_board(mine, ROWS,COLS,'0');//初始化展示区init_board(show,ROWS,COLS, '*');//埋雷put_mine(mine,ROWS, COLS);//打印雷盘//print_board(mine, ROWS, COLS);//上帝视角,打印雷区//printf("\n");print_board(show, ROWS, COLS);//玩家进行标记和排雷int num = 0;int ret = 0;while (num<ROW*COL-mine_num)//当重复的次数为所有非雷个数后跳出循环,胜利{mark_board(show, ROWS, COLS);//每次输入前的标记环节ret = player_option(mine, show, ROWS, COLS, &num);system("cls");print_board(show, ROWS, COLS);if (ret == 0){printf("很遗憾,你被炸死了,雷区如下:\n");print_board(mine, ROWS, COLS);//让玩家死后看到这盘扫雷游戏中雷都在哪里break;}//print_board(show, ROWS, COLS);}if (num == ROW * COL - mine_num)printf("恭喜你扫除了全部的雷!!!\n");
}void test()
{int input = 0;do{menu();printf("请选择:> ");scanf("%d", &input);switch (input){case 1:printf("开始游戏\n");game();break;case 0:printf("退出游戏\n");break;default:printf("输入错误,请重新输入\n");}} while (input);}int main()
{srand((unsigned int)time(NULL));test();return 0;
}
效果展示图
五、总结
💘原创不易,还希望各位佬佬支持一下\textcolor{colourful}{💘原创不易,还希望各位佬佬支持一下}💘原创不易,还希望各位佬佬支持一下
💓点赞,你的认可是我创作的动力!\textcolor{red}{💓点赞,你的认可是我创作的动力!}💓点赞,你的认可是我创作的动力!
💕收藏,你的青睐是我努力的方向!\textcolor{orange}{💕收藏,你的青睐是我努力的方向!}💕收藏,你的青睐是我努力的方向!
💞评论,你的意见是我进步的财富!\textcolor{aqua}{💞评论,你的意见是我进步的财富!}💞评论,你的意见是我进步的财富!
✨请点击下面进入主页关注大魔王
如果感觉对你有用的话,就点我进入主页关注我吧!