前言
今天来给大家介绍一下十分经典的扫雷游戏,如何从零开始到最后的代码的实现。它综合运用了许多知识,几乎结合了学习递归之前的所有知识,因此对它的综合运用意义非凡。
需要具备的知识
c语言数据类型和变量,分支和循环语句,数组,函数,数据结构基础......
代码展示
我的思路是用三个文件来对此次任务的实现,包括了一个头文件和两个源文件。
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);
game.c(函数的实现)
#define _CRT_SECURE_NO_WARNINGS 1
#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 <= row; i++)
{
printf("%d ", i);
}
printf("\n");
for (i = 1; i <= row; i++)
{
int j = 0;
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 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] +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';
}
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
{
if (show[x][y] != '*')
{
printf("该坐标已经被排查过了,无需再排查\n");
}
else
{
int count = GetMineCount(mine, x, y);
show[x][y] = count + '0';
DisplayBoard(show, ROW, COL);
win++;
}
}
}
else
{
printf("输入的坐标非法,请重新输入\n");
}
}
if (win == row * col - EASY_COUNT)
{
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
}
}
test.c(测试逻辑)
#define _CRT_SECURE_NO_WARNINGS 1
#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] = { 0 };
char show[ROWS][COLS] = { 0 };
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
SetMine(mine, ROW, COL);
DisplayBoard(show, ROW, COL);
FindMine(mine, show, ROW, COL);
}
int main()
{
int input = 0;
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;
}
代码实现成果展示
详细解析
扫雷游戏分析和设计
功能说明
• 游戏可以通过菜单实现继续玩或者退出游戏
• 扫雷的棋盘是9*9的格子
• 默认随机布置10个雷 (1表示雷,0表示非雷)
• 可以排查雷 (如果位置不是雷,就显示周围有几个雷;如果位置是雷,就炸死游戏结束;把除10个雷之外的所有非雷都找出来,排雷成功,游戏结束。)
• 以输入坐标的形式来确定自己想要排查的位置(这个是因为按现在的水平实现鼠标点击的难度还是太大了,因此就选择这种方式就好了)
游戏实践分析
第一步
我们所知的是1表示雷、0表示非雷,然后是9*9的格子,这是我们设计的基础,于c语言内部而言上述中不管是雷还是非雷,都是信息的范畴,因此我们需要用到数据结构来存储这些信息,由于是9*9的格子,我们第一反应是创建一个9*9的数组,但是真的是要创建9*9的数组吗?在第一步先留下这个疑问,后续会为大家解答。
第二步
基于第一步,我们对于目标的设置逐渐明确了,接下来就是一些细节方面的分析。第一,回答一下第一步的那个问题:我们的功能中有一项是如果排查的不是雷,就要显示周围有几个雷。举个例子:如果我们只设置9*9的数组的话,它是会打印出我们想要的9*9的扫雷格子,但是如果我们想排查的是边缘的格子呢?它如何知道周围的雷有多少,如下图,加入我查了边缘的地方(假如是图二中圈起来的那个),那么如何得知它周围有几个雷(主要是最右边的一列我们无法得知,那样属于越界访问,得知不了它周围有几个雷)当然我们是可以通过分支与循环语句再写一些程序对于这个情况进行一个讨论,但是那样的话不仅工作量会加大,而且程序也不是那么简洁,其实解决这个问题很简单,那就是我们引入一个11*11的数组,然后打印的时候只打印9*9的格子,然后将9*9以外的地方都设置成非雷(0),这样的话就简化了许多。(如图3)
第二,解决了数组设置的问题,现在来看看具体的设计问题,摆在面前的有一个重大问题就是如何更好地去实现我们的功能,比如实现初始化,以及排查雷之后,如果是雷还好说就直接爆了;如果不是雷,那么如何显示它周围有多少雷,于是我们想到了我们可以设置两个格子,一个是mine,负责存放我们已经随机生成好了的雷;另一个是show,负责存放排查出的雷的信息,并且打印出来以便于给下次猜测提示,也就是说打印的话,我们只会打印show那个数组格子。再者为了不让玩游戏的人看出来雷的位置,在排雷之前每个位置应该用特殊字符表示(“*”),然后猜测一个显示一个。这是我们的基本思路,设置两个格子的好处就是可以防止雷的信息在被猜测的前后混淆。
细节代码分析
game.h
对于game.h文件里面的内容没有太多需要设计的,它主要会包含数据类型和函数声明的内容,需要注意的是传参的参数要写完以及返回类型应该写void,因为我们不需要它返回什么值,只需要它执行相关指令就好了。
game.c
在game.c的文件中,它注意进行的是函数的实现,现在来逐个进行思路分析:
上图中,它是对于格子的一个初始化。
上图中,它是对于格子的打印。采用了for循环,这两个地方都使用了循环的嵌套。
这个代码的实施只有一个目的就是布置雷。接下来,我们关于上图中的1和2进行一个讲解,已知xy是用来表示雷的位置的,1(为什么不把count--放在while的括号后面?):因为我们设置的基本理念,当安排了一个雷之后,count才减一次,但是如果放在括号里面的话,意思就是不管安放雷的位置,每执行一次之后count都减一次,那么这就造成了一个问题就是由于x、y都是随机的,假如第一次设置的x、y的位置和第二次设置的位置是一样的,那么它依然会减一,这就导致了有可能实际设置的雷的个数小于10。2(x、y初始化解释):根据雷位置应该是随机生成的这个设计理念,x、y的位置应该是随机的,但是xy是有范围的,rand()%row/col之后,它的取值范围是0到8,但是最后为什么要+1呢,因为我们设置的是11*11数组,但是最后打印的是9*9数组,因此换算过来之后实际上9*9中的对应的第一个数字的下标就不再是(0,0)而是(1,1)了,因此需要+1。
这一步的目的是确定我们想排查的坐标周围的雷的数量,但是有一点需要注意的就是,我们向每个格子里面输入的根本上是字符,但是我们需要返回的其实是数字大小,这个时候怎么办呢?其实如果去翻ASCII码表的话,我们可以发现一个很有趣的事情,就是‘数字’的ASCII码减去‘0’的ASCII码,因此我们只需要把我们所排查的坐标的字符加起来然后减去8个‘0’的ASCII码就能实现我们的目的了。
这一步就是说基本的排查实践了,可以轻松看懂的,就不做过多的解释了。
test.c
test.c没太多解释的,就是一整个测试逻辑,上图打圈的是为了引入随机的x和y,这个创建随机数的办法挺实用的,它主要采用了时间戳的原理,是将此刻时间与1970年01月01日00时00分00秒做差。
小结
以上操作就是生成扫雷游戏的全部思路了,如果有不懂的,可以私信或者评论告诉小编,我也一定会为大家解答的!