学习了数组和函数之后,我们来讲一个扫雷小游戏。
本次作者第一次使用画图软件,奇丑无比,还请见谅。
目录
1.扫雷游戏的设计分析
1.1扫雷的功能说明
大家在小时候肯定都玩过扫雷游戏,所以它的玩法这里不过多介绍。
首先,大家在玩游戏的时候一开始肯定是一个菜单界面,在菜单里可以选择难度,开始游戏或退出游戏,所以我们在写扫雷时候也可以从菜单开始入手。
其次,我们要做出扫雷的这样一个机制,尽可能去还原:
//测试文件 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");
}
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);
return 0;
}
我们通过对input赋值来达到对菜单各功能的执行,input值为1时,开始游戏,进入game函数。input值为0时,退出游戏,而input被赋于其他值时,会执行default语句。当一次循环结束后,会根据input的值来判断是否再次进入循环,input不为0时,判断条件为真,进入循环,而input为0时,判断条件为假,离开循环,这也就达到了input输入0时退出游戏的目的。
2.扫雷的具体实现
2.1 扫雷的数据结构
因为在扫雷游戏中我们需要进行扫雷,标记一系列操作,所以需要数据结构对这些数据进行存储,因为我们需要在9*9的棋盘上布置雷的信息和排查雷,我们⾸先想到的就是创建⼀个9*9的数组来存放信息。
对于 这样一个9*9的数组,我们可以让为雷的地方为1,不为雷的地方为0。
比如说我们在(2,5)这一点周围雷的个数为2,所以我们排查这一点后应将这一点赋值为2,
因为我们在扫雷的时候需要看这一点周围的雷的个数,所以边界点会产生越界问题,这一问题可以通过改变边界点扫雷的方式来解决,也可以将数组扩大一周,也就是创建一个11*11的数组,但最外层全赋值为0,这样不会对内层的扫雷产生影响。
且如果我们扫雷时直接将数据存放在布置雷的数组中,这样雷的信息和雷的个数信息就可能或产⽣混淆和打印上的困难。我们不妨重新创建一个数组专门用来打印每次扫雷后的结果。
对于放置雷的数组,在放置雷之前,我们要初始化;对于打印每次扫雷结果的数组,我们也要进行初始化,所以我们可以写一个初始化的代码来对这两个数组进行初始化。
2.2 扫雷的文件结构设计
在扫雷代码中,我们设置三个文件,分别是:
1. game.c作为扫雷游戏的源文件,用来写扫雷函数的实现
2. game.h作为扫雷游戏的头文件,来写扫雷需要的函数声明和数据类型
3. teat.c作为测试文件,用来测试扫雷
2.3 数组的初始化函数
这两个数组需要在测试文件test.c里创建,初始化函数的实现应写在game.c中,该函数声明应写在game.h中。当然对于一个扫雷游戏而言,它的棋盘不一定一直都是9*9的,如果我们在创建数组的时候,直接使用9来创建,那么在棋盘大小改变时是很难修改的,所以我们可以使用#define来定义常量,这样在修改时只需要修改常量值即可达到目的。
因为有一个数组是专门用来存放每次扫雷后的信息的,但为了一开始的神秘感,我们将其初始化为*,这也是棋盘数组类型为char的原因之一。
game.h
#pragma once
#include<stdio.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
game.c
#include"game.h"
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;
}
}
}
test.c
#include"game.h"
game()
{
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
initboard(mine,ROWS,COLS,'0'); //对于初始化函数,起名为initboard
initboard(show,ROWS,COLS,'*');
}
在test.c文件和game.c文件中,需要引用game.h文件才能使用其声明的函数。
在game.h中,对棋盘大小和应该创建的数组大小进行了常量定义,在创建数组时使用的行和列与在扫雷操作时使用的行和列不同。
在game.c中,函数的四个参数分别为:要初始化的数组,数组的行数,列数,初始化的值。
2.4 打印棋盘函数
我们需要打印棋盘函数来对我们每次扫雷后的数组进行打印,这样可以对现有的扫雷情况进行观察。具体代码如下:
//game.h
void displayboard(char board[ROWS][COLS], int row, int col);
//game.c
void displayboard(char board[ROWS][COLS], int row, int col)
{
int i=0;
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");
}
printf("\n");
}
//test.c
game()
{
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
initboard(mine,ROWS,COLS,'0');
initboard(show,ROWS,COLS,'*');
displayboard(show, ROW, COL);
displayboard(mine, ROW, COL);
}
在game.c中,先打印列数,后将行数每行的数据一同打印出来
在test.c中,分别打印两个数组,也可以检验初始化是否正确
2.5 布置雷
打印棋盘也搞定了,接下来就是扫雷的一大难题,布置雷。
我们在布置雷时,每局游戏布置雷的位置肯定不能相同,所以这个雷不应该由人手动来布置。下面提到一个rand函数,它可以提供出随机的数字。
#include<stdio.h>
#include<stdlib.h>
int main()
{
int i=0,j=0;
for(i=0;i<5;i++)
{
j=rand();
printf("%d",j);
}
}
其中,rand()函数需要引用头文件<stdlib.h>,rand()的功能就是输出一个大小在0-32767的数字,但是我们执行时可以发现,我们每次执行时rand()输出的数字都是相同的,所以rand函数只是在程序单词执行时达到了随机的目的,但根本上还是没能解决我们的问题。
搜索后发现,rand()函数能输出随机值是因为它需要一个种子,由于在人为不改变的前提下,每次执行程序的时候这个种子是不会改变的,所以每次执行程序时才会输出相同的值。
现在,我们来介绍另一个函数,也就是rand()函数的兄弟srand(),当srand()的参数改变时,也就是改变了rand()的种子,rand()每次输出的值就会不同。可是我们为了输出随机值,在调整rand()的种子时也需要一个随机值,那么又该怎么办呢?
我们最后介绍一个在布置雷函数时使用到的函数time()函数。这个函数返回值是当前时间到1970 年 1 月 1 日 00:00 UTC 以来的秒数(即当前 unix 时间戳)。我们可以发现,时间是一直在改变的那么时间戳也就是一直在改变的,我们使用时间戳作为rand()的种子,不就可以解决输出随机值的问题了吗?
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
int main()
{
srand((unsigned int)time(NULL));
int i = 0, j = 0;
for (i = 0; i < 5; i++)
{
j = rand();
printf("%d", j);
}
return 0;
}
这样每次代码执行的时候就会输出不同的值了。
那么现在进入正题:布置雷
在布置雷的数组中,我们希望将布置雷的数据由0改为1,使用rand()来输出放置雷的坐标,对于雷的坐标(x,y),对应的应该是数组的第x+1行和第y+1列,而对于9*9的棋盘来说,坐标最大为(9,9),在数组中对应第10行第10列。我们知道了rand()输出数的范围是0-32767,我们让这个随机数对0取余,会得到一个在0-9的数字,而在此基础上再+1,那么这个随机数的范围就对应0-10,也就是数组上棋盘的位置。
确定了雷的位置,还有一个问题就是:不能多次将同一个位置布置雷,这样可能会使雷的数量减少。所以我们在随机出一个坐标时,应先判断这个坐标的位置上是否已经布置了雷。
这一部分主要说明了在布置雷的时候的逻辑和注意事项,下面是布置雷的代码:
//game.h
#define Easy 10
void setmine(char board[ROWS][COLS], int row, int col);
//game.c
void setmine(char board[ROWS][COLS], int row, int col)
{
int count = Easy;
while (count)
{
int x = rand() % 9 + 1;
int y = rand() % 9 + 1;
if (board[x][y] != '1')
{
board[x][y] = '1';
count--;
}
}
}
//test.c
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(mine, ROW, COL);
}
int main
{
srand((unsigned int)time(NULL));
}
在game.h中,将easy定义为常量,代表简单模式下的雷的数量。
因为有打印棋盘的函数,我们可以将布置好雷的数组打印出来进行检验。
2.6 扫雷
在讲完布置雷后,讲解扫雷的最后一部分,也是最难的一部分,那就是排查雷。
我们知道,排除雷是将排除坐标周围的雷的个数打印到这个坐标上,所以我们要计算排除坐标点周围的雷的个数。
在我们排查的时候,首先要判断的就是我们排查的这个坐标是不是布置雷的坐标,如果是雷的话,则直接结束游戏,如果不是雷的话则进行下一步判断。
如果排查的坐标没有雷,那么只需要将该坐标点周围的雷的个数打印出来,于是我们可以专门写一个函数来计算某一坐标周围的雷的个数。因为数组是字符数组,所以数组中的‘0’和‘1’存放的都是其ASCII码值,0就是48,1就是49,那么我们在计算的时候就需要将该坐标周围8个坐标的值相加再减去8个字符‘0’的值,就是该坐标点周围的雷的个数。
//game.h
int getminecount(char mine[ROWS][COLS], int x, int y);
//game.c
int getminecount(char mine[ROWS][COLS], int x, int y)
{
int i = 0;
int j = 0;
int sum = 0;
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
sum += mine[i][j]-'0';
}
}
return sum;
}
又因为我们需要将雷的个数打印出来,所以该函数将此数字作为返回值返回。
接下来来看排查雷的函数:
//game.h
void findmine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
//game.c
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)
{
printf("请输入要排查的坐标\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
{
int minecount = getminecount(mine, x, y);
show[x][y] = minecount + '0';
displayboard(show, ROW, COL);
win++;
}
}
else
{
printf("输入坐标非法,请重新输入\n");
}
}
}
//test.c
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);
}
在该函数中,创建了一个新的变量win,当成功排查一个坐标时,win的值就会+1,棋盘中未排查的坐标一共有row*col-easy个,win值小于这个值时,循环继续,win值等于这个值时,跳出循环,此时雷全部排查完,此次游戏也就结束了,退出game()函数,继续打印菜单进入下一次游戏。
到这里,整个扫雷的代码就全部介绍完了,整套代码并不难理解,但是对于雷的标记,以及当前所用的时间等等,都没有进行实现,也是一种遗憾,对现在的我来说。
下面是整套代码:
//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
#define Easy 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 board[ROWS][COLS], int row, int col);
void findmine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
int getminecount(char mine[ROWS][COLS], int x, int y);
//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;
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");
}
printf("\n");
}
void setmine(char board[ROWS][COLS], int row, int col)
{
int count = Easy;
while (count)
{
int x = rand() % 9 + 1;
int y = rand() % 9 + 1;
if (board[x][y] != '1')
{
board[x][y] = '1';
count--;
}
}
}
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)
{
printf("请输入要排查的坐标\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
{
int minecount = getminecount(mine, x, y);
show[x][y] = minecount + '0';
displayboard(show, ROW, COL);
win++;
}
}
else
{
printf("输入坐标非法,请重新输入\n");
}
}
}
int getminecount(char mine[ROWS][COLS], int x, int y)
{
int i = 0;
int j = 0;
int sum = 0;
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
sum += mine[i][j]-'0';
}
}
return sum;
}
//test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include"game.h"
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);
}
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);
return 0;
}
那么这次扫雷的代码就介绍到这里了,我们下次再见。