前言:感谢各位朋友的捧场,这里给大家分享的是扫雷游戏的简单实现
(PS:这里简单是指只实现了游戏的基础功能和主要流程,由于当前本人技术知识尚薄弱,相关的优化会通过后续的学习进行更新)
《扫雷》是一款大众类的益智小游戏,于1992年发行。游戏目标是在最短的时间内根据点击格子出现的数字找出所有非雷格子,同时避免踩雷,踩到一个雷即全盘皆输。 ——百度百科
一、准备工作
1. 文件分类
这里同样分为 game.h、game.c、test.c 三个文件进行编写,同专栏的下面这篇文章中有详细介绍:
【C语言】-三子棋-简单版
2. #define 定义标识符常量的设置
如下图:
- 解释:
(1)因为先着重于游戏主要功能的实现为目的,故将难度先默认为简单版也就是9×9的界面大小,总共只有10个雷;具体的后续优化将在后面的一部分介绍。
(2)这里先介绍一下后面排查雷的思路:选定一个坐标,看其周围一圈有几个雷。如下图所示:
假定蓝色圈为选定坐标,那么就要检测其周围八个位置即图中红色圈所经过的方块中有几个雷;而如果选定的坐标在边界处,如上图黄色圈的位置,此时若对其进行特殊情况处理会比较繁琐,故可以直接多增加一行一列来简化整体的程序。
3. 包含头文件
#include"game.h"
同样详细可见:
【C语言】-三子棋-简单版
二、编写过程
1. 简单主界面的建立
这里也和三子棋一样,就不再赘述啦,朋友们感兴趣的可以看看:
【C语言】-三子棋-简单版
下面我们开始编写 game()函数内容的介绍
2. 界面初始化与打印
- 在开始编写之前,先介绍一下一些“规则”,其实也就是实现的思路。
假设我们采用字符 ‘1’ 来表示雷;用字符 ‘0’来表示没有雷,这样就会有个问题:当游戏界面上出现一个1是表示当前位置是雷呢?还是表示当前位置的周围八个位置中有一个雷呢?
这里我们可以通过两个二维数组来解决这个二义性的问题:一个用于存放雷;一个用于显示。
布置雷的操作我们就在存放雷的二维数组中进行;游戏中实际的界面及扫雷过程我们就在显示的二维数组中进行(前者进行内部设置,后者进行对外展现)
这里可能可能还有一个问题:为什么不用 ‘*’ ‘#’ 等字符来表示雷呢,关于这个问题我们会在后面计算某坐标周围有几个雷时进行解答,好奇的朋友也可直接先跳转到那部分看看。 - 根据上述内容,我们就可以在 game() 函数中创建这两个二维数组了:
char mine[ROWS][COLS] ; //用于放置雷
char show[ROWS][COLS] ; //用于打印
接着,和使用一个变量一样,我们需先对其进行初始化,因为两个二维数组的作用不同,所以初始化的内容的也不同。我们的思路是这样的,对于放置雷的数组,先全都初始化为字符 ‘0’,后面再一个函数来进行布置雷;对于显示界面的数组,我们可以全都初始化为字符 ‘*’,来模拟扫雷一开始的游戏界面;接着,在打印界面的时候,我们可以加上行号和列号,便于游戏时通过键入坐标来排雷。初始化效果图如下:
放置雷的数组(实际游戏过程中不打印,这里仅说明用):
显示界面的数组:
接下来我们介绍具体是如何实现的。
(1)界面初始化
知识点: 相比于三子棋中初始化棋盘的函数,扫雷中的初始化函数新增添了一个参数,来实现指定初始化的内容,增强了代码的复用性,而不用为两个不同功能的二维数组分别编写初始化函数。
其在头文件中的声明如下:
在 game.c 文件中进行编写的代码如下:
void InitBoard(char board[ROWS][COLS], int row, int col, char set)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
board[i][j] = set;
}
}
}
解释:双循环遍历二维数组,将所有元素置为 set 中的内容即可。
(2)界面打印
头文件声明如下:
在 game.c 文件中进行编写的代码如下:
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
for (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++)
{
printf("%c ", board[i][j]); //打印内容
}
printf("\n");
}
}
解释:结合上述的效果图,我们先通过一个单独的for循环打印列标,然后再通过一个双重for循环实现在打印数组内容前先打印行标。(PS:为了对齐效果,列标比行标多打印了一个0,所以需要注意一下循环变量的初始化部分)
有了上述内容,game()函数就可以有以下这么几行:
char mine[ROWS][COLS] ; //用于放置雷
char show[ROWS][COLS] ; //用于打印
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
DisplayBoard(mine, ROW, COL); //用于测试,实际不打印
DisplayBoard(show, ROW, COL); //实际打印
game() 函数的测试结果:
完成初始化和打印界面的函数后,接下来就可以着手布置雷。
3. 布置雷
头文件声明如下:
在 game.c 文件中进行编写的代码如下:
void SetMine(char board[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int count = EASY;
while (count)
{
x = rand() % row + 1;
y = rand() % col + 1;
if ('0' == board[x][y])
{
board[x][y] = '1';
count--;
}
}
}
解释:这里放置雷的思路就是在 9×9 的游戏界面中生成10个随机坐标,但需要注意的是生成坐标后需判断一下当前坐标是否已经布置过雷了,避免相同的坐标处进行重复次数的布雷。以布雷的个数作为控制循环的条件,当布置雷的数量足够后即可跳出循环。对于其中随机数生成函数的较详细介绍可阅读下面这篇文章中的随机数生成部分:
【C语言】-猜数游戏-简单版
当布置雷函数编写完成后,game() 函数中就扩充为如下代码:
srand((unsigned int)time(NULL));
char mine[ROWS][COLS] = {0}; //用于放置雷
char show[ROWS][COLS] = {0}; //用于打印
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
SetMine(mine, ROW, COL);
DisplayBoard(mine, ROW, COL); //用于测试,实际不打印
DisplayBoard(show, ROW, COL); //实际打印
测试结果如下:
可以看到,用于布置雷的数组已经完成10个雷的布置,字符 ‘1’ 处即为雷的位置。
雷布置完成了,接下来介绍最后一个函数的实现,也就是排查雷
4. 排查雷
结合上述设计两个作用不同的二维数组的思路,我们在二维数组mine(布置雷的数组)中排查,在二维数组show(实际显示的数组)中反馈排查信息。整个排查雷的过程可分为几个部分,下面逐一进行讲解:
(1)输入排查坐标
此部分需要注意的就是对输入坐标合法性的判断:
- 输入坐标的是否超过界面大小
- 输入的坐标是否已经被排查过了
部分代码如下:
printf("请输入坐标:\n");
scanf("%d%d", &x, &y);
if (x < 1 || x>ROW || y<1 || y>COL) //检查输入坐标是否越界
{
printf("输入错误,请重新输入:\n");
continue;
}
if (show[x][y] != '*') //检查输入坐标处是否已被排查
{
printf("该位置已排查,请重新输入:\n");
continue;
}
(2)某坐标处周围雷数的计算:
这里我们设计一个函数来专门服务于某坐标周围雷数的计算
- 代码如下:
int get_mine_count(char mine[ROWS][COLS], int x, int y)
{
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’ 来表示雷的好处了:
- 这里先补充一个字符作为整数返回的规则:字符若作为整数返回,那么返回的就是其ASCII码值。如字符 ‘A’ 的ASCII码值为65,那么其在作为整数返回时,返回值就是65。
- 回到函数中,字符 ‘1’ 的ASCII码为49,而字符 ‘0’的ASCII码为48,那么二者相减得到就是ASCII码为1的字符,那么在把这个字符作为整数返回时,其实返回的就是它的ASCII码值,也就是1。
- 那么我们若想知道某坐标周围8个位置中有几个雷,也就是有几个字符 ‘1’ ,我们就可以把这些字符都加起来,然后分别都减去一个字符 ‘0’,这样最终返回的ASCII码值其实就是周围位置中雷的个数了。
(注:字符 ‘1’ 与 ASCII码为1的字符不是一个概念;字符 ‘1’ 加 字符 ‘1’可不等于 字符 ‘2’,本质是它们的ASCII码值进行相加;返回的ASCII码值不能超过ASCII码规定的范围,即0-127。) - 但若想在实际显示的数组(show)中显示表示有几个雷的具体数字,我们还需在函数返回值的基础上再加上一个字符 ‘0’,即如下面条语句:
show[x][y] = get_mine_count(mine, x, y) + '0';
因为该数组show的类型为字符型,此时若想通过打印字符的方式(即打印数组show的元素)来实现打印数字就需要打印数字对应的字符(而不是ASCII码值),有点绕,举个例子:
如若想屏幕上出现数字3,就得打印一个字符 ‘3’,而不是ASCII码值为3的字符。
所以,在函数返回周围有几个雷之后再加上一个字符 ‘0’ ,就得到雷数所对应的字符,也就能在屏幕上打印出来了。
(小知识:对于字符0-9,若想得到数字0-9,就可用相应字符减去一个字符 ‘0’;反之同理。)
通过这个函数,我们就完成了某坐标周围雷数的计算。
(3)扫雷失败与完成的条件
扫雷失败的条件为:输入排查雷的坐标在放置雷的数组(mine)中刚好对应着雷,也就是字符 ‘1’。
扫雷的完成条件为:把所有不是雷的位置都找出来。
具体到 9×9的界面,10个雷的难度中,就是要找出 9×9-10=71个位置。
由此我们可以创建一个计数变量,只要我们排查的坐标上不是雷,那么计数变量++,直到达到设定的值,在9×9上也就是71。
部分代码如下:
if (mine[x][y] == '1')
{
printf("You Lose!\n");
break;
}
else
{
show[x][y] = get_mine_count(mine, x, y) + '0';
win++;
}
DisplayBoard(show, ROW, COL);
if (win == row * col - EASY)
{
printf("You Win!\n");
break;
}
将以上部分组成循环:
当输入坐标不合法时,循环输入直至合法;
输入合法后,判断坐标是否为雷,若为雷,则打印“失败信息”并跳出循环;若不是雷,则可先打印界面(show数组),然后判断计数变量是否满足扫雷完成的条件,若满足,则打印“成功信息”并跳出循环。
实现排查雷的完整代码如下:
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;
while (1)
{
printf("请输入坐标:\n");
scanf("%d%d", &x, &y);
if (x < 1 || x>ROW || y<1 || y>COL) //检查输入坐标是否越界
{
printf("输入错误,请重新输入:\n");
continue;
}
if (show[x][y] != '*') //检查输入坐标处是否已被排查
{
printf("该位置已排查,请重新输入:\n");
continue;
}
if (mine[x][y] == '1')
{
printf("You Lose!\n");
break;
}
else
{
show[x][y] = get_mine_count(mine, x, y) + '0';
win++;
}
DisplayBoard(show, ROW, COL);
if (win == row * col - EASY)
{
printf("You Win!\n");
break;
}
}
}
至此,游戏进行所需的函数全都编写完成,接下来就可以进行“组装”测试了
game() 函数的最终内容如下代码:
void game()
{
srand((unsigned int)time(NULL));
char mine[ROWS][COLS] = {0}; //用于放置雷
char show[ROWS][COLS] = {0}; //用于打印
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
SetMine(mine, ROW, COL);
DisplayBoard(mine, ROW, COL);
DisplayBoard(show, ROW, COL);
FindMine(mine, show, ROW, COL);
}
5.完整代码:
(1)game.h文件:
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
//实际显示的界面大小
#define ROW 9
#define COL 9
//实际需要的二维数组的大小
#define ROWS ROW+2
#define COLS COL+2
//放置雷的个数
#define EASY 10
//界面初始化
void InitBoard(char board[ROWS][COLS], int row, int col, char set);
//打印界面
void DisplayBoard(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);
(2)game.c文件:
#include"game.h"
//界面初始化(代码的复用性)
void InitBoard(char board[ROWS][COLS], int row, int col, char set)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
board[i][j] = set;
}
}
}
//打印界面
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
for (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++)
{
printf("%c ", board[i][j]); //打印内容
}
printf("\n");
}
}
//放置雷
void SetMine(char board[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int count = EASY;
while (count)
{
x = rand() % row + 1;
y = rand() % col + 1;
if ('0' == board[x][y])
{
board[x][y] = '1';
count--;
}
}
}
//排查雷
//1.在二维数组mine中排查,在二维数组show中反馈排查信息
//2.专设函数判断周围一圈有几个雷
//3.排查完毕的条件
int get_mine_count(char mine[ROWS][COLS], int x, int y)
{
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表示雷不用#号等符号表示雷的原因:不用循环,直接加一圈,再把字符转换成数字
//字符1加字符1可不等于字符2
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;
while (1)
{
printf("请输入坐标:\n");
scanf("%d%d", &x, &y);
if (x < 1 || x>ROW || y<1 || y>COL) //检查输入坐标是否越界
{
printf("输入错误,请重新输入:\n");
continue;
}
if (show[x][y] != '*') //检查输入坐标处是否已被排查
{
printf("该位置已排查,请重新输入:\n");
continue;
}
if (mine[x][y] == '1')
{
printf("You Lose!\n");
break;
}
else
{
show[x][y] = get_mine_count(mine, x, y) + '0';
win++;
}
DisplayBoard(show, ROW, COL);
if (win == row * col - EASY)
{
printf("You Win!\n");
break;
}
}
}
(3)test.c文件:
#include"game.h"
void menu()
{
printf("***********************************\n");
printf("************ 1.Play *************\n");
printf("************ 0.Exit *************\n");
printf("***********************************\n");
}
void game()
{
srand((unsigned int)time(NULL));
char mine[ROWS][COLS] = {0}; //用于放置雷
char show[ROWS][COLS] = {0}; //用于打印
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
SetMine(mine, ROW, COL);
DisplayBoard(mine, ROW, COL);
DisplayBoard(show, ROW, COL);
FindMine(mine, show, ROW, COL);
}
void test()
{
int input = 0;
do
{
menu();
printf("请输入进行选择:\n");
scanf("%d", &input);
switch (input)
{
case 1:
game();
case 0:
break;
default:
printf("输入错误,请重新输入:\n");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}
三、优化思路:
PS:这一部分作为即时更新内容,由于本人知识技术尚薄弱,优化的具体实现需一段时间来完成,后续若有新的优化思路或解决了某个优化问题,将在这一部分进行更新分享
根据经典版扫雷的游玩, 总结了以下可优化之处:
(PS:主观上优化的重要程度按序号排列)
(1) 一开始选择的位置一定不是雷,且若周围没有雷则应向周围自动展开一片
(2)能标记和取消标记雷
(3)能显示剩余雷的个数及游戏已经进行的时间
(4)多打印一个界面,可进行难度的选择,而不是直接更改定义的标识符的常量的数值
(5)利用EasyX(C语言的界面库)进行相关视觉效果上的优化等
如果大伙还有什么优化思路,非常希望各位在评论区留下宝贵的意见。感激不尽!
以上就是我对C语言实现简单版扫雷的内容分享啦。
看完觉得有觉得帮助的话不妨点赞收藏鼓励一下,有疑问或看不懂的地方或有可优化的部分还恳请朋友们留个评论,多多指点,谢谢朋友们!🌹🌹🌹