今天我们来讲讲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("--------扫雷-------- "); play();//这里是游戏函数 break; case 0: printf("退出游戏 "); break; default: printf("选择错误,请重新选择 "); break; } } while (play_or_not); return 0; }
这段一定是写在test.c文件中作为我们的主程序的,我们的主程序中存在play()和menu()两个函数,那么这两个函数我们也放在主程序中,其他一律写在我们自己的函数库,即game.c文件中。
(二)menu函数
菜单界面我们只需要打印文字即可,因为在我们的主程序中已经有了scanf函数,这里边不需要输入了,只需要打印说明就行,因此我们直接这样写:
void menu()//这里我们只需要打印,不需要返回值,所以void { printf("****************** "); printf("******1.play****** "); printf("******2.exit****** "); printf("****************** "); }
这样我们的菜单就做好了,当然仅供参考,友友们有更好看的图案也可以加上去!
(三)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(" "); } }
但是再看看我们的运行结果,确实打印出来了但是没有行号和列号,这让游玩者寻找坐标很困难。因此我们可以加上行号和列号:
void dayin(char arr[ROWS][COLS],int row,int col) { int i = 0; for (i = 0;i <= col;i++) { printf("%d ", i);//打印列 } printf(" "); for (i = 1;i <= row;i++) { printf("%d ", i);//打印行 int j = 0; for (j = 1;j <= col;j++) { printf("%c ", arr[i][j]); } printf(" "); } }
这样我们的打印函数就完成了。
(五)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("请输入坐标: "); scanf("%d %d", &x, &y); if (x <= ROW && x > 0 && y <= COL && y > 0) { if (mine[x][y] == '1') { printf("很抱歉你被炸死了 "); dayin(mine, ROW, COL); break; } else { int count = geshu(mine, x, y); show[x][y] = count + '0'; dayin(show, ROW, COL); z++; } } else { printf("非法的坐标,请重新输入 "); } } }
那如果真的出现了一个很厉害的人,把所有的雷都排除掉了,这个时候我们就需要另外一种可能了:
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("请输入坐标: "); scanf("%d %d", &x, &y); if (x <= ROW && x > 0 && y <= COL && y > 0) { if (mine[x][y] == '1') { printf("很抱歉你被炸死了 "); dayin(mine, ROW, COL); break; } else { int count = geshu(mine, x, y); show[x][y] = count + '0'; dayin(show, ROW, COL); z++; } } else { printf("非法的坐标,请重新输入 "); } } if (z == ROW * COL - leidegeshu) { printf("恭喜你,排雷成功 "); 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("请输入坐标: "); scanf("%d %d", &x, &y); if (x <= ROW && x > 0 && y <= COL && y > 0) { if (mine[x][y] == '1') { printf("很抱歉你被炸死了 "); dayin(mine, ROW, COL); break; } else { int count = geshu(mine, x, y);//统计雷的个数函数 show[x][y] = count + '0'; dayin(show, ROW, COL); printf("输入00可提前结束哦! "); z++; } } else if (x == 0 && y == 0) { printf("游戏提前结束 "); break; } else { printf("非法的坐标,请重新输入 "); } } if (z == ROW * COL - leidegeshu) { printf("恭喜你,排雷成功 "); 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("****************** "); printf("******1.play****** "); printf("******2.exit****** "); printf("****************** "); } int main() { int play_or_not = 0; do { menu();//这里是菜单函数 printf("请选择"); scanf("%d", &play_or_not); switch (play_or_not) { case 1: printf("--------扫雷-------- "); play();//这里是游戏函数 break; case 0: printf("退出游戏 "); break; default: printf("选择错误,请重新选择 "); 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(" "); for (i = 1;i <= row;i++) { printf("%d ", i); int j = 0; for (j = 1;j <= col;j++) { printf("%c ", arr[i][j]); } printf(" "); } } //布置雷 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("请输入坐标: "); scanf("%d %d", &x, &y); if (x <= ROW && x > 0 && y <= COL && y > 0) { if (mine[x][y] == '1') { printf("很抱歉你被炸死了 "); dayin(mine, ROW, COL); break; } else { int count = geshu(mine, x, y); show[x][y] = count + '0'; dayin(show, ROW, COL); printf("输入00可提前结束哦! "); z++; } } else if (x == 0 && y == 0) { printf("游戏提前结束 "); break; } else { printf("非法的坐标,请重新输入 "); } } if (z == ROW * COL - leidegeshu) { printf("恭喜你,排雷成功 "); 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