相信大家都曾玩过扫雷这一款经典游戏,那么它是怎么通过C语言实现的呢?其中又有哪些需要注意的点以及有难度的地方呢?话不多说,直接开始。
0.扫雷游戏的功能与分析
这是官网的游戏规则,从游戏规则可以看出,我们要编写的扫雷程序需要满足以下几个要求:
- 首先我们的扫雷游戏可以游玩,但我们没有前端界面,所以我们只能通过控制台进行游戏。
- 我们要在进游戏之前显示一个游戏菜单,以便玩家判断是否游戏。
- 我们的游戏必须可以排查雷,比如显示周围有多少雷。
- 雷的布置必须随机。
以上就是我们的需求,那么怎么实现呢?往下看
首先,我们需要创建三个文件,分别是
- 游戏实现文件
- 游戏逻辑以及游戏页面
- 游戏函数声明及其他
如下图所示:
文件已经创建好了,和GTA6进度一样。(bushi ,接下来就要分析扫雷怎么实现。
首先,我们的游戏要先打印出一个9*9的方格,也就是(棋盘),那么我们就需要一个二维数组来存放信息,比如有雷和无雷,假设我们将有雷标记为1,没雷标记为0,如下:
0 0 0 0 0 1 0 0 0
0 1 0 0 0 0 0 1 0
0 0 0 0 1 0 1 0 0
0 0 0 0 0 0 0 0 0
0 0 0 1 0 0 0 0 0
1 0 0 0 0 0 1 0 0
0 0 0 1 0 0 0 0 0
0 0 0 0 0 1 0 0 0
0 1 0 0 0 0 0 0 0
这就是我们的棋盘,现在开始排雷,我们预期结果是点一格排除周围九格,并显示周围九格一共有几个雷,可是如果是边界的话怎么办呢?如下:
如果我要点开了蓝色方块中的雷,它的周围明显不够9格,这样就会造成数组越界,那么怎么解决?如下,我们只需使数组扩大但不显示即可
* * * * * * * * * * * * *
* * * * * * * * * * * * *
* * 0 0 0 0 0 1 0 0 0 * *
* * 0 1 0 0 0 0 0 1 0 * *
* * 0 0 0 0 1 0 1 0 0 * *
* * 0 0 0 0 0 0 0 0 0 * *
* * 0 0 0 1 0 0 0 0 0 * *
* * 1 0 0 0 0 0 1 0 0 * *
* * 0 0 0 1 0 0 0 0 0 * *
* * 0 0 0 0 0 1 0 0 0 * *
* * 0 1 0 0 0 0 0 0 0 * *
* * * * * * * * * * * * *
* * * * * * * * * * * * *
布置好雷以后,我们要考虑排查的问题,排查雷的数目如果直接放在布置好的棋盘中,会造成与游戏棋盘的干扰,因此我建议将排查的二维数组和游戏界面的二维数组分开放,这样就避免了互相干扰的问题。理论存在,实践开始:
1.打印游戏菜单
打印游戏菜单需要我们知道,首先,游戏是有开始和推出两个选项的,其次,我们的扫雷程序需要做到结束游戏后不需要重新启动程序就能进行下一盘,因此我们需要do while循环,请注意:本代码应包含在test.c文件中
#define _CRT_SECURE_NO_WARNINGS 1
void menu()
{
printf("***********************\n");
printf("****** 1. play *****\n");
printf("****** 0. exit *****\n");
printf("***********************\n");
}
int main()
{
do
{
menu();
printf("请选择:>");
} while ()
return 0;
}
那这就是我们第一步所打印出来的菜单。没什么技术含量,注意一点,menu函数是调用的,小白看不懂也可以写成这样:
#define _CRT_SECURE_NO_WARNINGS 1
int main()
{
do
{ printf("***********************\n");
printf("****** 1. play *****\n");
printf("****** 0. exit *****\n");
printf("***********************\n");
printf("请选择:>");
} while ()
return 0;
}
这两个效果是一样的,我就拿上面作为例子。
2.输入数字选择开始或结束
通过上面菜单的打印我们知道选择1开始,选择0退出,可是就是有人手欠输入别的数字比如6,那么当别人输错数字时,我们应该让他重新输入,那么这个功能怎么实现呢?可一通过if判断来实现,但是因为条件较为简单,且输入都为整形,所以switch语句更适合游戏环境,以下代码就是实现游戏菜单的功能的:
注意:switch判断的条件必须为整形输入
#define _CRT_SECURE_NO_WARNINGS 1
void menu()
{
printf("***********************\n");
printf("****** 1. play *****\n");
printf("****** 0. exit *****\n");
printf("***********************\n");
}
int main()
{
int input = 0;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("tnnd,选择错误,重新选择\n");
break;
}
} while (input);
return 0;
}
需要注意的有两点
为啥这个input放外面,原因是我们的输入值要在整个菜单中应用,包括后面的while语句,因此我们的input应该放在判断外面,这样才能让while也用到input变量。
while中的input是咋来的?跟着语句来分析,当输入非0数时,有两种情况进入游戏或重新输入,这两种情况都不需要跑出循环,但是若是输入0时,只有一种情况就是退出游戏,那这不需要重新输入,因此就跑出循环,因此输入0时while要满足为假,而input刚好符合条件,于是input就可以作为while循环的条件。
3.游戏实现准备工作
上文说我们布置雷和排查雷需要用到两个数组,我们分别将布置雷定为mine,排查雷定为show。下文将用mine和show代替。
需要注意的是,我们初始化游戏时,mine存放的数据都是0,而show存放的都是*,为了打印时方便,我们打印时将mine数组中存放的数据也存放为字符0而不是数字0,C语言中的数字0和字符0是有区别的!详情见ASCII码一览表,ASCII码对照表 (biancheng.net),代码如下:
void game()
{
//数组
char mine[11][11];//'0'
char show[11][11];//'*'
InitBoard(mine, 11, 11, '0');
InitBoard(show, 11, 11, '*');
}
需要 注意的是,
这里的棋盘的存放格式都是字符,不是数字!
但是如果这样的话,如果我们日后要改变游戏难度的话,这样写代码会有牵一发而动全身的感觉,不好改,因此,我们可以将数据放到头文件中,将tese.c文件中的数字变为符号,以上代码都是放到test.c文件中的。具体实现过程如下:
void game()
{
//数组
char mine[ROWS][COLS];//'0'
char show[ROWS][COLS];//'*'
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
}
而头文件中存放的数据则是这样:
#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 InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
可是,这样写代码后你会发现test.c中的函数并不能使用头文件中的声明,原因是test.c文件中没有包含头文件,只需加上#include"game.h"即可,完整test.c代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#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];//'*'
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
}
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;
}
在头文件中包含了声明后,我们终于可以开始游戏的实现了。
4.游戏的实现(正式)
1.数组的初始化
首先,我们需要进行数组的初始化,注意,以下代码都归于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;
}
}
}
以上代码就是数组初始化的方法,首先,注意game.c文件也需要包含头文件
其次,注意
这里的char set是为了传入字符而作的方便写法,怎么方便?看上文的test.c中的初始化,
这里分别传入了字符0和字符*,因此,game.c中只需印用即可。
2.棋盘的打印
首先,我们需要 定义一个打印棋盘的函数,命名为display。我们需要在test.c中将函数传出,并在头文件中声明此函数。
test.c函数如下:
void game()
{
char mine[ROWS][COLS];//'0'
char show[ROWS][COLS];//'*'
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//棋盘打印
//DisplayBoard(mine, ROW, COL);
DisplayBoard(show, ROW, COL);
}
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 InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
//打印棋盘的
void DisplayBoard(char board[ROWS][COLS], int rows, int cols);
game.c中代码如下:
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++)
{
printf("%d ", i);
int j = 0;
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
printf("--------扫雷游戏-------\n");
}
需要注意的是
虽然传入的是ROWS和COLS,但是由于我们打印时并不是11*11,而是9*9,因此我们的int 后面跟的是row 和 col,包括game.c中用的也是row和col。而且不仅名称变化,打印的起始点也有变化,如图:
可以注意到,我们是从1开始打印的,原因如下图:
可以看到,我们的棋盘序号实际上是从1开始的,也就是说我们应该从1打印。而单独打印棋盘并不方便,因为我们是输入坐标扫雷,所以将上图对应的序号打印上是最好的。
上图是打印序号的函数,红圈中的换行很容易被忽略,很多人打印出来是这样一种情况:
这就是没有换行的结果,有一行直接跑后面了。 所以一定要换行!!!
这样我们的棋盘就打印完成了
3.布置雷
与打印棋盘一样,我们也需要在test.c中定义一个函数并传参,我们把他定义为SetMine,那test.c中的代码就变成了这样:
void game()
{
//数组
char mine[ROWS][COLS];//'0'
char show[ROWS][COLS];//'*'
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//棋盘打印
//DisplayBoard(mine, ROW, COL);
DisplayBoard(show, ROW, COL);
//布置雷
SetMine(mine, ROW, COL);
//DisplayBoard(mine, ROW, COL);
这里的红框指的是布置好雷的棋盘,为了防止泄露,变成了注释,将注释取消就变成了作者模式。
既然我们定义了函数,那么头文件中也必须声明一下函数,这样才能传参。
#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_COUNT 10
//函数的声明
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
//打印棋盘的
void DisplayBoard(char board[ROWS][COLS], int rows, int cols);
//布置雷
void SetMine(char mine[ROWS][COLS], int row, int col);
注意下图,定义了一个布置雷的数量, 这个放在头文件中是因为这个需要我们随时可以改,因此赋了一个变量。
game.c中的代码如下:
void SetMine(char mine[ROWS][COLS], int row, int col)
{
int count = EASY_COUNT;
while (count)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (mine[x][y] == '0')
{
mine[x][y] = '1';
count--;
}
}
}
这里的int count等于上文所赋的变量,若我们要改变雷的数量时,只需在头文件中操作即可。
这里while(count)的意思是,count刚开始时等于10,当进入循环时,count布置一个雷就会少一个,当雷都布置完时,count等于0,此时while判断为假,也就跳出循环了。
这两行是取随机数的代码,详情可以看C语言随机数生成教程,C语言rand和srand用法详解 (biancheng.net)
本文不再仔细探讨,当然,rand需要调用srand函数,所以在test.c中加入,如下图:
而还有一点如下图:
不管是0还是1,始终是字符,不要忘记。
以上,我们布置雷的工作就结束了,最后,让我们实现扫雷。
4.扫雷
扫雷仍然需要定义函数,我们将其定义为FindMine,进行到这里,有没有发现每当我们要进行一个新功能的时候,都要定义函数,这里就体现出定义函数的重要性了。至此,我们的test.c代码如下所示:
void game()
{
//数组
char mine[ROWS][COLS];//'0'
char show[ROWS][COLS];//'*'
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//棋盘打印
//DisplayBoard(mine, ROW, COL);
DisplayBoard(show, ROW, COL);
//布置雷
SetMine(mine, ROW, COL);
//DisplayBoard(mine, ROW, COL);
//排查雷
FindMine(mine, show, ROW, COL);
}
注意一点:
这里的上下对比一下你会发现,下面比上面多了一个show,原因是排查雷时,我们既需要mine数组显示,也需要show数组显示有多少雷,因此两个函数都要传参。下面将game.c中排雷是怎么实现的:
首先输入坐标来排查:
但是格子是9*9,有人喝醉了写了99*99怎么办,那这样我们就应该提示他输入错误:
其中,&&表示并且
但是,我们又不能只输入一个坐标,只排查一次,因此我们应该使用循环:
使用了while循环之后,就可以进行排查了,需要注意的是,
这里用while(1)是因为我们要一直排查,因此必须使条件一直为真。
此时,我们进行下一步,排雷:
这里用一个if判断,如果输入坐标为雷,便显示被炸死,且显示棋盘格,并用break跳出循环。进行下一次游戏。而如果不是雷,就要查看周围雷的个数,怎么实现呢?往下看:
这里else以下就是不是雷的情况了,定义了一个函数,我们需要发现周围八个格子中雷的总数并显示出来,注意,这里显示的是数字 ,对照ASCII表发现,两个连续数字字符对应的数字之间差一,也就是说
像这样我们可以显示为‘1’+‘1’-‘0’-‘0’-‘0’-‘0’-‘0’-‘0’-‘0’=2(注意:是ASCII表的剑法)
那么,若中间坐标为(x,y)其余坐标怎么表示呢?
将所有这些值加起来减去'0'的个数就行了:
这样我们就可以表示雷的个数了。找雷的代码如下:
static 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;
while (1)
{
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
{
int count = GetMineCount(mine, x, y);
show[x][y] = count + '0';
DisplayBoard(show, ROW, COL);
}
}
else
{
printf("坐标非法,重新输入\n");
}
}
这样,我们就实现了排查雷的工作。 可是这样有一个问题,如图:
这个while循环会一直进行下去,那我们怎么赢得游戏,因此,我们需要一个变量win ,每当我们排掉一个雷时,我们就让win加一,当我们把全部雷都排完以后,此时的win应该等于71,那怎么跳出循环呢,让win等于81-10即可,如果小于这个数,循环进行,等于,则跳出循环,游戏胜利!代码如下:
static 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
{
int count = GetMineCount(mine, x, y);
show[x][y] = count + '0';
DisplayBoard(show, ROW, COL);
win++;
}
}
else
{
printf("坐标非法,重新输入\n");
}
}
到此为止,整个游戏进程结束,看到这里,你也能写出风靡一时的游戏了,是不是非常有成就感。我们下次再见!
全部游戏的完整代码如下:
1.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++)
{
printf("%d ", i);
int j = 0;
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
printf("--------扫雷游戏-------\n");
}
void SetMine(char mine[ROWS][COLS], int row, int col)
{
int count = EASY_COUNT;
while (count)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (mine[x][y] == '0')
{
mine[x][y] = '1';
count--;
}
}
}
static 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
{
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);
}
}
2.test.c
#define _CRT_SECURE_NO_WARNINGS 1
#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];//'*'
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//棋盘打印
//DisplayBoard(mine, ROW, COL);
DisplayBoard(show, ROW, COL);
//布置雷
SetMine(mine, ROW, COL);
//DisplayBoard(mine, 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;
}
3.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_COUNT 10
//函数的声明
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
//打印棋盘的
void DisplayBoard(char board[ROWS][COLS], int rows, int cols);
//布置雷
void SetMine(char mine[ROWS][COLS], int row, int col);
//排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
谢谢大家