目录
前言
一、分模块化
二、准备雷盘
2.1 游戏菜单
2.2 创建雷盘思路
2.3 构建雷盘
2.4 雷盘展示
2.4.1 初始化雷盘
2.4.2 打印雷盘
三、排雷
3.1 布置雷
3.2 排查雷
四、进阶版扫雷
总结
前言
C语言实现扫雷小游戏,帮我们更进一步的掌握数组、模块化思想等知识。
一、分模块化
对于扫雷小游戏,相信老铁们应该不陌生,根据信息进行排雷,大家可以参考网页版的扫雷游戏:扫雷小游戏
对于扫雷小游戏,虽然是一个小项目,代码量不多,但对于初学者来说,重要的是学习如何分模块开发项目。
本文将该游戏分成三个文件:
- test.c游戏测试文件
- game.c游戏执行逻辑(函数实现)
- game.h游戏声明(函数声明,头文件)
二、准备雷盘
扫雷游戏,首先需要一个雷盘,本文讲解的雷盘为9×9规格。
2.1 游戏菜单
一个游戏,最先呈现给用户看的是游戏菜单,对于游戏菜单,可以用do...while循环 + switch选择语句完成,因为无论用户是否开始还是退出游戏,程序都会运行一次。
test.c文件
#include "game.h"
void menu()
{printf("********************\n");printf("***** 1、play ******\n");printf("***** 0、exit ******\n");printf("********************\n");
}
int main()
{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;
}game.h文件
#pragma once
#include <stdio.h>
2.2 创建雷盘思路
对于雷盘,使用二维数组存储数据是最合适的,符合行列。那一个雷盘就可以了吗?
我们规定:雷用字符1表示,非雷用字符0表示。
因此冲突点就出现了,如果对某个位置进行排雷,统计雷数时,正好也为1,那究竟是雷还是雷数呢?
为此,这里用了一个很巧妙的方法,我们定义两个大小一致的二维数组,也就是两个雷盘,一个雷盘用于布置雷,一个雷盘用于存放统计后雷的个数。
因此,我们有以下规定:
- 布置雷的二维数组叫mine。
- 存放雷数的二维数组叫show。因为要展示给用户看选择的位置周围的雷数,因此叫show。
- mine雷盘中字符0为非雷,字符1为雷。
- show雷盘中字符*表示未排查,字符数字表示该位置已被排查。
前面说过,我们的雷盘是一个9×9的规格,那相对于的两个数组的大小也是9×9吗?
其实不是,应该为11×11,因为当我们对最上、最下、最左、最右中的位置进行排雷时,统计周围有几颗雷时,会造成数组越界的情况,因此我们定义多2行2列的数据,这部分位置不存放雷,将雷存放在9×9的雷盘中,多定义的2行2列只是为了防止数组越界。
2.3 构建雷盘
对于数组的大小,我们不能写死,应该用#define标识符常量来表示,方便以后想扩大雷盘。并且我们需要分别定义9×9和11×11的标识符,因为在布置雷、打印雷盘、排雷的功能中只需要用到9×9的区域,而在初始化雷盘时,需要用到11×11区域。
test.c文件
#include "game.h"
void menu()
{printf("********************\n");printf("***** 1、play ******\n");printf("***** 0、exit ******\n");printf("********************\n");
}
void game()
{//定义两个大小相同的二维数组//这里的数组大小最好用#define标识符表示,后续想改变就该#define就好了//用于布置雷,用字符数组的原因是规定字符0为非雷,字符1为雷char mine[ROWS][COLS] = { 0 };//用于存放排查雷的信息(主要给用户看的页面//用字符数组的原因是规定雷数用字符数字表示,其它位置用字符*表示char show[ROWS][COLS] = { 0 };}
int main()
{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;
}game.h文件
#include <stdio.h>
/*解释一下这里定义的标识符:该游戏的排雷范围为9×9。点击一处位置,那周围的8处位置就要判断有几个雷。当位置位于上下一行时或者左右一行时,那访问周围8处位置时,就会造成越界访问了,因此这里可以加多两行两列,目的是为了不越界访问这两行两列不放置雷,雷的范围只在9×9中
*/
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
2.4 雷盘展示
2.4.1 初始化雷盘
首先,我们要对六个雷盘进行初始化操作,mine雷盘一开始全为字符0,因为还没布置雷;show雷盘一开始全为字符*,因为还没开始排雷。
test.c文件
#include "game.h"
void game()
{//初始化两个棋盘//mine雷盘一开始全是字符0,因为还没布置雷InitializeMinefield(mine, ROWS, COLS, '0');//show雷盘一开始全是字符*,因为还没统计周围雷数InitializeMinefield(show, ROWS, COLS, '*');
}game.h文件
#pragma once
#include <stdio.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
//初始化雷盘
//这里的参数写什么?
//既然是初始化雷盘,那要有个数组来接收传来的mine数组和show数组
//初始化9×9还是11×11?11×11,这样方便后续计算雷的个数
//还有一个参数很重要,那就是set,既然用一个函数就初始化两个雷盘
//那就要将标识字符传过来,进行设置
void InitializeMinefield(char init[ROWS][COLS],int rows,int cols,char set);game.c文件
#include "game.h"
//初始化雷盘
void InitializeMinefield(char init[ROWS][COLS], int rows, int cols, char set)
{for (int i = 0; i < rows; i++){for (int j = 0; j < cols; j++){init[i][j] = set; //set为标识符号,mine雷盘传'0',show雷盘传'*'}}
}
初始化两个雷盘,要为每个雷盘分别写一个函数吗?
不需要,mine雷盘初始化时将字符0当成参数;show雷盘初始化时将字符*当成参数即可。
2.4.2 打印雷盘
打印雷盘的注意点在于打印多大的雷盘:是9×9,还是11×11?
前面说过,其实我们真正的雷盘大小为9×9的,11×11是为了让数组不越界。
test.c文件
#include "game.h"
void game()
{//打印雷盘,给用户展示的是show雷盘//打印多大?9×9,因为这是真正排雷区域PrintMindefield(show, ROW, COL);
}game.h文件
#pragma once
#include <stdio.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2//打印雷盘
//这里需要注意,接收数组的大小需要写11×11,因为定义数组的时候就是11×11
//打印时只打印9×9,但在语法上来说,数组的大小为11×11。
void PrintMindefield(char print[ROWS][COLS],int row,int col);game.c文件
#include "game.h"
//打印雷盘
void PrintMindefield(char print[ROWS][COLS], int row, int col)
{printf("-------扫雷--------\n");//给雷盘编号,这样用户更快找到坐标进行排雷//上边编号for (int i = 0; i <= col; i++){printf("%d ", i);}printf("\n");//初始值从下标1开始,最后下标row/col//因为数组的大小为11×11,只打印9×9时,就不能从0开始了,结尾也同理for (int i = 1; i <= row; i++){printf("%d ", i); //左边编号for (int j = 1; j <= col; j++){printf("%c ", print[i][j]);}//每打印一行就\nprintf("\n");}
}
虽然我们只打印9×9的雷盘,但是定义数组时的大小为11×11,在函数形参接收时,数组中的[][]应为11×11,但传过去的行和列为9、9。
雷盘准备完毕,我们看看效果:
三、排雷
3.1 布置雷
布置雷的思路很简单,我们在mine数组上,生成随机坐标,需要注意的如下:
- 该坐标位置没布置过雷(坐标内容为字符0)。
- 生成随机数的函数,srand随机数生成器和rand函数搭配使用。
- #define标识符定义雷的个数。
test.c文件
#include "game.h"
void game()
{//布置雷//在mine数组中布置//#define标识符定义雷数//同样,只需要在row × col中布置Place_mine(mine, ROW, COL);
}game.h文件
#pragma once
#include <stdio.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
//雷的总个数
#define Mine_Sum 10
//布置雷
void Place_mine(char mine[ROWS][COLS], int row, int col);game.c文件
#include "game.h"
//布置雷
//随机布置,随机生成坐标,使用rand函数前要设置随机生成器srand
/*条件:坐标不是雷,即不是'1'
*/
void Place_mine(char mine[ROWS][COLS], int row, int col)
{int x = 0;int y = 0;int count = Mine_Sum;//总雷数,当count为0时,表示已布置好雷while (count){x = rand() % row + 1; y = rand() % col + 1; if (mine[x][y] == '0'){mine[x][y] = '1';count--;}}}
3.2 排查雷
对于用户操作方面:
- 用户通过坐标进行排雷。
- 输入的坐标有边界条件:x>=1 && x<=row && y>=1 && y<=col
- 输入的坐标不能重复
什么时候结束呢?两种情况:
- 被炸死
- 排雷成功:排雷次数<row*col - 雷数
如何统计周围雷数:
- 周围坐标之和 - 8*字符0。
- 因为是mine数组中只有字符0或字符1.
- 字符数字转为数字:字符数字-字符0 = 数字。因为字符进行加减操作时,是以ascll码进行。
- 数字转为字符数字:数字+字符0 = 数字。
- 因此周围坐标之和再给每个坐标依次减字符0,那最终的结果就为周围雷的个数(数字)。
- 统计完成后,将个数转为字符个数,存储到对应位置的show雷盘中。
test.c文件
#include "game.h"
void game()
{//排雷(用户输入坐标排雷)/*排雷功能需要两个雷盘,mine雷盘用来检查周围雷的个数show雷盘用于存放统计好周围雷的个数,并展示*/Examine(mine, show, ROW, COL);
}game.h文件
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <stdlib.h>
#include <time.h>
#include <Windows.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
//雷的总个数
#define Mine_Sum 10
//排雷
void Examine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);game.c文件
#include "game.h"
//排雷
/*两个核心:1、通过用户坐标排雷2、检查周围坐标有几个雷后,放置到show雷盘中显示
*/
//统计该坐标周围的雷数
//此时的查找的雷盘范围为:11×11,防止越界
int StatisNumberMines(char mine[ROWS][COLS], int x, int y)
{/*mine雷盘中放的都是:字符0 或 字符1前备知识:字符数字转为数字: 字符数字 - 字符0 = 数字数字转字符数字:数字 + 字符0 = 字符数字原因:因为在进行字符间的加减操作时,是以ascll码进行的。现在mine雷盘中放的都是字符数字,那将8个位置的字符数字相加后: (8个位置之和) - 8*字符0再依次减字符0,那就能统计处周围有几个雷*/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';
}
void Examine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{int x = 0;int y = 0;/*坐标限制:1、不能重复2、不能超出有效范围:x>=1 && x<=row && y>=1 && y<=col*//*什么时候结束呢?两种情况:1、被炸死。2、排雷成功:排雷次数<row*col - 雷数*/int num = 0;while (num< row * col-Mine_Sum){printf("请玩家输入排雷坐标:>");scanf("%d %d", &x, &y);if (x >= 1 && x <= row && y >= 1 && y <= col){//输入坐标不能重复/*检查show雷盘,未排雷过的坐标为字符*,排查过的坐标字符0或字符数字*/if (show[x][y] == '*'){//判断是否是雷if (mine[x][y] == '0'){/*输入的坐标不是雷统计周围8个坐标有几颗雷*///在mine雷盘中统计int count = StatisNumberMines(mine, x, y);//统计好后放到show雷盘中//当然,要将数字转为字符数字,因为数组时char类型show[x][y] = count + '0';PrintMindefield(show, ROW, COL);num++;}//是雷就结束else{printf("您被炸死了\n");PrintMindefield(mine, ROW, COL);break;}}else{printf("该坐标已被排查\n");}}else{printf("输入的坐标超出范围\n");}}if (num == row*col - Mine_Sum){printf("恭喜你,排雷成功!\n");}
}
四、进阶版扫雷
简易版的扫雷有什么缺陷呢?
其实有很多功能都没有实现,比如即时、标记、剩余雷数等等,但最重要的是展开一片的功能没有实现,因此进阶版扫雷,将修改排雷函数,让用户输入坐标后,将不是周围一大片不是雷的地方展开,并提供更多雷数信息。
此时的扫雷,只能一个一个坐标的排查,并不会展开一片。
展开一大片的关键点:
- 该坐标不是雷
- 该坐标周围坐标没有雷
- 排查坐标没有被排查过。(为什么?因为当有一个坐标满足三个条件后,然后该坐标周围又有坐标满足条件,进行展开,那原先满足条件的坐标也算是它周围8个坐标,那还要判断它吗?它已经不是雷了,因此这个条件很重要,容易造成死递归)
//统计该坐标周围的雷数
//此时的查找的雷盘范围为:11×11,防止越界
int StatisNumberMines(char mine[ROWS][COLS], int x, int y)
{/*mine雷盘中放的都是:字符0 或 字符1前备知识:字符数字转为数字: 字符数字 - 字符0 = 数字数字转字符数字:数字 + 字符0 = 字符数字原因:因为在进行字符间的加减操作时,是以ascll码进行的。现在mine雷盘中放的都是字符数字,那将8个位置的字符数字相加后: (8个位置之和) - 8*字符0再依次减字符0,那就能统计处周围有几个雷*/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';
}
//排雷,展开一片
/*基本思路:1、递归,终止条件为周围坐标之和不为0,说明周围有雷2、如果为0,那将该坐标设为空字符,然后依次查看周围坐标的周围坐标是否为0,如果是那就设置为空但前提是查看的坐标没有被排查过,就是坐标里的值是'*',并且该坐标没超出范围
*/
void Unfold(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{int count = StatisNumberMines(mine, x, y);if (count == 0){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] == '*' && i > 0 && i < 10 && j>0 && j < 10){Unfold(mine, show, i, j);}}}}else{show[x][y] = count + '0';}
}
//查看show雷盘中还剩多少个位置没排查,当只剩Mine_Sum个时,排雷成功
int Win(char show[ROWS][COLS],int row,int col)
{int count = 0;for (int i = 0; i < row; i++){for (int j = 0; j < col; j++){if (show[i][j] == '*'){count++;}}}return count;}
void Examine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{int x = 0;int y = 0;/*坐标限制:1、不能重复2、不能超出有效范围:x>=1 && x<=row && y>=1 && y<=col*//*什么时候结束呢?两种情况:1、被炸死。2、排雷成功:排雷次数<row*col - 雷数*/int num = 0;while (num< row * col-Mine_Sum){printf("请玩家输入排雷坐标:>");scanf("%d %d", &x, &y);if (x >= 1 && x <= row && y >= 1 && y <= col){//输入坐标不能重复/*检查show雷盘,未排雷过的坐标为字符*,排查过的坐标字符0或字符数字*/if (show[x][y] == '*'){//判断是否是雷if (mine[x][y] == '0'){//进阶版----展开一片Unfold(mine, show, x, y);PrintMindefield(show, ROW, COL);}//是雷就结束else{printf("您被炸死了\n");PrintMindefield(mine, ROW, COL);break;}}else{printf("该坐标已被排查\n");}}else{printf("输入的坐标超出范围\n");}if (Win(show, row, col) == Mine_Sum){printf("恭喜你,排雷成功!\n");PrintMindefield(mine, ROW, COL);break;}}
}
使用递归实现,当周围坐标没有雷时,将该坐标设置为空字符,然后再依次对周围坐标判断是否被排查过并且不超过有效范围,如果满足,则递归,看该坐标的周围坐标是否符合没有雷条件。直到周围坐标有雷,递归就结束,开始返回。
总结
这就是扫雷小游戏,希望对您有所帮助,后续会出更多干货,多多支持,关注我❤❤❤!源代码自取。