接下来我们从零开始设计一个简单的扫雷游戏:
因为经典的扫雷是一个二维平面模型,我们便自然而然的联用到用二维数组来表示我们的扫雷盘,如果要在9*9的棋盘上布置雷的信息和排查雷,我们⾸先想到的就是创建⼀个9*9的数组来存放信息。
假设我们认定,1就是雷,0就是无雷,当我们随机布置好棋盘时,便会发现:
当我们排查(4,6)这个坐标时,可以通过周边八个格子看出有两个雷在其附近,但当我们排查(8,6)这个坐标时,发现有三个格子是已经超出了边界外的,因此为了防止越界,我们可以通过增大数组长度,向外扩大一圈,但雷阵还是会布置在中间9*9的棋盘内,这样我们就可以解决越界的问题了
当我们排查某处周围是否有雷时,我们需要将排查出的雷的数量信息记录存储,并打印出来。但1是雷,0是非雷,打印出来周围雷的数量也是数字,那么这样会不会造成干扰呢?
我们这里便采用创建两个数组的方式避免这些问题的出现,一个数组储存雷的信息,另一个数组打印出来,展示给玩家看雷的数量的信息。
定义两个数组,一个是mine,一个是show。我们把雷布置到mine数组,在mine数组中排查雷,排查出的数据存放在show数组,并且我们可以随时打印show数组来检测参考。同时为了保持神秘,show数组开始时初始化为字符'*',为了保持两个数组的类型⼀致,可以使用同⼀套函数处理,mine数组最开始也初始化为字符'0',布置雷改成'1',于是我们用char定义两个数组。
为了实现模拟未来的多文件的各种代码,我们这里也用多文件来尝试:所以我们再新建两个文源件,一个头文件
首先,我们要创建一个源文件来对游戏运行进行测试,名字test.c,并创建一个源文件game.c用来书写游戏中需要有运用到的函数。再创建头文件game.h来对这些函数以及所需要用到的数据进行声明:
现在正式开始实现我们的扫雷游戏代码了。
首先第一步我们想到做一个菜单,让玩家选择开始游戏或者是退出游戏,我们决定在控制台上一开始打印一个菜单,为了方便,就用do-while循环,并选择声明一个函数menu专门负责打印菜单。
在此之前,我们需要声明所需用到的头文件,为了方便,我们把对系统头文件的声明统一放在game.h中,这样一来,我们两个源文件便只需要声明头文件game.h就可以了。
我们定义了一个变量input用来接受玩家的指令,当玩家输入1时,开始游戏,当玩家输入0时,退出游戏,结束循环,因此我们还需要通过在主函数中使用条件语句来进行不同选择的执行。
因为输入的input是整形,我们就使用switch语句比较好。
接下来开始写不同语句所需执行的代码:首先是case 1的情况,我们要写一个game函数在test.c中代表游戏的主体。
当我们进入游戏后,首先应该是先有一个让玩家扫雷的棋盘,我们便把一开始的二维数组放在game函数里定义,为了方便后续调整,我们可以在game.h中用#define 定义ROW行为9,COL列为9,这样就可以通过对#define ROW 9的调整,省略后续一连片的值的修改。
除此之外,由于我们还需要一个大一圈的数组,于是可以定义ROWS为ROW+2,COLS为COL+2。
定义好二维数组后,我们就可以初始化棋盘,按照一开始的想法,我们给展示给玩家看的数组show赋值为'*',给存储雷的信息的数组mine赋值为'0'。
于是我们在game.c中设定一个函数InitBoard来实行初始化功能,于是我们把数组,数组的行,列,以及想要初始化的字符传参给函数InitBoard,并将此函数在game.h中声明。
当我们在game()函数中初始化了两个数组后,我们就想着打印一下看看是不是如我们所期待的一样,于是我们决定声明一个DisplayBoard函数来打印棋盘(后续函数的声明与定义没有特殊说明都指在game.c中定义,game.h中声明)
并且,我们先运行一下看看效果:
若是把这个棋盘展示给玩家,难免会让人看得头晕眼花,我们是不是可以通过在旁边写上横纵坐标来帮助玩家来分辨坐标呢?
那我们便改变一下DisplayBoard的代码:
让其打印出来带有坐标:
。
打印函数DisplayBoard写完后,我们便考虑如何随机分配雷阵了,以9*9的雷阵中有十个雷为例,如何让这十个雷随机分配呢?
我们想起了时间戳的概念,借助rand函数来随机横纵坐标,由此来随机分配地雷:
于是我们写下随机布雷函数:
由于我们在这里使用了rand,所以我们需要用srand来随机起点:
布置好雷之后,我们如何取数所查坐标周围的地雷个数呢?
由于在mine数字上打印店是字符'1'或'0',字符使用ASCII码值表示,倘若我们用周围八个字符的ASCII码值之和减去八个字符'0'的ASCII码值之和,那所得到的不正是地雷个数吗?
于是我们写下GetMineCount函数来算出周围的地雷数量。
接下来,我们需要一个函数来实现玩家对雷寻找过程,比如让玩家输入一个坐标,查找其周围是否含有雷,都需要一个函数来实现,于是我们可以定义一个FindMine函数:
这样子,我们扫雷小游戏的初步代码就写完了,但大家可以发现,这样的游戏显然是太简陋了一点,下篇博客就带大家优化改善一下小游戏吧!!
这是game.h:
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
void menu();
void game();
void InitBoard(char arr[ROWS][COLS], int rows, int cols, char set); //row是行数,col是列数,set是所要打印的字符
void DisplayBoard(char arr[ROWS][COLS], int row, int col); //row是行数,col是列数
void SetMine(char arr[ROWS][COLS], int row, int col);//row是行数,col是列数
int GetMineCount(char mine[ROWS][COLS],int x, int y);//x,y是所求地址的横纵坐标,arr是mine数组
void FindMine(int row, int col, char show[ROWS][COLS], char mine[ROWS][COLS]);//这里函数的形参数组与实参一样是为了更好说明
//哪个数组在哪个模块被使用
game.c
#include"game.h"
void menu()
{
printf("*********************************\n");
printf("**********1:开始游戏*************\n");
printf("*********************************\n");
printf("**********0:退出游戏*************\n");
printf("*********************************\n");
}
void game()
{
//用来储存摆放的雷的信息
char mine[ROWS][COLS] = { 0 };
//用来储存查找出的雷的信息
char show[ROWS][COLS] = { 0 };
//初始化mine数组
InitBoard(mine, ROWS, COLS, '0');
//初始化show数组
InitBoard(show, ROWS, COLS, '*');
//打印show数组
DisplayBoard(show, ROW, COL);
//随机分配地雷
SetMine(mine, ROW, COL);
//正式开始排查雷
FindMine(ROW, COL, show, mine);
}
void InitBoard(char arr[ROWS][COLS], int rows, int cols, char set)
{
for (int i = 0; i < rows; i++)
{
int j = 0;
for (j = 0; j < cols; j++)
{
arr[i][j] = set;
}
}
}
void DisplayBoard(char arr[ROWS][COLS], int row, int col)
{
for (int x = 0; x <= col; x++)//打印出第一行的0到9十个数字作为列号
{
printf("%d ", x);
}
printf("\n");//打印换行
for (int i = 1; i <= row; i++)
{
int j = 1;
printf("%d ", i);//在每一列开始前打印行号
for (j = 1; j <= col; j++)
{
printf("%c ", arr[i][j]);
}
printf("\n");
}
}
void SetMine(char arr[ROWS][COLS], int row, int col)
{
int count = 10;
while (count)//每一次布置雷,count便会减一,布置十次后count=0,while循环结束
{
int i = rand() % 10 + 1;
int j = rand() % 10 + 1;
if (arr[i][j] == '0')//为了防止在同一位置布置多次
{
arr[i][j] = '1';//布置地雷
count--;
}
}
}
int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
int sum = 0;
for (int i = x - 1; i <= x + 1; i++)
{
int j = 0;
for (j = y - 1; j <= y + 1; j++)
{
sum += mine[i][j];//算出九个位置ASCII码值之和
}
}
return sum - 9 * '0';//减去九个字符0的ASCII码值就等于周围雷数
}
void FindMine(int row, int col, char show[ROWS][COLS], char mine[ROWS][COLS])
{
int x = 0;//代表玩家输入的横坐标
int y = 0;//代表玩家输入的纵坐标
int win = 0;//代表玩家的胜利指标,当win的值等于所有的空格子时,玩家胜利
while (win < (ROW* COL - 10))//当玩家没有猜中雷的时候,游戏会一直进行,所以使用while循环
{
printf("请输入你要查验的坐标(x,y)\n");
scanf("%d,%d", &x, &y);
if (x >= 0 && x <= row && y >= 0 && y <= col)
{
if (mine[x][y] == '1')//'1'代表是地雷,当数组的值等于'1'的时候,说明中雷了
{
printf("不好意思,你被炸死了,游戏失败!\n");
DisplayBoard(mine, ROW, COL);//游戏结束后给玩家展示所有的地雷
break;//踩中雷了,break结束循环。
}
else
{
int num = GetMineCount(mine, x, y);//如果不是中雷,则用num等于周围雷数
show[x][y] = num + '0';//由于是char类型数组,num是整形,一个整数加上字符0等于该整数的ASCII码值,打印出来则是该字符整数
DisplayBoard(show, ROW, COL);//打印被展示的棋盘show
win++;//每次猜对一个格子,win便会增加1
}
}
else
{
printf("坐标非法,请重新输入:\n");
}
}
}
test.c
#include"game.h"
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请选择:>\n");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择无效,请重新输入\n");
break;
}
} while (input);
return 0;
}