引言
游戏概述
扫雷游戏是是一款大众类的益智小游戏,于1992年发行。游戏目标是在最短的时间内根据点击格子出现的数字找出所有非雷格子,同时避免踩雷,踩到一个雷即全盘皆输。在网页版扫雷游戏中,有不同的级别模式,级别越高,棋盘就越大,雷的数量就越多,要排查的时间也越久。甚至可以设置满屏,可谓是地狱难度,而雷数高达455个,喜欢扫雷的朋友们去可以尝试一下啦。同样你也可以自定义棋盘大小和雷数。本篇文章就要通过C语言实现一下这个扫雷游戏,就从9x9的初级模式开始,那接着往下看吧!
游戏规则
1.到我们的游戏界面,可以看到显示雷数和花费的时间,所有方块都得未被点击的。
2.当我们随机点开一个方块,游戏界面上就会出现一些数字,这写数字代表着点开方块的周围8个方块中有几个雷
3.点击方块,如果方格中没有雷,则会被打开;如果方块里有雷,则代表着踩到雷了,游戏结束。
4.当显示的数字与周围的方块数量相同时,如果点击的方块中是空白的,则会来带着周围方块一起打开。
5.游戏目标是在最短的时间内根据点击的方块出现的数字找出所有非雷方块。
扫雷游戏可以锻炼观察和推理的能力,培养玩家耐心和细心,通过不断的推测来排查哪些方块到底是雷,最终排查出所有的雷获得胜利,玩起来非常有趣!
扫雷游戏实现的基本思路
扫雷游戏同三子棋核心思想都是二维数组的应用,游戏的实现也用到了循环控制语句,分支语句,函数的递归等相关C语言知识,通过本篇文章也会加深大家对二维数组的理解。
实现的基本思路:
1.创建菜单,设置循环框架,通过switch语句进行选择
2.设定两个9*9的二维数组,一个是mine数组用来存放布置好的雷的信息,一个show数组用来存放排查出的雷的信息,用1表示雷,0表示不是雷的地方。
3.初始化这两个二维数组,mine数组一开始全是字符0,而show数组一开始全是字符#。
4.打印棋盘,每排查一次都打印出来看到排查的情况。
5.布置雷,这里我们在棋盘上随机产生10个雷,这里用的是rand函数。
6.排查雷,玩家输入坐标,若查到雷则游戏结束;若没有排查到雷则在show数组中显示该坐标周围8个坐标的雷的数量。
7.游戏结束,再次打印棋盘,玩家可选择继续玩或者退出游戏。
扫雷游戏的实现
创建菜单,设置循环
扫雷游戏的一些逻辑框架与我们三子棋如出一辙,都需要通过功能分解用函数的模块化思想来实现程序设计,这里我们将代码分别写到game.h,game.c,test.c里面去,game.h专门用来声明我们定义的函数,game.c里实现我们各个函数的功能,test.c来测试我们的游戏代码,这种分块思想我们就很熟悉了,在三子棋游戏里也使用过,便于理解。
int main()
{
int input = 0;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
printf("Let us start!\n");
game();
break;
case 0:
printf("exit!\n");
break;
default:
printf("error!\n");
break;
}
} while (input);
return 0;
}
这里的game函数里包含着我们后续将要实现的各种功能函数,所以当我们实现了函数在调试的时候,只需在game函数里修改便可。
初始化棋盘
这里我们需要考虑三个问题,首先我们为什么要创建两个数组?我们在分析思路的时候我们设置了用1表示雷,0表示不是雷的地方还记得吗,既然我们用了数字表示,那我们后面排查查看棋盘的时候也会显示数字1,所以这里不知道数字1到底表示的是雷还是雷的个数,就会产生误解,所以我们设置两个数组严格对应信息,不会产生歧义。
其次,后续我们排查到如下图边缘的坐标时候,我们依然希望能给我们返回该坐标周围8个坐标的雷的数量,不难看出这里会统计到棋盘范围之外的坐标,会出现数组越界的情况,所以我们这里为了防止在统计坐标周围雷的数量的时候,我们把数组设置为11*11,这样四周都不会出现数组越界情况。
再者,我们考虑把两个二维数组设置为字符数组,mine数组一开始全是字符0,而show数组一开始全是字符#,这样我们初始化棋盘只需封装一个函数,两个数组都可以使用。
常量的定义:
#define EASY_COUNT 10//雷数
#define ROW 9 //可随意设置,扫雷棋盘列数
#define COL 9 //可随意设置,扫雷棋盘行数
#define ROWS ROW+2 //数组的列数
#define COLS COL+2 //数组的行数
初始化棋盘:
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; //我们希望,mine数组一开始全是字符0,而show数组一开始全是字符#
}
}
}
注意,我们为了防止越界,我们使用的数组一直都是1111的数组,但实际操作的是99的数组,所以这里要注意传参时候我们到底要访问是11 11还是9*9数组。
打印棋盘
我们初始化好肯定需要打印出来看看是什么样子的,这里就只需打印9*9即可,所以我们封装一个打印的函数:
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
printf("--------扫雷游戏--------\n"); //打印分割信息
for (i = ; i <= col; i++)
{
printf("%d ", i);
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%d ", i);
int j = 0;
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
mine数组:
show数组打印:
布置雷
同样的我们在9*9棋盘上实现布置雷的操作,这里需要用到我们生成随机种子的函数rand,生成随机坐标布置雷,这个随机数的生成我们在三子棋游戏中我们也讲过。
void Setmine(char board[ROWS][COLS], int row, int col)
{
int count = EASY_COUNT;
while (count)
{
int x = rand() % row + 1; //生成0~9的数
int y = rand() % col + 1; //生成0~9的数
if (board[x][y] == '0')
{
board[x][y] = '1';
count--; //每成功布置一个雷count--
}
}
}
实际上我们真正在玩游戏时候也不会打印出雷的信息,在调试阶段我们打印出来看到,已经随机生成了10个雷。
排查雷
雷布置好了我们就可以开始排查了,排查雷的实现也是本文的核心部分。排查雷的过程中一定会涉及到两个数组,因在mine数组中排查把信息放置在show数组中,操作的同样是9*9的数组。
排查过程是需要通过坐标完成,首先就要判断坐标的合法性,坐标范围要在我们9*9的棋盘内,坐标不合法需要重新输入。
输入的坐标刚好是字符1,说明我们排到了雷,游戏结束,并打印出棋盘看到结果;没有排到雷,统计周围雷的个数。我们用Getminecount函数来统计,这里如图所示我们统计周围8个坐标后返回周围雷数。
我们先把雷盘也打印出来,我们看着雷盘来分析,这里(2,1)处是一个雷,所以我们排查(1,2)处时候这里就返回打印出了字符1,说明(1,2)处周围8个坐标有一个雷,以此类推。
这里为了方便测试我们把雷数设为80个,测试一个排雷成功的例子:
我们通过雷盘发现只有(9,4)处不是雷,我们输入要排查的坐标(9,4),看到我们排雷成功了,说明我们这个80个雷的测试通过。此处无雷区域的展开我们使用递归函数实现,如果无雷,那么我们就不返回0了,而是将show数组中的#都赋值为空格 ,并且使用for循环将以这个坐标的周围8个坐标使用递归函数将show数组中的#都赋值为空格,而在if-else语句那里有雷的代码就可以直接调用这个扩展函数了,如图
最后我们判断输赢,如果整个排雷过程中排到雷了当时就能够打印结果,如果没有排到雷,那我们的判断条件就是所有除雷之外的坐标都被查完了,说明我们游戏胜利了。
完整代码
game.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define EASY_COUNT 10
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
//初始化
void InitBoard(char board[ROWS][COLS],int rows,int cols,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);
//递归展开
void ExpandBlank(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)
game.c
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include"game.h"
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;
printf("--------扫雷游戏--------\n");
for (i = 0; i <= col; i++)
{
printf("%d ", i);
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%d ", i);
int j = 0;
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
void Setmine(char board[ROWS][COLS], int row, int col)
{
int count = EASY_COUNT;
while (count)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (board[x][y] == '0')
{
board[x][y] = '1';
count--;
}
}
}
int Getminecount(char mine[ROWS][COLS], int x, int y)
{
return (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');
}
void ExpandBlank(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y);
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- EASY_COUNT)
{
printf("请输入要排查的坐标:>");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (mine[x][y] == '1')
{
printf("很遗憾,你被炸死了\n");
DisplayBoard(mine, ROW, COL);
break;
}
else
{
ExpandBlank(mine, show, x, y);
DisplayBoard(show, ROW, COL);
win++;
}
}
else
printf("坐标非法,重新输入\n");
}
if (win == row * col - EASY_COUNT)
{
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
}
}
//无雷区域的扩展,使用递归实现
void ExpandBlank(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{
int count = Getminecount(mine, x, y);
if (count)
{
// 将雷的数量放入show数组中, 数字加上字符转换成对应数字字符,‘0’是将num由int类型转换为字符char类型
show[x][y] = count + '0';
}
else if (show[x][y] == '#')
{
show[x][y] = ' ';//将没有雷的地方赋值为空格
int i = 0, j = 0;//使用循环作为递归的条件,找出无雷区域
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
ExpandBlank(mine, show, i, j);
}
}
}
}
test.c
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include"game.h"
void menu()
{
printf("********************\n");
printf("****** 1.play ******\n");
printf("****** 0.exit ******\n");
printf("********************\n");
}
void game()
{
char mine[ROWS][COLS];//存放布置好的雷
char show[ROWS][COLS];//存放排查雷的信息
//初始化棋盘
//mine一开始都是0
//show一开始都是#
InitBoard(mine, ROWS, COLS,'0');
InitBoard(show, ROWS, COLS,'#');
//打印棋盘
/*DisplayBoard(mine, ROW, COL);*/
DisplayBoard(show, ROW, COL);
//布置雷
Setmine(mine, ROW, COL);
DisplayBoard(mine, ROW, COL);
//排查雷
Findmine(mine, show, ROW, COL);
}
int main()
{
int input = 0;
do
{
menu();
srand((unsigned int)time(NULL));
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
printf("Let us start!\n");
game();
break;
case 0:
printf("exit!\n");
break;
default:
printf("error!\n");
break;
}
} while (input);
return 0;
}
结语
扫雷游戏运用到了我们二维数组,循环语句和分支语句等知识,代码实现不算太难,更重要的是如何运用编程思维通过C语言把扫雷规则表达出来,通过扫雷游戏的从无到有的实现过程也对知识点有了更加深入的理解和运用,所以说还得实践出真知!今天的分享就到这里了,这也是我的第三篇博客了,完成了第二个小游戏的实现,嘿嘿加油!也希望大家批评指正,都能越来越好!