C语言小项目 -- 扫雷游戏完整代码(递归展开 + 选择标记)
目录
🚀0. 游戏介绍:
🐷 游戏规则
🛸雷诀八条
🚀1. 开发环境及框架:
🚀2. 游戏具体功能分析实现:
🐷2.1 棋盘
🐷2.2 棋盘初始化 init_Board;
🐷2.2 棋盘打印 show_board;
🐷2.3 布置雷 set_mine
🐷2.4 排查雷及判断输赢 fine_mine
🐷2.5 按规则展开static void explosion_spread
🐷2.6 获取周围雷的个数get_mine_count
🐷2.7 标记雷mark_mine
🚀3. 游戏完整代码:
🛰️MineSweeper.h——头文件
🛰️MineSweeper.c——源文件
🛰️MineSweepertest.c
🚀3. 游戏效果图:
🚀0. 游戏介绍:
《扫雷》是一款大众类的益智小游戏,于1992年发行。游戏目标是在最短的时间内根据点击格子出现的数字找出所有非雷格子,同时避免踩雷,踩到一个雷即全盘皆输。
🐷 游戏规则
在一个9×9(初级)、16×16(中级)、16×30(高级)或自定义大小的方块矩阵中随机布置一定量的地雷(初级为10个,中级为40个,高级为99个),再由玩家逐个翻开方块,以找出所有地雷为最终游戏目标。如果玩家翻开的方块有地雷,则游戏结束。
🛸雷诀八条
- 第一条:基本定式不要忘,现场推理真够呛。
- 第二条:鼠标点击不要快,稳定节奏把空开。
- 第三条:顺手标雷不要惯,积累下来记录悬。
- 第四条:无从下手不要愣,就近猜雷把心横。
- 第五条:遇到猜雷不要怕,爆了脸上不留疤。
- 第六条:猜雷猜错不要悔,哭天抢地也白费。
- 第七条:碰上好局不要慌,紧盯局部慢扩张。
- 第八条:痛失好局不要恨,既然有缘定有份。
🚀1. 开发环境及框架:
本人开发环境采用—VS2022,学会调试,参考文章调试规则,以及windows自带画图板
采取工程内多文件进行链接编译,包括:
- MineSweeper.h——头文件,建议把所有的常量、宏、系统全局变量和函数原型写在头文件中,在需要的时候随时引用这些头文件。
- MineSweeper.c——源文件,里边含各种函数的具体功能实现
- MineSweepertest.c——测试扫雷游戏的使用,包含主体运行,主函数等
🚀2. 游戏具体功能分析实现:
🐷2.1 棋盘
如何定义初始化棋盘,便是需要解决的第一个问题
可知棋盘用二维数组更便于表示:
char show[ROWS][COLS];//展示棋盘char mine[ROWS][COLS];//雷盘
#define ROW 9 #define COL 9
定义了两个棋盘,分别用来保存布置雷的信息和排查雷的信息,这样就可以避免二者相互干扰或者相互覆盖;
同时,我们使用宏定义的标识符常量表示雷盘的大小以及雷的个数,想增加扫雷的难度的时候,我们只需要改动这里一次即可,增加了代码的可维护性。
问题分析:
如图:当我们排查x位置时,如果x处不是雷,那么我们就会依次检查其周围8个坐标是否有雷,如果有,就会把周围雷的总数量显示在x位置处;但是当我们排查x位置时,我们发现, 数组排查雷时会发生越界,所以为了避免数组越界,我们就需要增加一系列限制条件,这样做无疑是比较麻烦的,所以有的大佬就想出了这样一种办法:在定义数组长度时我们直接在上下左右四个方向各多给一行的空间,并把这些空间中的数据初始化为非雷,这样,就轻松解决了数组越界的问题!
#define ROW 9 #define COL 9#define ROWS (ROW+2) #define COLS (COL+2)
🐷2.2 棋盘初始化 init_Board
char show[ROWS][COLS];//展示棋盘char mine[ROWS][COLS];//雷盘
对实列化的展示棋盘,以及雷盘进行初始化
void init_Board(char show[ROWS][COLS], char mine[ROWS][COLS]) {int i = 0;int j = 0;for (i = 0; i < ROWS; i++){for (j = 0; j < COLS; j++){show[i][j] =' ';}}for (i = 0; i < ROWS; i++){for (j = 0; j < COLS; j++){mine[i][j] = '0';}} }
对于展示棋盘,由图可看出,初始化为空格 ' ' 表示最为恰当,而对于雷盘,为方便初始化为字符 '0'
🐷2.2 棋盘打印 show_board
对于,棋盘初始化完成,为方便操作,在展示棋盘的同时,添加标识行列,方便玩家操作,此时初始化为 11 的 show 棋盘,就可以展示其第一个作用,便于打印附上坐标。
void show_board(char show[ROWS][COLS]) {for (int i = 0; i <= ROW; ++i){for (int j = 0; j <= COL; ++j){if (j == 0){printf(" %d |", i);}else if (i == 0){printf(" %d |", j);}else{printf(" %c ", show[i][j]);if (j <= COL){printf("|");}}}printf("\\n");if (i < ROW){int j = 0;for (j = 0; j <= COL; j++){printf("---");if (j <= COL)printf("|");}printf("\\n");}}printf("\\n");}
初始化棋盘完成,打印棋盘完成,此时便可以通过MineSweepertest.c进行测试,观察棋盘是否符合扫雷游戏棋盘
🐷2.3 布置雷 set_mine
为了便于修改游戏难度,设置雷数,因此我们使用#define在头文件MineSweeper.h——头文件定义全局标识符常量,用于初始化雷数
#define EASY_COUNT 10//雷数
注意事项:
- 布置雷首先需要生成随机的x ,y 两个坐标,而调用伪随机 rand 库函数需要配合 srand 根据 time 函数返回的时间戳生成,因此 srand 只需要调用一次即可。
- 注意控制随机数生成坐标的范围,同时判断此位置是否重复生成
void set_mine(char mine[ROWS][COLS]) {int count = EASY_COUNT;int x = 0;int y = 0;while (count){x = rand() % ROW + 1;y = rand() % COL + 1;if (mine[x][y] == '0'){mine[x][y] = '*';count--;}} }
🐷2.4 排查雷及判断输赢 fine_mine
- 排查雷,需要获取坐标,检查坐标合法性,同时判断此坐标是否已经被排查
- 给出输赢计数器,通过判断所排查位置个数减去雷的个数,判断游戏是否结束
- 如果排查到雷,便游戏结束,同时展示雷盘,得知为何输
- 如果未排查到雷,会根据规则进行展开,同时给出每个位置周围雷的个数
- 排查位置之后,根据玩家输入判断是否需要标记
void fine_mine(char mine[ROWS][COLS], char show[ROWS][COLS]) {int x = 0;int y = 0;int win = 0; // 判段输赢计数器int* pw = &win;char ch = 0; //接收是否需要标记while (win < ROW * COL - EASY_COUNT){printf("请输入要排查的坐标:>");scanf("%d %d", &x, &y);if (x >= 1 && x <= ROW && y >= 1 && y <= COL){if (mine[x][y] == '*'){printf("很遗憾,被炸死了\\n");show_board(mine); //炸死后,展示雷盘,的知为何死break;}else{if (show[x][y] == ' ' ||(show[x][y] == '!'))//判断是否重复排查{explosion_spread(mine, show, x, y, pw);//展开一片system("cls");// 清屏show_board(show);// 打印展示雷盘printf("需要标记雷的位置请输入y/Y,否则请按任意键->");while ((ch = getchar()) != '\\n'); //清理缓冲区scanf("%c", &ch);if (ch == 'Y' || ch == 'y'){mark_mine(show); //标记雷system("cls");// 清屏show_board(show);// 打印展示雷盘}else{continue;}}else {printf("该坐标已被排查,请重新输入!\\n");continue; //直接进入下一次循环}}}else{printf("坐标非法,重新输入\\n");}}if (win == ROW * COL - EASY_COUNT){printf("恭喜你,排雷成功\\n");show_board(mine);} }
🐷2.5 按规则展开static void explosion_spread
根据输入位置判断周围雷的个数,个数为0,进行展开,再次判断周围雷的个数,个数为0,进行展开...
可知通过周围八个位置坐标,进行八路递归,控制递归条件,同时避免重复坐标判断,便可以按规则进行展开
注意:
每次递归一个位置,便会进行展开,因此需要控制输赢计数器,每次递归一层,进行返回,计数器便增加一
static void explosion_spread(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y, int* pw) {if (x >= 1 && x <= ROW && y >= 1 && y <= COL) //判断坐标是否为排查范围内{int num = get_mine_count(mine, x, y); //获取坐标周围雷的个数if (num == 0){(*pw)++;show[x][y] = '-'; //如果该坐标周围没有雷,就把该坐标置,并向周围八个坐标展开int i = 0;int j = 0;for (i = x - 1; i <= x + 1; i++){for (j = y - 1; j <= y + 1; j++){if (show[i][j] == ' ') //限制递归条件,防止已经排查过的坐标再次递归,从而造成死递归explosion_spread(mine, show, i, j, pw);}}}else{(*pw)++;show[x][y] = num + '0';}} }
🐷2.6 获取周围雷的个数get_mine_count
static int get_mine_count(char mine[ROWS][COLS], int x, int y) {int count = 0;for (int i = 0; i < 3; ++i){for (int j = 0; j < 3; ++j){if(mine[x - 1 + i][y - 1 + j] == '*')count++;}}return count;}
🐷2.7 标记雷mark_mine
由图可知,可以通过标价,便于进行游戏,而此时通过判断此位置是否被排查,进行标记,通过'!'字符进行标记,如果取消标记,直接输入排查位置即可
void mark_mine(char board[ROWS][COLS]) {int x = 0;int y = 0;while (1){printf("请输入你想要标记位置的坐标->");scanf("%d %d", &x, &y);if (x >= 1 && x <= ROW && y >= 1 && y <= COL) //判断该坐标是否合法{if (board[x][y] == ' ') //判断该坐标是否被排查{board[x][y] = '!';break;}else{printf("该位置不能被标记,请重新输入!\\n");}}else{printf("输入错误,请重新输入!\\n");}} }
🚀3. 游戏完整代码:
🛰️MineSweeper.h——头文件
#pragma once #include<stdio.h> #include<stdlib.h> #include<time.h> #include<stdbool.h> #define ROW 9 #define COL 9#define ROWS (ROW+2) #define COLS (COL+2) #define EASY_COUNT 10//雷数 //初始化 void init_Board(char show[ROWS][COLS], char mine[ROWS][COLS]); //打印雷盘 void show_board(char show[ROWS][COLS]);//布置雷 void set_mine(char mine[ROWS][COLS]); //排查雷 void fine_mine(char mine[ROWS][COLS], char show[ROWS][COLS]);//标记雷 void mark_mine(char board[ROWS][COLS]);
🛰️MineSweeper.c——源文件
#include"MineSweeper.h"void init_Board(char show[ROWS][COLS], char mine[ROWS][COLS]) {int i = 0;int j = 0;for (i = 0; i < ROWS; i++){for (j = 0; j < COLS; j++){show[i][j] =' ';}}for (i = 0; i < ROWS; i++){for (j = 0; j < COLS; j++){mine[i][j] = '0';}} }void show_board(char show[ROWS][COLS]) {for (int i = 0; i <= ROW; ++i){for (int j = 0; j <= COL; ++j){if (j == 0){printf(" %d |", i);}else if (i == 0){printf(" %d |", j);}else{printf(" %c ", show[i][j]);if (j <= COL){printf("|");}}}printf("\\n");if (i < ROW){int j = 0;for (j = 0; j <= COL; j++){printf("---");if (j <= COL)printf("|");}printf("\\n");}}printf("\\n");}void set_mine(char mine[ROWS][COLS]) {int count = EASY_COUNT;int x = 0;int y = 0;while (count){x = rand() % ROW + 1;y = rand() % COL + 1;if (mine[x][y] == '0'){mine[x][y] = '*';count--;}} } static int get_mine_count(char mine[ROWS][COLS], int x, int y) {int count = 0;for (int i = 0; i < 3; ++i){for (int j = 0; j < 3; ++j){if(mine[x - 1 + i][y - 1 + j] == '*')count++;}}return count;} static void explosion_spread(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y, int* pw) {if (x >= 1 && x <= ROW && y >= 1 && y <= COL) //判断坐标是否为排查范围内{int num = get_mine_count(mine, x, y); //获取坐标周围雷的个数if (num == 0){(*pw)++;show[x][y] = '-'; //如果该坐标周围没有雷,就把该坐标置,并向周围八个坐标展开int i = 0;int j = 0;for (i = x - 1; i <= x + 1; i++){for (j = y - 1; j <= y + 1; j++){if (show[i][j] == ' ') //限制递归条件,防止已经排查过的坐标再次递归,从而造成死递归explosion_spread(mine, show, i, j, pw);}}}else{(*pw)++;show[x][y] = num + '0';}} } void mark_mine(char board[ROWS][COLS]) {int x = 0;int y = 0;while (1){printf("请输入你想要标记位置的坐标->");scanf("%d %d", &x, &y);if (x >= 1 && x <= ROW && y >= 1 && y <= COL) //判断该坐标是否合法{if (board[x][y] == ' ') //判断该坐标是否被排查{board[x][y] = '!';break;}else{printf("该位置不能被标记,请重新输入!\\n");}}else{printf("输入错误,请重新输入!\\n");}} }void fine_mine(char mine[ROWS][COLS], char show[ROWS][COLS]) {int x = 0;int y = 0;int win = 0; // 判段输赢计数器int* pw = &win;char ch = 0; //接收是否需要标记while (win < ROW * COL - EASY_COUNT){printf("请输入要排查的坐标:>");scanf("%d %d", &x, &y);if (x >= 1 && x <= ROW && y >= 1 && y <= COL){if (mine[x][y] == '*'){printf("很遗憾,被炸死了\\n");show_board(mine); //炸死后,展示雷盘,的知为何死break;}else{if (show[x][y] == ' ' ||(show[x][y] == '!'))//判断是否重复排查{explosion_spread(mine, show, x, y, pw);//展开一片system("cls");// 清屏show_board(show);// 打印展示雷盘printf("需要标记雷的位置请输入y/Y,否则请按任意键->");while ((ch = getchar()) != '\\n'); //清理缓冲区scanf("%c", &ch);if (ch == 'Y' || ch == 'y'){mark_mine(show); //标记雷system("cls");// 清屏show_board(show);// 打印展示雷盘}else{continue;}}else {printf("该坐标已被排查,请重新输入!\\n");continue; //直接进入下一次循环}}}else{printf("坐标非法,重新输入\\n");}}if (win == ROW * COL - EASY_COUNT){printf("恭喜你,排雷成功\\n");show_board(mine);} }
🛰️MineSweepertest.c
#include"MineSweeper.h" void game() {char show[ROWS][COLS];//展示棋盘char mine[ROWS][COLS];//雷盘init_Board(show, mine);set_mine(mine);show_board(show);fine_mine(mine, show); } void menu() {printf("##\\n");printf(" 1.play \\n");printf(" 0.exit \\n");printf("##\\n");printf("--------------------\\n"); } void test() {srand((unsigned int)time(NULL));do{menu();printf("请选择:>");int input = 0;if (~scanf("%d", &input)){switch (input){case 1:game(); break;case 0:exit(0);default:printf("输入有误\\n"); break;}}else{printf("输入有误\\n");}} while (1); } int main() {test();return 0; }