写在前面:
由于在讲解本游戏前,笔者已经讲解过另外一个游戏“三子棋”了,因此本文在解释部分内容时比较简略,请初学者先复习三子棋的实现方法
三子棋游戏链接:https://blog.csdn.net/2302_80297338/article/details/136057596?spm=1001.2014.3001.5501
目录
一.扫雷游戏介绍
二.代码思路
三.开始菜单
四.游戏实现+输赢判断
五.全部代码
六.游戏优化
《扫雷》是一款大众类的益智小游戏,于1992年发行。游戏目标是在最短的时间内根据点击格子出现的数字找出所有非雷格子,同时避免踩雷,踩到一个雷即全盘皆输。游戏的基本操作包括左键单击(Left Click)、右键单击(Right Click)、双击(Chording)三种。其中左键用于打开安全的格子,推进游戏进度;右键用于标记地雷,以辅助判断,或为接下来的双击做准备;双击在一个数字周围的地雷标记完时,相当于对数字周围未打开的方块均进行一次左键单击操作。
首先创建三个文件,分别是:
test.c //测试游戏的逻辑
game.c //游戏代码的实现 game.h //游戏代码的声明(函数声明,符号定义)
上述内容详细介绍请看本文:函数基础
要在游戏中实现以下几个目标:
开始菜单➡游戏实现+输赢判断➡游戏优化(更多功能)
开始菜单已经在三子棋游戏中讲解过,此处省略。
本次游戏是要实现一个9*9大小的游戏平台,并藏有10个雷,并把‘*’作为未知位置
由于数组的下标从0开始,同时对于一些边缘位置的判断不同于非边缘位置,如下图
注:红底为边缘位置,黑横为游戏平台外的位置(即不需要判断和打印的位置)
因此需要在game.h文件定义四个宏
#define ROW 9
#define COL 9 //只是把数组创建成11*11大小,但是在游戏中只会玩9*9的大小(例如打印,坐标输入错误提示等……)
#define ROWS ROW+2
#define COLS COL+2 //为防止游玩时出现数组越界,将数组行列大小分别增大2
注:不同于上次三子棋的讲解,本次讲解即为代码附近的批注(下同)
游戏实现要分为三步:1.游戏平台的初始化和打印 2.设置雷 3.玩家开始游玩+输赢判断
1.游戏平台的初始化和打印:
在三个文件中分别声明、定义与使用,详细解释请看三子棋那篇文章,此处省略
假设我们把没有雷的地方赋‘0’,有雷的地方赋‘1’
因此代码如下:
void game()
{
char mine[ROWS][COLS] = { 0 };//存放布置好的雷
char show[ROWS][COLS] = { 0 };//存放排查出的雷的信息
//初始化需求:mine没有布置雷的时候,都是‘0’;show数组在没有在排除雷的时候都是‘*’
Init(mine, ROWS, COLS,'0');
Init(show, ROWS, COLS,'*');
//最后一个‘0’是为了传参以后让函数能够判断存入数组的是‘0’还是‘*’,再次调用时的‘*’也是为了这个目的
//初始化完,开始打印
Display(show, ROW, COL); //打印只需9*9大小,使用11*11大小那么游戏平台大小就不是所要求的了
//在test.c文件中
void Init(char board[ROWS][COLS], int rows, int cols,char flag)
{
for (int i = 0; i <= rows - 1; i++)
{
for (int j = 0; j <= cols - 1; j++)
{
board[i][j] = flag; //flag上述代码已讲解
}
}
}
void Display(char board[ROWS][COLS], int row, int col) //打印时行与列都要从数组下标1开始(只玩9*9)
{
printf( "--------------------扫雷游戏--------------------\n" );
for (int i = 0; i <= col ; i++)
{
printf("%d ", i); //给一个列号(便于玩家游玩)
}
printf("\n");
for (int i = 1; i <= row; i++)
{
printf("%d ", i); //给一个行号(便于玩家游玩)
for (int j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n"); //打印完一行空一行
}
printf("--------------------扫雷游戏--------------------\n");
}
注:头文件等请自行在game.h文件中引用,此处省略
2.设置雷:
第一步是定义、声明和打印
第二部开始设置雷,由于每次雷的位置都是随机的,因此需要使用rand()函数、srand()函数和time()函数,这三个函数具体讲解如下:
time()函数:
本函数即为时间戳,时间戳是指格林威治时间1970年01月01日00时00分00秒起至现在的总秒数。time(NULL)的返回值即为这个总秒数,因此这个返回值无时无刻在变化。
注:本函数使用前需要引头文件
rand()函数:
本函数是用来产生随机数的,但该随机数是个伪随机数,范围在0~32767。在调用本函数前,可以使用srand()函数设置随机数种子,如果没有设置,电脑自动设计随机数种子为1。随机数种子相同,那么产生随机数相同(恒为1)。
注:本函数使用前需要引头文件
srand()函数:
该函数需要一个无符号整数seed作为参数,这个seed在大小上没有要求,只是要求seed无时无刻在变化(不变的情况下,随机数种子相同,产生随机数相同),所以一般用time(NULL)作为该函数的参数。
注:本函数使用前需要引头文件
第三步为自定义一个set函数,开始实现设置雷的操作(注意,此处设置雷只能限制在9*9大小范围内)
首先在game.h文件中定义一个宏:#define easy_count 10 把他作为雷的数量,雷的数量越多,游戏难度越大,因此这样操作在日后修改游玩难度时比较便捷,具体代码如下:
void Set(char board[ROWS][COLS], int row, int col)
{
int count = easy_count; //count为10
while (count)
{
int x = rand() % row + 1; //行范围为1~9
int y = rand() % col + 1; //列范围为1~9
if (board[x][y] == '0')
{
board[x][y] = '1'; //‘0’为无雷,‘1’为有雷(if语句判断是为了防止已经有雷的位置重新布置雷)
count--; //count为10,每次布置完一个雷count减去1,等到count为0时10个雷布置完了,成功退出循环语句
}
}
}
3.玩家开始游玩+输赢判断:
自定义find()函数,声明定义与使用,排除雷是需要在排完mine数组一个元素以后,打印show数组的,同时笔者还想实现踩到雷后能清楚所有雷的位置,因此函数传参需要将mine与show数组一同传到find()函数中
void Find(char mine[ROWS][COLS], char show[ROWS][COLS] int row, int col)
{
int x, y;
printf("请输入要排除的坐标:>");
scanf("%d%d", &x, &y);
while (1)
{
if (x >= 1 && x <= 9 && y >= 1 && y <= 9) //范围为1~9
{
if (mine[x][y] == '1') //被雷炸死的情况
{
printf("你被炸死了\n");
Display(mine, ROW, COL); //让玩家知道10个雷的位置分别在哪
break;
}
else //没踩到雷
{
……;//该情况较为复杂,会仔细讲解
}
}
else //范围不是1~9
{
printf("输入错误,请重新输入");
}
}
}
没踩到雷的情况:
此时需要判断被排除位置上下左右一圈雷的数量,即
x-1,y-1 | x-1,y | x-1,y+1 |
x,y-1 | x,y | x,y+1 |
x+1,y-1 | x+1,y | x+1,y+1 |
注:红底坐标为被排除位置的坐标
现在mine()函数中存的都是字符1,而我们现在可以通过字符和整型数字之间转换的方式解决被排除位置周围雷数量的判断
字符和整型数字之间转化的方法:
‘1’ - 1 ---> 1
‘0’ - 0 ---> 0
由此可以得出,本游戏的实现可以是(被排除坐标周围所有字符相加 - 8 * ‘0’)
再把统计出来的周围雷的数量存入show函数中,注意这里需要把统计出来的数字重新转换成字符(统计出来的数字为整型数字,数组为字符型数组),综上所述,代码如下
int count = ((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');
show[x][y] = count + '0'; //转换成数字字符
Display(show, ROW, COL); //打印新的游戏平台
输的情况已经在上文讲解,现在还剩下赢的情况,赢的情况只需要定义一个计数器整型变量win,每确定一个非雷的位置,win加1,直到win为非雷位置总和,玩家获胜
void Find(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x, y;
printf("请输入要排除的坐标:>");
scanf("%d%d", &x, &y);
int win = 0;
while (win < row * col - easy_count)
{
if (x >= 1 && x <= 9 && y >= 1 && y <= 9) //范围为1~9
{
if (mine[x][y] == '1') //被雷炸死的情况
{
printf("很遗憾,你被炸死了\n");
Display(mine, ROW, COL); //让玩家知道10个雷的位置分别在哪
break;
}
else //没踩到雷
{
win++;
int count = ((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');
show[x][y] = count + '0'; //转换成数字字符
Display(show, ROW, COL); //打印新的游戏平台
}
}
else //范围不是1~9
{
printf("输入错误,请重新输入");
}
}
}
if (win == row * col - easy_count) //玩家胜利提醒
{
printf("恭喜你,排雷成功!\n");
Display(mine, ROW, COL);
}
}
注:vscode2022无法直接使用宏,可以再定义一个整型变量count,让其等于宏
注:笔者是在vscode上进行的编译
#include"game.h"
void menu() {
printf("**************************\n");
printf("***** 1.play 0.exit *****\n");
printf("**************************\n");
}
void game()
{
char mine[ROWS][COLS] = { 0 };//存放布置好的雷
char show[ROWS][COLS] = { 0 };//存放排查出的雷的信息
//初始化需求:mine没有布置雷的时候,都是‘0’;show数组在没有在排除雷的时候都是‘*’
Init(mine, ROWS, COLS,'0'); //最后一个0是为了传参以后让函数能够判断存入数组的是‘0’还是‘*’,再次调用时的1也是为了这个目的
Init(show, ROWS, COLS,'*');
//初始化完,开始打印
Display(show, ROW, COL); //打印只需9*9大小,使用11*11大小那么游戏平台大小就不是所要求的了
//设置雷
Set(mine, ROW, COL);
//排除雷
Find(mine, show, ROW, COL);
}
int main() {
int input;
srand((unsigned int)time(NULL));
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;
}
//在test.c文件中
#include"game.h"
void Init(char board[ROWS][COLS], int rows, int cols,char flag)
{
for (int i = 0; i <= rows - 1; i++)
{
for (int j = 0; j <= cols - 1; j++)
{
board[i][j] = flag;
}
}
}
void Display(char board[ROWS][COLS], int row, int col) //打印时行与列都要从数组下标1开始(只玩9*9)
{
printf( "--------------------扫雷游戏--------------------\n" );
for (int i = 0; i <= col ; i++)
{
printf("%d ", i); //给一个列号(便于玩家游玩)
}
printf("\n");
for (int i = 1; i <= row; i++)
{
printf("%d ", i); //给一个行号(便于玩家游玩)
for (int j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n"); //打印完一行空一行
}
printf("--------------------扫雷游戏--------------------\n");
}
void Set(char board[ROWS][COLS], int row, int col)
{
int count = easy_count; //count为10
while (count)
{
int x = rand() % row + 1; //行范围为1~9
int y = rand() % col + 1; //列范围为1~9
if (board[x][y] == '0')
{
board[x][y] = '1'; //‘0’为无雷,‘1’为有雷(if语句判断是为了防止已经有雷的位置重新布置雷)
count--; //count为10,每次布置完一个雷count减去1,等到count为0时10个雷布置完了,成功退出循环语句
}
}
}
void Find(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x, y, count = easy_count;
printf("请输入要排除的坐标:>");
scanf("%d%d", &x, &y);
int win = 0;
while (win < row * col - count)
{
if (x >= 1 && x <= 9 && y >= 1 && y <= 9) //范围为1~9
{
if (mine[x][y] == '1') //被雷炸死的情况
{
printf("很遗憾,你被炸死了\n");
Display(mine, ROW, COL); //让玩家知道10个雷的位置分别在哪
break;
}
else //没踩到雷
{
win++;
int count = ((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');
show[x][y] = count + '0'; //转换成数字字符
Display(show, ROW, COL); //打印新的游戏平台
}
}
else //范围不是1~9
{
printf("输入错误,请重新输入");
}
}
if (win == row * col - count) //玩家胜利提醒
{
printf("恭喜你,排雷成功!\n");
Display(mine, ROW, COL);
}
}
//在game.c文件中
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2 //只是把数组创建成11*11大小,但是在游戏中只会玩9*9的大小(例如打印,坐标输入错误提示等……)
#define easy_count 10;
void Init(char board[ROWS][COLS], int rows, int cols);
void Display(char board[ROWS][COLS], int row, int col); //此处数组还是[11][11]而不是[9][9],因为在定义数组时是[11][11],所以此处要相同
void Set(char board[ROWS][COLS], int row, int col);
void Find(char mine[ROWS][COLS],char show[ROWS][COLS],int row, int col);
//在game.h文件中
上述版本是基础版本,下面提出一些可以优化的地方:
1.插个旗作为标记该处有雷的功能并没有实现
2.
代码一开始就会展开一片
tips:当同时满足以下三种种情况
1.被排除坐标处没有雷 2.该坐标周围没有雷 3.该坐标不是雷
那么就可以展开一片,减少玩家无效游玩时间(因为这种坐标周围完全不需要判断雷在哪个位置)
展开方法:遇见某处周围1个雷也没有,且本身也不是雷,使用递归再次调用判断函数,像这样调用8次,然后新的可以展开坐标再次使用相同方法
3.玩家在游玩时需要自己数还剩多少个非雷坐标(或者是未被标记的雷的位置)
4.玩家有可能在游戏开始时就踩中了雷,可以通过重新打乱雷的位置等方法解决这个问题
5.玩家无法选择难度(即游戏平台大小,雷的数量等……)
6. .……
写在最后:
终于把两个游戏写完啦!!!撒花撒花
笔者将会在C语言全部学习完以后,跟着B站上的植物大战僵尸制作讲解视频进行复习,届时也会对植物大战僵尸的实现代码进行讲解,敬请期待~