今天我们来讲讲C语言初阶较为重要的一个小实验:扫雷游戏。这个游戏运用了很多的函数以及循环分支语句,因此对于新手训练C语言是非常友好的。下面我们就一起来看看吧!
我们先看看程序构成。我们所要写出来的程序一定要根据功能来写,因此,我们按照如下思维导图的递进一点点的完成这段代码。
首先最重要的就是程序主体,我们再写程序的时候一定不能上来就写我们需要的函数,而是在程序主体的完善过程中一点点理解我们需要什么函数,需要什么功能,再去完成函数。为了方便写主程序以及函数,我们同时建立test.c、game.c和game.h三个文件。game.c用来编写我们的函数代码,game.h用来声明函数,而主程序test.c包含game.h程序即可使用我们自己定义的函数库。
为了实现这个游戏,我们需要两个字符数组。一个用来存储我们的雷(mine),另外一个展示给游玩者(show)。因此我们需要将show数组全部统一初始化,在游玩者游玩时,根据mine数组来确定周围雷的个数,再替换掉show数组相应的元素即可是先扫雷过程。
介于游戏规则,我们会在游戏时扫描坐标周围九宫格内雷的个数,但是当坐标位于上下左右边界时,没有完整的九宫格会产生越界,因此我们可以将大小换到11*11,但是有雷的地方仅仅限制于9*9的范围内。
话不多说,我们开始:
一、test.c文件
(一)程序主体设计
作为一个游戏,菜单当然是必不可少的,因此这必然会出现在我们的主程序中。对于C语言初学者来说,想要实现鼠标控制并不容易,因此我们使用键盘输入,也就是scanf函数来输入菜单选项。因此我们可以把play选项设置为1,exit选项设置为0。对于我们不同的输入,我们要给出不同的判断,因此我们在菜单后使用分支语句来判断。由于我们所需输入的只有数字,因此我们可以使用较为简单的switch语句,并且在其中的play分支后要运行我们的游戏程序。因此,一个大框架出来了,如下:
int main()
{ int play_or_not = 0;
do
{
menu();//这里是菜单函数
printf("请选择");
scanf("%d", &play_or_not);
switch (play_or_not)
{
case 1:
printf("--------扫雷--------\n");
play();//这里是游戏函数
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,请重新选择\n");
break;
}
} while (play_or_not);
return 0;
}
这段一定是写在test.c文件中作为我们的主程序的,我们的主程序中存在play()和menu()两个函数,那么这两个函数我们也放在主程序中,其他一律写在我们自己的函数库,即game.c文件中。
(二)menu函数
菜单界面我们只需要打印文字即可,因为在我们的主程序中已经有了scanf函数,这里边不需要输入了,只需要打印说明就行,因此我们直接这样写:
void menu()//这里我们只需要打印,不需要返回值,所以void
{
printf("******************\n");
printf("******1.play******\n");
printf("******2.exit******\n");
printf("******************\n");
}
这样我们的菜单就做好了,当然仅供参考,友友们有更好看的图案也可以加上去!
(三)play函数
play函数就是我们的游戏函数,是最重要的函数,当然了,我们不可能一口气把这个函数里面所有的函数写完,例如数组,初始化,打印,扫雷,再打印,我们一定会分多个模块完成,因此我们可以先给出play函数的主体:
void play()
{
char show[][] = { 0 };
char mine[][] = { 0 };
chushihua();
chushihua();
buzhi();
dayin();
saolei();
//dayin();
}
为了大家看得懂,我直接把函数命名为了汉语拼音。mine数组是我们的雷盘数组,是不需要展示给游戏者看的,但是大家可以加在后面看看雷盘是否设置成功了,使用后再注释掉即可。上图中左右的黄色字体均是我们自己编写的函数,分别是:
- chushihua:初始化数组,将棋盘中的字符统一
- buzhi:即布置棋盘,把随机的雷埋入我们的数组中,原理就是替换数组中的数字
- dayin:即打印,打印出我们的雷盘,让游玩者选择
- saolei:即扫雷,是play函数中的最重要的主体函数
下面我们就可以分段设计我们所需要的函数了。
二、game.c文件
game.c文件作为我们的函数库的定义文件,我们需要讲上文的四个函数写入其中。
(一)一些说明
- 在我们的程序中,我们会用到一些数,例如二维数组的行、列,以及雷的个数,如果我们将其写入程序中,当我们需要修改数值时便需要从头到尾修改,非常的不方便。因此我们把我们需要的熟悉定义好,作为全局变量代入程序中,因此我们可以将我们的变量放在game.h文件中。我们定义ROW和COL作为棋盘的大小,ROWS和COLS作为数组的列和行,具体原因上问题到过,这里就不再赘述。同时,雷的个数也是可变量,我们设为leidegeshu,也放入game.h文件中。
- 为什么不用整型数组而用字符数组,是因为我们所需展现出来的棋盘数字形式不易游玩,换成一些符号更容易些。
(二)chushihua函数
要想把我们的数组初始化,在现阶段我们只能用for循环来解决,因此show数组可以这样初始化:
void chushihua(char arr[ROWS][COLS])
{
for (int i = 0;i < ROWS;i++)
{
for (int j = 0;j < COLS;j++)
{
arr[i][j] = '*';
}
}
}
但是问题来了,我们需要把mine数组初始化成字符0、1,而这个函数只能初始化成“*”,那我们为什么不把初始化的形式变为变量呢?如下:
void chushihua(char arr[ROWS][COLS],char set)
{
for (int i = 0;i < ROWS;i++)
{
for (int j = 0;j < COLS;j++)
{
arr[i][j] = set;
}
}
}
这样两个数组我们都可以初始化,这时候我们的play函数已经完成了一部分了:
void play()
{
char show[ROWS][COLS] = { 0 };
char mine[ROWS][COLS] = { 0 };
chushihua(show, '*');
chushihua(mine, '0');
buzhi();
dayin();
saolei();
}
(三)buzhi函数
既然是随机的布置雷,我们肯定要用到随机数,而随机数的产生最简单的就是利用时间指针。因此我们可以以时间为种子产生随机数,每布置一个雷,计数就减少一个,因此我们可以用do-while循环。如下:
void buzhi(char arr[ROWS][COLS],int row, int col)
{
srand((unsigned int)time(NULL));
int count = leidegeshu;
do
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (arr[x][y] = '0')
{
arr[x][y] = '1';
count--;
}
} while (count);
}
(四)dayin函数
这一步的目的是将棋盘打印在屏幕上,供游玩者读取。我们需要挨个打印,同时需要注意换行
这样我们就可以写出来以下代码:
void dayin(char arr[ROWS][COLS],int row,int col)
{
int i = 0;
for (i = 1;i <= row;i++)
{
int j = 0;
for (j = 1;j <= col;j++)
{
printf("%c ", arr[i][j]);
}
printf("\n");
}
}
但是再看看我们的运行结果,确实打印出来了但是没有行号和列号,这让游玩者寻找坐标很困难。因此我们可以加上行号和列号:
void dayin(char arr[ROWS][COLS],int row,int col)
{
int i = 0;
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 ", arr[i][j]);
}
printf("\n");
}
}
这样我们的打印函数就完成了。
(五)saolei函数
即扫雷函数。当已经输入的坐标数量小于不是雷的坐标数量时,就可以进入扫雷程序,因此我们需要一个if语句:
void saolei(char mine[ROWS][COLS], char show[ROWS][COLS])
{
int x = 0;
int y = 0;
int z = 0;
while (z < ROW * COL - leidegeshu)
{
printf("请输入坐标:\n");
scanf("%d %d", &x, &y);
if (x <= ROW && x > 0 && y <= COL && y > 0)
{
if (mine[x][y] == '1')
{
printf("很抱歉你被炸死了\n");
dayin(mine, ROW, COL);
break;
}
else
{
int count = geshu(mine, x, y);
show[x][y] = count + '0';
dayin(show, ROW, COL);
z++;
}
}
else
{
printf("非法的坐标,请重新输入\n");
}
}
}
那如果真的出现了一个很厉害的人,把所有的雷都排除掉了,这个时候我们就需要另外一种可能了:
void saolei(char mine[ROWS][COLS], char show[ROWS][COLS])
{
int x = 0;
int y = 0;
int z = 0;
while (z < ROW * COL - leidegeshu)
{
printf("请输入坐标:\n");
scanf("%d %d", &x, &y);
if (x <= ROW && x > 0 && y <= COL && y > 0)
{
if (mine[x][y] == '1')
{
printf("很抱歉你被炸死了\n");
dayin(mine, ROW, COL);
break;
}
else
{
int count = geshu(mine, x, y);
show[x][y] = count + '0';
dayin(show, ROW, COL);
z++;
}
}
else
{
printf("非法的坐标,请重新输入\n");
}
}
if (z == ROW * COL - leidegeshu)
{
printf("恭喜你,排雷成功\n");
dayin(mine, ROW, COL);
}
这样我们可以提醒他排雷成功了,那如果玩家玩到一半想退出怎么办,那这里我们可以给出另一个分支:
void saolei(char mine[ROWS][COLS], char show[ROWS][COLS])
{
int x = 0;
int y = 0;
int z = 0;
while (z < ROW * COL - leidegeshu)
{
printf("请输入坐标:\n");
scanf("%d %d", &x, &y);
if (x <= ROW && x > 0 && y <= COL && y > 0)
{
if (mine[x][y] == '1')
{
printf("很抱歉你被炸死了\n");
dayin(mine, ROW, COL);
break;
}
else
{
int count = geshu(mine, x, y);//统计雷的个数函数
show[x][y] = count + '0';
dayin(show, ROW, COL);
printf("输入00可提前结束哦!\n");
z++;
}
}
else if (x == 0 && y == 0)
{
printf("游戏提前结束\n");
break;
}
else
{
printf("非法的坐标,请重新输入\n");
}
}
if (z == ROW * COL - leidegeshu)
{
printf("恭喜你,排雷成功\n");
dayin(mine, ROW, COL);
}
那么这就是我们saolei函数的完整代码了,是不是很简单呢?在这个函数中还有一个geshu函数,图中已标出,这个函数用来统计这个坐标一圈的九宫格里有多少雷,因此我们还需要写一个geshu函数。
(六)geshu函数
由于数组是字符数组,直接相加并不可取。但是注意到,字符1的ASCII码值是50,字符0的ASCII码值是49,二者想减正好是1。因此我们可以将九宫格内的每个字符都与字符0想减,所得的值再相加就是最终的雷的个数。如下:
int geshu(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');
}
当然,这里也可以用for循环,留给小伙伴们自己考虑,这里便不再赘述。至此,我们的代码已经基本上全部完成了。
三、game.h文件
我们在game.c中写的函数定义需要在game.h文件中声明才可以使用,加上上文所说的全局变量,我们的game.h文件:
#define _CRT_SECURE_NO_WARNINGS 1
#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 leidegeshu 10
void chushihua(char arr[ROWS][COLS], char set);
void dayin(char arr[ROWS][COLS], int row, int col);
void buzhi(char arr[ROWS][COLS], int row, int col);
int geshu(char arr[ROWS][COLS], int x, int y);
void saolei(char arr1[ROWS][COLS], char arr2[ROWS][COLS]);
同时,由于我们在game.h文件中包含了一些头文件,因此test.c文件中便不再需要包含该头文件。此外还要注意我们自己编写的函数库需要用双引号引用。
四、成文
(一)test.c文件
#include"game.h"
void play()
{
char show[ROWS][COLS] = { 0 };
char mine[ROWS][COLS] = { 0 };
chushihua(show, '*');
chushihua(mine, '0');
buzhi(mine, ROW, COL);
dayin(show, ROW, COL);
saolei(mine,show);
}
void menu()
{
printf("******************\n");
printf("******1.play******\n");
printf("******2.exit******\n");
printf("******************\n");
}
int main()
{ int play_or_not = 0;
do
{
menu();//这里是菜单函数
printf("请选择");
scanf("%d", &play_or_not);
switch (play_or_not)
{
case 1:
printf("--------扫雷--------\n");
play();//这里是游戏函数
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,请重新选择\n");
break;
}
} while (play_or_not);
return 0;
}
(二)game.c文件
#include"game.h"
//数组初始化
void chushihua(char arr[ROWS][COLS],char set)
{
for (int i = 0;i < ROWS;i++)
{
for (int j = 0;j < COLS;j++)
{
arr[i][j] = set;
}
}
}
//打印棋盘
void dayin(char arr[ROWS][COLS],int row,int col)
{
int i = 0;
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 ", arr[i][j]);
}
printf("\n");
}
}
//布置雷
void buzhi(char arr[ROWS][COLS],int row, int col)
{
srand((unsigned int)time(NULL));
int count = leidegeshu;
do
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (arr[x][y] = '0')
{
arr[x][y] = '1';
count--;
}
} while (count);
}
//扫雷
void saolei(char mine[ROWS][COLS], char show[ROWS][COLS])
{
int x = 0;
int y = 0;
int z = 0;
while (z < ROW * COL - leidegeshu)
{
printf("请输入坐标:\n");
scanf("%d %d", &x, &y);
if (x <= ROW && x > 0 && y <= COL && y > 0)
{
if (mine[x][y] == '1')
{
printf("很抱歉你被炸死了\n");
dayin(mine, ROW, COL);
break;
}
else
{
int count = geshu(mine, x, y);
show[x][y] = count + '0';
dayin(show, ROW, COL);
printf("输入00可提前结束哦!\n");
z++;
}
}
else if (x == 0 && y == 0)
{
printf("游戏提前结束\n");
break;
}
else
{
printf("非法的坐标,请重新输入\n");
}
}
if (z == ROW * COL - leidegeshu)
{
printf("恭喜你,排雷成功\n");
dayin(mine, ROW, COL);
}
}
//统计雷的个数
int geshu(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');
}
(三)game.h文件
#define _CRT_SECURE_NO_WARNINGS 1
#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 leidegeshu 10
void chushihua(char arr[ROWS][COLS], char set);
void dayin(char arr[ROWS][COLS], int row, int col);
void buzhi(char arr[ROWS][COLS], int row, int col);
int geshu(char arr[ROWS][COLS], int x, int y);
void saolei(char arr1[ROWS][COLS], char arr2[ROWS][COLS]);
好了,我们本期的分享到这里就结束了,友友们都学会了吗?没能理解也不要紧,大家加油!我们下期再见!
附上扫雷的网页版:扫雷游戏网页版 - Minesweeper