文章目录
前言
扫雷游戏需要玩家运用逻辑推理和数学计算来推断哪些格子可能有地雷,哪些格子是安全的。通过不断尝试和推理,最终完成游戏。总的来说扫雷游戏是一款能够锻炼玩家多种能力的益智游戏,对提高玩家的逻辑推理、数学计算、观察、耐心和毅力、空间想象力等能力都有帮助。
对于学习编程的我们,肯定要和普通玩家有所区别!玩游戏,就要从“零开始”玩!!!下面带大家从代码开始,玩一款不一样的扫雷游戏。
一、扫雷游戏的玩法
扫雷游戏的起源可以追溯到20世纪60年代和70年代的大型机游戏。然而,第一款被广泛认知的扫雷游戏是在1989年由Robert Donner开发的,他当时已经是微软的一员。Donner为了提升自己的编程技能,创作了这个名为《扫雷》(Minesweeper)的游戏,并将其加入到了微软即将发布的Windows Entertainment Pack中。这款游戏因其趣味性成功入选,并且后来成为Windows系统中的一个经典组件。
扫雷游戏规则:
- 棋盘上的有些格子里有地雷,有格子没有地雷。
- 游戏开始后,排查过的棋盘上会有一些数字,这些数字表示以该数字为中心的周围 8 个格子中地雷的数量。
- 排查格子,如果格子是空白的,则会打开该格子;如果格子中有地雷,则游戏结束。
- 如果排查的格子周围没有地雷,则会将周围没有地雷的格子一并打开,直到遇到有地雷的格子。
- 游戏的目标是在不踩到地雷的情况下,排查出所有没有地雷的格子。
二、成品演示
扫雷游戏
三、扫雷游戏的制作
游戏设计思路:
第一步:我们需要创建一个菜单界面函数,用于选择开始游戏或退出游戏。
第二步:布置雷场,打印棋盘。
第三步:键盘输入坐标,排查地雷,然后反馈结果,踩雷则游戏结束,否则游戏继续进行。
游戏分模块化实现
1、头文件game.h
把全部要用到的头文件、数据类型、函数声明等等放在一个文件上,分类清晰,减少冗余代码。比如棋盘的大小为9x9,我们可以用宏定义把ROW定义为9,COL定义为9,这样后面每个函数在传值时就直接用ROW和COL来代替,如后期想提高难度,把棋盘改为16x16的,我们在只需改动ROW和COL的值就行了,不需要再回去把每个函数的传值再去改一遍。
#define ROW 9
#define COL 9
#define MINES 10
#define ROWS ROW+2
#define COLS COL+2
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#include<Windows.h>
#include<string.h>
//注要功能函数定义
//菜单
void menu();
//进入游戏
void game();
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col);
void Display(char board[ROWS][COLS], int row, int col);
//布置雷
void SetMine(char board[ROWS][COLS], int row, int col);
//排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
//统计雷个数
int GetMineCount(char mine[ROWS][COLS], int x, int y);
//展开非雷
void Expand(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y);
//失败惩罚
void Punishment();
2、test.c文件
创建游戏逻辑测试函数test()
在test函数中,创建变量input,根据菜单来输入对于的选择,用do while循环来实现开始游戏或退出游戏。程序进入do while循环后,先调用Preface函数去打印游戏的规则。再调用menu函数打印一个游戏菜单,进行选择开始游戏或推出游戏。然后是用switch来判断玩家的选择,输入 1,则进入游戏,输入 0,则退出游戏。最后让main函数调用test函数。
void test()
{
int input = 0;
srand((unsigned int)time(NULL));//使⽤time函数的返回值设置种⼦
//因为srand的参数是unsigned int类型,我们将time函数的返回值强制类型转换
do
{
menu();//打印菜单
printf("请选择(1/0):>");
scanf("%d", &input);
switch (input)
{
case 1:
system("cls");//清屏
game();//进入游戏
break;
case 0:
printf("已退出游戏!");
break;
default:
printf("输入错误,请重新选择!");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}
3、game.c文件
①、创建菜单函数
一开始运行程序时,先打印出一个菜单显示游戏名和游戏规则,能让玩家更容易上手游戏。
但是我发现直接用printf打印,有点没意思。我的想法是要一个字一个字地打印出这句话,所以我使用了Sleep函数,Sleep函数注要功能是使程序暂停一段时间,比如Sleep(1000)就是让程序暂停一秒,要注意需要引入Windows.h头文件。
把游戏玩法存到数组str1中,再用循环和Sleep函数就可以实现一个字一个字地打印句子了。
效果图
:
代码:
void menu()
{
int i = 0;
char str1[] = {"扫雷游戏玩法:\n1.一开始,所有方格都是#号\n2.玩家输入坐标来开始游戏\n3.输入坐标后,方格上会显示数字\n4.数字表示周围八个方格中有几个地雷\n5.如排到地雷,则游戏就结束!\n请开始您的游戏!\n"};
for (i = 0; i < strlen(str1); i++) {
Sleep(5);
printf("%c", str1[i]);
}
printf("\n");
printf(" 1.play\n\n");
printf(" 0.exit\n\n");
}
②、棋盘的整体布置
首先我们要在控制台上显示一个9x9的棋盘,然后把棋盘分成81个小方格,在这些小方格里面随机分布着10个地雷,还有一些记录地雷信息的数字。
在扫雷过程中,布置的雷和排查出的雷的信息都需要存储,看到是9x9的棋盘,我们首先能想到的是创建一个9x9的二位数组来存放这些信息。另外,为了使布置雷的信息和排查出雷的信息互不干扰,可以创建一个mine数组来存放布置好的雷的信息,再创建一个新的show数组来存放排查出的雷的信息。
对于mine数组和show数组:
- mine数组是存放布置地雷信息的,先把mine数组初始化为‘0’,代表此时棋盘还没有布置地雷,当布置为雷时,就设计为字符‘*’,方便区分。
- 因为刚开始游戏时是看不到地雷的,所以我们可以把show数组先全部初始化为‘#’。此外根据游戏规则,在棋盘上排雷时还需要显示出周围的地雷的个数,具体是以排查位置为中心的外一圈,也就是周围八个方格的位置的地雷信息。看到这里,聪明的你是否也发现了有一个bug,那就是最外一层格子的排查存在问题。根据前面的分析,整个棋盘我们是使用数组来存储的,既然是数组,我们就要注意是否出现数组越界访问。当我们去排查最外一层方格时,还需要同时让程序去检测周围八个方格位置的地雷信息的,这个时候就出现了有三个方格位置是越界访问的。要解决这个问题,方法有很多种,我们可以给最外一层格子设置一些限定条件防止出现越界访问,但是我觉得太过麻烦了,我认为最简单的方法,就是让棋盘四周再增加一层方格。原本是9x9的棋盘,我们可以设置成11x11的,把地雷布置在里面的9x9的方格里面,把新增的最外层方格不去布置地雷,排查地雷也只能排查里面9x9的方格,这样就能轻松解决了数组越界的问题了。
③、初始化mine数组和show数组
注意这里数组是ROWS和COLS,即11x11的大小,虽然我们是想要9x9大小的棋盘,但为了防止数组越界访问,只能把数组扩大为11x11大小的
//存放布置的地雷
char mine[ROWS][COLS] = { 0 };
//展示排查后周围雷的信息
char show[ROWS][COLS] = { 0 };
初始化函数的目的是先把mine数组初始化为字符‘0’,把show数组初始化为字符‘#’。
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;//set在mine数组中为字符‘0’,在show数组中为字符‘#’
}
}
}
④、画一个9x9的方格棋盘
一款好的游戏,肯定是要和玩家有交互性,让玩家看到“实物”。我们这款小游戏是扫雷游戏,那就得需要让玩家看到一个9x9的棋盘吧。因此,我们可以用一些字符在控制台上画一个棋盘,可以让玩家更加直观得去玩游戏。
那应该怎样去画这个棋盘呢?
对于这个问题,每个人都有各自的想法。有人喜欢简约美,那就可以画一个简单点的棋盘;也有人喜欢高度复刻原版游戏的棋盘,那就需要花多些时间和耐心去编写。
下面我介绍一下我的棋盘是怎样画的。
效果图
:
首先我们需要从上往下去画这个棋盘。
上面的这个“- + - + - + - + - + - + - + - 扫雷游戏 - + - + - + - + - + - + - + -”,我们卡哇伊直接使用printf直接打印,也可以使用循环去打印。使用循环去打印的好处是,如果我们想提高一下游戏难度,把棋盘改成16x16的大小,那么这个抬头也可以完美地对齐棋盘的大小。
//printf("-+-+-+-+-+-+-+- 扫雷游戏 -+-+-+-+-+-+-+-\n");
for (i = 1; i < col; i++)
{
printf("-+");
}
printf(" 扫雷游戏 ");
for (i = 1; i < col; i++)
{
printf("-+");
}
然后再把第一行的列标打印出来,方便玩家快速寻找位置。
printf("\n ");
for (i = 1; i <= col; i++)
{
printf("%3d ", i);
}
printf("\n");
接下来就是网格棋盘的制作,用字符’- - - +‘和字符‘|’把方格画出来,同时把初始化为字符’#'的show数组也打印出来。
printf(" +");
for (i = 1; i <= col; i++)
{
printf("---+");
}
printf("\n");
//--------------------------------------------
for (i = 1; i <= row; i++)
{
printf("%2d", i);
printf("|");
for (j = 1; j <= col; j++)
{
printf(" %c |", board[i][j]);
}
printf("\n");
printf(" +");
for (j = 0; j < col; j++)
{
printf("---+");
}
printf("\n");
}
}
下面是整体的代码
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
//printf("-+-+-+-+-+-+-+- 扫雷游戏 -+-+-+-+-+-+-+-\n");
for (i = 1; i < col; i++)
{
printf("-+");
}
printf(" 扫雷游戏 ");
for (i = 1; i < col; i++)
{
printf("-+");
}
//--------------------------------------------------------------------------
printf("\n ");
for (i = 1; i <= col; i++)
{
printf("%3d ", i);
}
printf("\n");
//--------------------------------------------------------------------------
printf(" +");
for (i = 1; i <= col; i++)
{
printf("---+");
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%2d", i);
printf("|");
for (j = 1; j <= col; j++)
{
printf(" %c |", board[i][j]);
}
printf("\n");
printf(" +");
for (j = 0; j < col; j++)
{
printf("---+");
}
printf("\n");
}
}
⑤、雷场分布图
Display函数是当玩家踩雷后,我们可以简单地把雷场的分布图打印出来,以便玩家知道自己踩到雷了。
效果图
;
代码:
void Display(char board[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
for (int 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++)
{
if (board[i][j] == '*')
printf("%c ", board[i][j]);
else
printf(" ");
}
printf("\n");
}
}
⑥、布置雷
如果我们玩过网页版的扫雷就会发现,里面的地雷并不是我们玩家自己布置的,因此就要考虑如何去实现随机布置地雷。
C语言提供了一个函数叫rand,这个函数是可以生成随机数的。
int rand(void);
注意rand函数的使用需要包含头文件:stdlib.h。
但是单靠rand函数并不能完全解决问题,经过测试发现,rand函数生成的随机数是伪随机数。伪随机数并不是真正的随机数,而是通过某种算法生成的。经过查阅资料发现,rand函数是对一个叫“种子”的基准值进行运算生成随机数的,只要这个种子不变,那么它生成的随机数就不会改变,这就达不到随机的效果了。
C语言中又提供了一个函数叫srand,它是用来初始化随机数的生成器的。
void srand(unsigned int seed);
程序在调用rand函数之前先调用srand函数,通过srand函数的参数seet来设置rand函数生成随机数的时候的种子,只要种子在变化,每次生成的随机数序列也就变化起来了。
在程序中我们一般是使用程序运行的时间作为种子,因为时间时刻在发生变化。我们使用time函数去获取程序运行当前的时间戳来设置这个种子。
srand((unsigned int)time(NULL));//使⽤time函数的返回值设置种⼦
//因为srand的参数是unsigned int类型,我们将time函数的返回值强制类型转换
有了以上的铺垫,我们就可以去实现随机布置地雷的函数了。
用rand() % 9,就能等到取值范围为0~8的随机数,因此要进行+1操作。最后得到的坐标x,y就是布置好的地雷位置,我们只需要随机布置10个地雷,因此需要用count记录一下,布置好一个雷,count- -。
int x = rand() % row + 1;
int y = rand() % col + 1;
void SetMine(char board[ROWS][COLS], int row, int col)
{
int count = MINES;//count是还需要布置的地雷个数
while (count)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (board[x][y] == '0')
{
board[x][y] = '*';
count--;
}
}
}
⑦、统计排查周围的地雷个数
当我们输入要排查的坐标(x,y)后,同时要统计出围绕坐标(x,y)周围的八个位置的地雷情况,然后把统计的结果存放到(x,y)位置上,以便提示玩家进行排查判断。统计方法也很简单,那就是直接遍历x-1->x+1和y-1->y+1围成的正方形区域,如果为地雷,则count++,不用担心(x,y)位置,因为它为雷的话你就被炸死了。
int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
int i = 0;
int j = 0;
int count = 0;
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
if (mine[i][j] == '*')
{
count++;
}
}
}
return count;
}
⑧、爆炸式展开无雷区域
根据网页版游戏,我们发现当排查位置没有地雷时,它是会把没有地雷的位置全部展开并置空,直到遇到有地雷的区域才停下。
实现的思路也不难,就是采用递归的思想。同时也要防止出现死递归,因为我们发现会要重复递归的部分,这样会造成程序一直永无止境的重复递归,因此我们要加入限制条件if(show[i][j])=='#')
时,才进行递归。
剩余的‘#’代表可能为地雷的位置,根据数字来判断。
void Expand(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
if (GetMineCount(mine, x, y) == 0)//(x,y)周围没有地雷
{
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] == '#')
{
Expand(mine, show, row, col, i, j);//递归展开
}
}
}
}
else
{
show[x][y] = GetMineCount(mine, x, y) + '0';//周围有地雷,则统计完后把结果放到(x,y)位置上
}
}
⑨、开始排查地雷
排查地雷函数的实现思路:玩家输入排查位置坐标,程序反馈结果,如果是地雷,则游戏结束,打印提示信息并把雷场分布打印出来,之后进行关机整蛊惩罚程序;如果排查坐标没有地雷,则调用Expand函数把周围无雷的位置展开,有雷的区域统计地雷个数。因为要排查多次,所以要使用循环,而循环退出的条件是当把无雷的位置全部排查后,循环退出并赢取游戏胜利。
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 - MINES)//win < 9x9的方格数-有地雷的方格
{
printf("请输入要排查的坐标:");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col && show[x][y] == '#')
{
if (mine[x][y] == '*')//踩到地雷
{
printf("很遗憾,你被炸死了!\n");
printf("雷场分布如下(*):\n");
Display(mine, ROW, COL);
system("pause");
Punishment();
system("cls");
break;
}
else//该坐标不是雷,就统计坐标周围的雷的个数
{
system("cls");
win++;
Expand(mine, show, row, col, x, y);//无雷展开
DisplayBoard(show, row, col);//更新棋盘
}
}
else
{
printf("坐标非法,请重新选择!\n");
}
}
if (win == row * col - MINES)//胜利条件
{
printf("恭喜你,排雷成功!\n");
printf("雷场分布如下:\n");
Display(mine, ROW, COL);
}
}
⑩、踩雷后的关机小惩罚
为了增加一点游戏刺激,或者整蛊同学,我们可以把前的关机小程序加上,在玩家踩到雷后就直接把他的电脑强行关机。当然这算是比较狠的了,我们还是留一点挽回的余地吧,把关机程序设计为60秒,或者更短时间,设计一些有趣的问题去整蛊玩家。
详细介绍请看之前的博客《使用goto语句和Linux命令实现关机整蛊小程序》
void Punishment()
{
int x = 0;
system("shutdown -s -t 60");//使用命令关机
again:
printf("请注意!系统检测到你智商太低,将强制关机\n请回答问题增加一点智商\n请在一分钟内回答:1 + 1 = ?\n");
printf("回答正确,就能取消关机!!\n");
printf("请输入:>");
scanf("%d", &x);
if (x == 2)
{
system("shutdown -a");
}
else
{
goto again;
}
}
最后
程序源码
game.h文件:
#define ROW 9
#define COL 9
#define MINES 10
#define ROWS ROW+2
#define COLS COL+2
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#include<Windows.h>
#include<string.h>
//菜单
void menu();
//进入游戏
void game();
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col);
void Display(char board[ROWS][COLS], int row, int col);
//布置雷
void SetMine(char board[ROWS][COLS], int row, int col);
//排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
//统计雷个数
int GetMineCount(char mine[ROWS][COLS], int x, int y);
//展开非雷
void Expand(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y);
//失败惩罚
void Punishment();
game.c文件:
#include"game.h"
void Preface()
{
int i = 0;
char str1[] = {"扫雷游戏玩法:\n1.一开始,所有方格都是#号\n2.玩家输入坐标来开始游戏\n3.输入坐标后,方格上会显示数字\n4.数字表示周围八个方格中有几个地雷\n5.如排到地雷,则游戏就结束!\n请开始您的游戏!\n"};
for (i = 0; i < strlen(str1); i++) {
Sleep(5);
printf("%c", str1[i]);
}
printf("\n");
}
void menu()
{
printf(" 1.play\n\n");
printf(" 0.exit\n\n");
}
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 DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
//printf("-+-+-+-+-+-+-+- 扫雷游戏 -+-+-+-+-+-+-+-\n");
for (i = 1; i < col; i++)
{
printf("-+");
}
printf(" 扫雷游戏 ");
for (i = 1; i < col; i++)
{
printf("-+");
}
printf("\n ");
for (i = 1; i <= col; i++)
{
printf("%3d ", i);
}
printf("\n");
printf(" +");
for (i = 1; i <= col; i++)
{
printf("---+");
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%2d", i);
printf("|");
for (j = 1; j <= col; j++)
{
printf(" %c |", board[i][j]);
}
printf("\n");
printf(" +");
for (j = 0; j < col; j++)
{
printf("---+");
}
printf("\n");
}
}
void Display(char board[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
for (int 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++)
{
if (board[i][j] == '*')
printf("%c ", board[i][j]);
else
printf(" ");
}
printf("\n");
}
}
void SetMine(char board[ROWS][COLS], int row, int col)
{
int count = MINES;
while (count)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (board[x][y] == '0')
{
board[x][y] = '*';
count--;
}
}
}
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 - MINES)
{
printf("请输入要排查的坐标:");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col && show[x][y] == '#')
{
if (mine[x][y] == '*')
{
printf("很遗憾,你被炸死了!\n");
printf("雷场分布如下(*):\n");
Display(mine, ROW, COL);
system("pause");
Punishment();
system("cls");
break;
}
else
{
//该坐标不是雷,就统计坐标周围的雷的个数
system("cls");
win++;
Expand(mine, show, row, col, x, y);
DisplayBoard(show, row, col);
}
}
else
{
printf("坐标非法,请重新选择!\n");
}
}
if (win == row * col - MINES)
{
printf("恭喜你,排雷成功!\n");
printf("雷场分布如下:\n");
Display(mine, ROW, COL);
}
}
int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
int i = 0;
int j = 0;
int count = 0;
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
if (mine[i][j] == '*')
{
count++;
}
}
}
return count;
}
void Expand(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
if (GetMineCount(mine, x, y) == 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] == '#')
{
Expand(mine, show, row, col, i, j);
}
}
}
}
else
{
show[x][y] = GetMineCount(mine, x, y) + '0';
}
}
void Punishment()
{
int x = 0;
system("shutdown -s -t 30");//使用命令关机
again:
printf("请注意!系统检测到你智商太低,将强制关机\n请回答问题增加一点智商\n请在30秒内回答:1 + 1 = ?\n");
printf("回答正确,就能取消关机!!\n");
printf("请输入:>");
scanf("%d", &x);
if (x == 2)
{
system("shutdown -a");
}
else
{
goto again;
}
}
test.c文件:
#include"game.h"
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);
//DisplayBoard(mine, ROW, COL);
//排查雷
FindMine(mine,show,ROW,COL);
}
void test()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
Preface();
menu();
printf("请选择(1/0):>");
scanf("%d", &input);
switch (input)
{
case 1:
system("cls");
game();
break;
case 0:
printf("已退出游戏!");
break;
default:
printf("输入错误,请重新选择!");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}
也可以去我的👉gitee仓库👈👀免费查看或下载源码。