目录
前言
- 当我们学习完C语言中二维数组和函数这一块的知识以后,我们就可以编写一些小游戏了,比如说五子棋,扫雷,本文将讲述如何通过C语言实现扫雷,想学习如何实现五子棋的伙伴可以看看我的上一篇文章
- 链接:在15*15的棋盘中下五子棋
以下是本篇文章正文内容,下面案例可供参考
一:游戏框架的书写
- 我们要把这个游戏的框架分为三个文件,分别是game.c,game.h,test.c
- 在game.c中我们实现我们需要的函数的定义
- 在game.h中实现常量的定义和函数的声明和需要的头文件的引用
- 在test.c中实现游戏的主体框架
- 首先我们先看看test.c的完成品,才来说如何实现
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
void game()
{
//数组的创立
char mine[ROWS][COLS];
char show[ROWS][COLS];
//初始化棋盘
Initboard(mine, ROWS, COLS, '0');
Initboard(show, ROWS, COLS, '*');
//展示棋盘
Displayboard(show, ROW, COL);
//埋雷
Setmine(mine, ROW, COL);
//Displayboard(mine, ROW, COL);
//排雷
Findmine(mine, show, ROW, COL);
}
void menu()
{
printf("************************\n");
printf("****** 1.play ******\n");
printf("****** 0.exit ******\n");
printf("************************\n");
}
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (input);
}
1.main函数
- 我们首先看到main函数,抛开srand那一行先不谈,剩下的do while语句实现的是接收玩家输入的值,然后根据值进入不同的接口
- 如果是1则进入game函数开始游戏,如果是0则退出游戏
- 为了让玩家知道应该输入什么进行和退出游戏,我们首先可以打印一个菜单,就是menu函数
- 我们还需要考虑到玩家可能输入除0和1以外的其它值,对此做出判断,让玩家重新输入
- 所以while后的括号中填input就完美实现了这一点,如果是0为假则循环结束
- 如果是非零则进入循环,再通过switch语句判断是1还是其它值
- do while则保证了至少一次输入
2.game函数
- main函数实现之后,游戏的大体框架就基本完成,剩下我们需要往里面填充血肉
- 这个血肉其实就是一个个函数,其中最重要的函数就是game函数
- 而game函数又包括了好几个子函数
- 我们思考一下game函数需要哪些子函数才能完成它应有的功能
- 首先我们需要一个棋盘,所以我们需要一个初始化棋盘函数Initboard
- 重点来了,其实我们需要两个棋盘,因为我们需要一个用来布置雷,另一个才是玩家看到的棋盘,上面一开始是空白,随着玩家的输入,棋盘上会出现数字告诉玩家周围有多少雷
- 想一想,如果我们只有一个棋盘,那怎么做到记住雷的位置的同时又显示周围有多少雷呢,而我们有两个棋盘的话,就会变得十分容易
- 我们把埋雷的棋盘叫做mine,用于显示的棋盘叫做show
- 现在讨论一下棋盘大小的问题
- 假设我们要实现9*9的扫雷
- 我们棋盘大小应该设置成11*11
- 因为在后面为了告诉玩家周围8个格子有多少雷的时候,我们需要遍历玩家给出的坐标附件的8个格子,才能得到周围雷的个数,设置成11*11的棋盘可以避免当玩家给出的坐标在边界时,造成的越界访问问题。
- 虽然我们棋盘大小是11*11
- 但我们实际进行游戏的棋盘还是9*9,为了后面方便修改我们棋盘的大小,我们可以使用#define定义ROW,COL为9,ROWS,COLS为11
- 而前文说了有关常量的定义我们放在game.h中,是为了方便另外两个文件的使用
- 创建完棋盘以后,我们就需要初始化棋盘,也就是实现Initboard函数
- 初始化棋盘之后,我们需要展示棋盘给玩家,就需要Display函数
- 为了给我们的mine数组中埋雷,我们需要Setmine函数
- 最后让玩家排雷,就需要Findmine函数
- 到此为止,game函数的框架我们也定下来了
- 剩下的只需要实现我们所需要的函数,扫雷就大功告成了。
二、函数的声明
- 在实现我们需要的函数之前,我们先在game.h中对其进行声明
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define COL 9
#define ROW 9
#define ROWS ROW+2
#define COLS COL+2
#define level 10
//函数的声明
//初始化棋盘函数
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 mine[ROWS][COLS], int row, int col);
//排雷函数
void Findmine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
//
- 其中第一行的#pragma once是为了防止头文件的多次引用
- 常量level代表我们所需要埋雷的数量
三:game.c中函数的实现
1.Initboard函数实现
-
这个函数的实现非常简单,不过对于mine数组和show数组要有不同的初始化
-
我们把mine数组的值全部初始化为0,代表现在所有位置都只有0个雷
-
当后续调用Setmine函数进行埋雷时再把mine数组中随机10个位置改为1,代表这10个位置有雷
-
对于show数组我们全部初始化为*,代表未知,玩家需要输入坐标获取某一位置的信息
-
要是这个坐标是雷,则游戏结束,不是则把*改为数字,代表周围雷的个数
-
所以函数的参数就有4个,一个是board数组用于接收mine或show数组
-
rows和cols分别代表所需要初始化的行列数
-
set代表所需要初始化的值,mine是0,show是*
-
代码如下
void Initboard(char board[ROWS][COLS], int rows, int cols, char set)
{
int i = 0;
int j = 0;
for (i = 0; i < rows ; i++)
{
for (j = 0; j < cols ; j++)
{
board[i][j] = set;
}
}
}
2.Displayboard函数实现
-
这个函数的作用就是打印棋盘的内容,不过我们需要打印行索引和列索引方便玩家查看行列
-
同时还需要通过-字符打印个框架,其实没有这一步也可,不过打印上看起来更加舒适
-
成品如下:
-
代码如下:
void Displayboard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i <= col; i++)
{
printf("%3d", i);
}
printf("\n");
for (i = 0; i < 3 * (COL+1); i++)
{
printf("-");
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%2d|", i);
for (j = 1; j <=col; j++)
{
printf("%3c", board[i][j]);
}
printf("\n");
}
for (i = 0; i < 3 * (COL + 1); i++)
{
printf("-");
}
printf("\n");
}
3.Setmine函数实现
- 这个函数作用就是将mine数组中10个位置的0改为1,代表埋雷
- 具体实现就可以依靠rand函数,不过这个函数需要一个种子,就是srand函数
- 并且只需要一次,所以我们将它放在主函数中,还记得吗
- srand((unsigned int)time(NULL));
- 之后我们就可以通过rand函数生成随机数进行埋雷
- 不过为了保证数字的有效性,我们需要将得到的随机数的值修改
- 将这个数对行或列取余,然后加一,取余很好理解,加一是为什么呢
- 首先取余保证这个数在0-8之间,加一则让这个数在1-9之间,是因为我们实际的棋盘大小为11*11
- 而使用的只有9*9,将随机数限制在1-9之间刚好能够保证它在我们实际使用的棋盘上
- 另外我们可能生成相同的随机数,所以每次对mine数组的值做出判断
- 如果是0则将其改成1,并让count–,如果是1则不修改
- 这样当count为0时我们就埋好10个雷了
- 代码如下
void Setmine(char mine[ROWS][COLS], int row, int col)
{
int count = level;
while (count)
{
int i = rand() % row + 1;
int j = rand() % col + 1;
if (mine[i][j] == '0')
{
mine[i][j] = '1';
count--;
}
}
}
4.getmine函数实现
- 这个函数的作用并未在game函数中出现,因为这是一个辅助函数
- 作用是找到玩家输入的坐标周围的8个格子有几个雷,并返回雷的个数,我们将在Findmine函数中使用它
- 代码如下
int getmine(char mine[ROWS][COLS], int x, int y)
{
int i = 0;
int j = 0;
int sum = 0;
if (x <= ROW && y <= COL&&x>=1&&y>=1)
{
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
if (i == x && j == y)
{
continue;
}
sum += mine[i][j] - '0';
}
}
}
return sum;
}
5.moreinformation函数实现
- 这个函数是扫雷游戏最重要的一个辅助函数了,作用是成片展开show数组告诉玩家这些位置有多少雷
- 前面的getmine函数只能返回一个坐标附近雷的个数,而这里能展开一片
- 我们为什么需要展开一片呢?玩过扫雷的都知道,当我们点中一个位置,如果这个位置一个雷都没有,它将继续展开周围的格子告诉我们雷的个数,直到某个位置周围有1个或一个以上的雷才停止
- 具体实现时就是通过递归函数,首先玩家输入一个坐标,我们用getmine获取这个坐标周围有多少雷,如果为0个雷,我们就调用moreinformation函数,就是对这个坐标周围8个坐标继续调用getmine函数返回雷的个数,如果是0则继续,不是0则停止
- 其中递归终止的条件我们确定了,但还有一点我们需要注意,就是判断一个格子是否已经调用过getmine函数,也就是是否已经返回了雷的个数,如果是我们需要跳过这一次递归,否则会造成死递归
- 举个栗子,假设我们对x,y坐标处调用getmine函数返回了0,我们需要调用moreinformation对x,y周围8个坐标再次调用getmine获取雷的个数
- 我们首先从x-1,y-1这个位置开始调用,如果发现这个位置返回的雷的个数仍是0,我们又需要对x-1,y-1这个位置调用moreinformation函数,然后这时x,y在x-1,y-1周围8个坐标中,我们又会对x,y调用getmine函数,而这个地方前面已经确定是0,所以又会调用moreinformation函数,然后又会对x-1,y-1这个位置调用getmine函数发现是0,继续调moreinformation函数,这时候又会回来对x,y调用getmine,然后调用moreinformation
- 这就陷入了死递归
- 所以我们每次需要对show数组的值进行判断,如果不是*,代表已经调用过getmine,就跳过这次递归
- 其中ret代表一个位置雷的个数,如果是0,则终止递归
- 代码如下:
int moreinformation(char show[ROWS][COLS], char mine[ROWS][COLS],int ret,int x,int y)
{
int i = 0;
int j = 0;
if(ret)
{
return 0;
}
if (!ret)
{
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
if (show[i][j] != '*')
continue;
if ((i == x && j == y) || i>ROW || j>COL||i<1||j<1)
{
continue;
}
show[i][j] = '0' + getmine(mine, i, j);
moreinformation(show, mine, show[i][j] - '0', i, j);
}
}
}
}
- 实现后效果如下
6.victory函数实现
- 这个函数的作用是判断棋盘上标记雷的数量,并且判断玩家标记雷的位置和埋雷的位置是否一致,如果一致,则让sum++,最终返回sum的值
- 当sum的值和我们埋下的雷的值一致时,则可以判定排雷成功,游戏结束
- 代码如下:
int victory(char show[ROWS][COLS],char mine[ROWS][COLS])
{
int i = 0;
int j = 0;
int sum = 0;
for (i = 1; i <= ROW; i++)
{
for (j = 1; j <= COL; j++)
{
if (mine[i][j] == '1')
{
if (show[i][j] == '#')
sum++;
}
}
}
return sum;
}
7.Findmine函数实现
- 这个函数的作用就是让玩家排雷,首先我们提供给玩家两个选择
- 一个是1-排雷,另一个是0-标记雷
- 我们还需要对玩家输入的值进行判断,如果不是1或0,则让玩家重新输入
(1)排雷
- 如果输入1,则进入排雷,让玩家输入他想排雷的坐标,先判断坐标合法性,
- 如果合法,则判断玩家输入的坐标是否是雷,就是判断mine数组的这个位置是不是1,如果是1,代表这个位置是雷,则玩家排雷失败
- 如果是0,代表这个位置不是雷,我们继续判断这个位置对应的show数组的值
- 如果是#,则询问玩家是否需要取消标记
- 如果是数字,则告诉玩家这个地方已经排查过
- 如果是*,我们就调用getmine函数返回这个位置附近雷的个数
- 如果getmine返回的是0,则调用moreinformation函数实现成片展开
- 之后展现棋盘,再调用victory函数判断此时棋盘上有多少个标记的雷
- 如果sum==10,则排雷成功游戏结束
(2)标记雷
- 如果玩家输入0,则进入标记雷,让玩家输入他想标记的坐标,判断坐标合法性,再判断show数组这个位置是否是*,如果是,则将其改为#,代表标记这个位置是雷
- 标记成功后调用victory函数,让sum的值改变
- 如果不是*,再判断是数字还是#,如果是数字,提醒玩家这里是已经排查过的坐标,
- 如果是#,则询问玩家是否需要取消标记,如果玩家输入1,则代表玩家需要取消标记,我们就将这个位置的#改为*
- 最后判断sum的值是否为10,如果为10,代表玩家已经排了10个雷,则玩家胜利
- 代码如下:
void Findmine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int a = 0;
int b = 0;
int choose=0;
int c = 0;
int sum = 0;
while (sum<10)
{
printf("排查雷请按1,标记雷请按0\n");
scanf("%d", &choose);
if (choose != 0 && choose != 1)
{
while (1)
{
printf("无效数字,请重新输入>\n");
scanf("%d", &choose);
if (choose == 0 || choose == 1)
{
break;
}
}
}
if (!choose)
{
printf("请输入要标记雷的坐标>示例:1 1\n");
scanf("%d %d", &a, &b);
if (a >= 1 && a <= row && b >= 1 && b <= col)
{
if (show[a][b] == '*')
{
show[a][b] = '#';
printf("标记成功\n");
sum = victory(show, mine);
Displayboard(show, ROW, COL);
}
else if (show[a][b] == '#')
{
printf("这个地方是你标记的雷,你需要取消吗?\n");
printf("取消标记请按1,不取消标记请按0\n");
scanf("%d", &c);
if (c != 0 && c != 1)
{
while (1)
{
printf("无效数字,请重新输入>\n");
scanf("%d", &c);
if (c == 0 || c == 1)
{
break;
}
}
if (c == 1)
{
show[x][y] = '*';
printf("取消标记成功!");
Displayboard(show, ROW, COL);
}
}
}
else
printf("此处是已排查雷的坐标,请勿标记!\n");
continue;
}
else
{
printf("坐标输入错误,请重新输入\n");
}
}
printf("请输入要排查雷的坐标> 示例:5 5\n");
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 if(show[x][y]!='*')
{
if (show[x][y] == '#')
{
printf("这个地方是你标记的雷,你需要取消吗?\n");
printf("取消标记请按1,不取消标记请按0\n");
scanf("%d", &c);
if (c != 0 && c != 1)
{
while (1)
{
printf("无效数字,请重新输入>\n");
scanf("%d", &c);
if (c == 0 || c == 1)
{
break;
}
}
if (c == 1)
{
show[x][y] = '*';
printf("取消标记成功!");
Displayboard(show, ROW, COL);
}
}
}
else
{
printf("此处已排查过,请勿重复输入\n");
}
}
else
{
int ret = getmine(mine,x,y);
show[x][y] = '0' + ret;
int t=moreinformation(show, mine, show[x][y]-'0', x, y);
Displayboard(show, ROW, COL);
sum = victory(show,mine);
}
}
else
{
printf("坐标输入错误,请重新输入\n");
}
}
if (sum == level)
{
printf("恭喜你,排雷成功\n");
Displayboard(mine, ROW, COL);
}
}