前言
用C语言写出一个扫雷程序。像大家熟悉的电脑扫雷一样,如果选的位置有雷就结束游戏;如果没有雷,就标记出来周围格子雷的数量。这里写出来的扫雷不能用鼠标点击,也没有提供标记雷的功能,只在CMD黑框框里展示出来,所以看上去比较难看、用起来也没有windows系统自带的扫雷那样方便。
设计要求
- 首先需要写一个展示功能,否则玩家拿什么扫
- 其次,要对雷区进行随机布雷。这里布雷的要求是第一步永远不能被炸死。我想到了两种方法解决:第一种方法是先生成随机雷阵,待玩家走第一步后判断这一步的位置是否有雷,如果有雷就把雷随机放在一个没有雷的位置;第二种方法是让玩家先走第一步,然后在避开玩家走第一步的位置的前提下进行随机布雷。本人觉得第二种容易实现且代码简单,就选择了第二种。
- 然后,需要扫雷里最重要的东西–玩家扫雷。玩家通过输入坐标的方式选择要扫的位置,然后程序检查该位置是否有雷,有雷就失败,没有就标记出该位置或者该位置的扩展位置的周围8个位置的雷数。如果雷数为0则不显示。
- 最后就是对前面3步的调度,玩什么难度、失败后继续还是不玩了。。。 一切由玩家选择。
补充一点,我所使用的是VS2008, 采用TDD模式,扫雷程序分为3个源文件,分别是:game.h 、game.c 、index.c
正文
为了更好地解释代码和体现思维的连贯性,按照代码的顺序进行说明。文末附有扫雷的全部代码。
先来看 index.c 中的代码:
#define _CRT_SECURE_NO_WARNINGS
#include "game.h"
int main(){
char mine[MROW][MCOL];
char show[MROW][MCOL];
int row=0, col=0;
int count=0;
int dead[2]={0};
char again[3]={'5'};
int c=0;
int Fx=-1, Fy=-1;
int WinSign=0;
srand((unsigned int) time(NULL));
AGAIN : Init_Board(mine,show, &row, &col);
Show_Board(show, row, col);
while(1)
{
printf("选择位置:>");
scanf("%d%d", &Fx, &Fy);
while((c = getchar()) != '\n' && c != EOF); //清除键盘缓冲区的万能写法
if ( Check_Legal(row, col, Fx, Fy) !=1)
{
printf("输入位置不合法,再次");
continue;
}
break;
}
PutMine(mine, row, col, Fx, Fy, &count);
while(Player_Do(mine, show, row, col, dead, &Fx, &Fy) ==0)
{
if(!Check_Win(show, row, col, count))
{
WinSign=0;
// Show_Board(mine, row, col);
Show_Board(show, row, col);
printf("*********************共有%d颗雷********************\n", count);
}
else
{
WinSign=1;
break;
}
}
if (WinSign==1)
{
Show_Board(show, row, col);
printf("恭喜你,排完了所有雷!\n");
}
else
{
show[dead[0]][dead[1]] = '*';
Show_Board(show, row, col);
}
printf("**********************************游戏结束****************************************\n");
Sleep(2500);
printf("你还想再玩一局吗?(Y/N) >");
scanf("%c", &again);
while((c = getchar()) != '\n' && c != EOF); //清除键盘缓冲区的万能写法
if ( strcmp(again, "Y")==0 || strcmp(again, "y")==0 || strcmp(again, "YES")==0 || strcmp(again, "yes")==0 || strcmp(again, "Yes")==0 ) goto AGAIN ;
return 0;
}
这里引入头文件 game.c 是因为所有的函数实现是放在game.c文件中,函数和全局变量、对标准库的引用全是放在game.h文件中的。
mine 矩阵是雷矩阵,用于内部逻辑处理; show 矩阵是展示矩阵,用于和玩家交互。
MROW、MCOL 是在game.h中用define定义的两个宏常量。
row、col是玩家选择板子的行数和列数。
Init_Board 函数的作用是随机布雷并由玩家决定板子的行数row和列数col。
Show_Board 函数的作用是显示板子的内容。
Check_Legal 函数的作用是检测玩家选择的第一个位置是否合法,合法则返回1。
PutMine 函数的作用是利用玩家走完第一步、用Fx、Fy记录下该位置后在避开该位置的前提下布雷,同时将雷的总数放入 count 中。
Player_Do 函数的作用是玩家选择要排的位置,然后判断,该位置不是雷就扩展,是雷的话就在 dead[2] 数组中写入这个游戏结束位置。
Check_Win 函数的作用是通过检查剩余未判断区域的数量是否大于总雷数 count 来判断是否排完全部的雷,是则返回1。
接下来对各个函数进行说明
Init_Board
static void Create_Board(char mine[MROW][MCOL], char show[MROW][MCOL], const int level, int *row, int *col){
int i=0, j=0;
*row=level;
*col=level;
for(i=0; i<MROW; i++)
{
for(j=0; j<MCOL; j++)
{
mine[i][j]='0';
show[i][j]='#';
}
}
}
void Init_Board(char mine[MROW][MCOL], char show[MROW][MCOL], int *row, int *col)
{
int choose;
int c=0;
printf("请选择难度( 1:容易 2:一般 3:困难 )>");
scanf("%d", &choose);
while((c = getchar()) != '\n' && c != EOF);
while(choose!=1 && choose!=2 && choose!=3)
{
printf("输入有误,请重新选择难度( 1:容易 2:一般 3:困难 )>");
scanf("%d", &choose);
while((c = getchar()) != '\n' && c != EOF);
}
switch (choose){
case 1:Create_Board(mine,show, EAZY, row, col); break;
case 2:Create_Board(mine, show,ORDINARY, row, col); break;
case 3: Create_Board(mine,show, DIFFICUTE, row, col);break;
default: printf("输入错误\n");
}
}
这里使用开关语句比较 choose 的值,根据 choose 值的不同,调用静态函数 Create_Board 创建用不同枚举常量的 mine 、 show 矩阵且将玩家选择的板子的行数和列数用 rwo、col 记录。注意这里的初始化并内有布雷,只是无差别地让 mine 的每个单元为字符O、让 show 的每个单元为字符#。布雷函数单独存在。
PutMine
void PutMine(char mine[MROW][MCOL], const int row, const int col, int Fx, int Fy, int *count)
{
int i=0, j=0;
int x=0, y=0;
int ss=0;
*count = row + (int)pow(2,(col/4))-3; //雷数 一级: 14颗 二级:32颗 三级:88颗
for (i=0; i<*count; i++)
{
x=rand()%row + 1;
y=rand()%col + 1;
if(mine[x][y]=='1' || ((x==Fx) && (y==Fy)) )
{
i--;
continue;
}
mine[x][y]='1';
ss++;
// printf("第%d颗雷的位置是 (%d, %d)\n", ss, x ,y);
}
}
这里根据初始化板子时确定的 row、col 来指定雷数 count,采用随机函数布雷, 有雷的位置在 mine 矩阵里用字符1代表。
Show_Board
void Show_Board( const char board[MROW][MCOL], const int row, const int col){
int i = 0, j = 0;
for(i=0; i<row+1; i++)
{
if (i==0)
{
for (j=0; j<row+1; j++) (j<10) ? printf(" %d ", j) : printf(" %d ", j);
printf("\n");
}
else
{
for(j=0; j<col+1; j++) (j==0) ? printf(" ") : printf(" ---");
printf("\n");
for (j=0; j<col+1; j++)
{
if (j==0) (i<10) ? printf(" %d", i) : printf(" %d", i);
else printf("| %c ",board[i][j]);
}
printf("|\n");
if(i==row)
for(j=0; j<col+1; j++) (j==0) ? printf(" ") : printf(" ---");
}
}
printf("\n\n");
}
这里没什么可说。
Check_Legal
int Check_Legal(const int row, const int col, const int x, const int y)
{
return ( x<row+1 && x>0 && y<col+1 && y>0 );
}
当玩家选择的第一步的位置不在板子的范围类判定为非法,返回0。
Check_Win
int Check_Win(static char show[MROW][MCOL], static int row, static int col, int count)
{
int i=0, j=0;
for (i=1; i<=row; i++)
{
for(j=1; j<=col; j++)
{
if (show[i][j]=='#')
{
// printf("show[%d][%d]=%c ", i, j, show[i][j]);
count--;
}
// else printf("show[%d][%d]= ", i, j);
}
// printf("\n");
}
// printf("\n");
if (count<0)
{
return 0;
}
return 1;
}
遍历 row*col show矩阵, 若 count 小于0,说明剩余 ‘#’个数大于雷的总数,没有排完雷,返回0。否则,返回1。
Player_Do
该函数最为复杂,也是扫雷的核心程序,故而放在最后。先放出 Player_Do 函数的实现,接下来分别对 Player_Do 函数内部的一些调用做出解释。
int Player_Do(char mine[MROW][MCOL], char show[MROW][MCOL], const int row, const int col, int dead[2], int *Fx, int *Fy)
{
int x=0, y=0;
int c=0;
x=*Fx; y=*Fy;
Reput:
if (*Fx ==-1 && *Fy ==-1)
{
printf("选择位置:>");
scanf("%d%d", &x, &y);
while((c = getchar()) != '\n' && c != EOF);
}
*Fx=-1; *Fy=-1;
if(Check_Legal(row, col, x, y)==1)
{
if(mine[x][y]=='1')
{
dead[0] = x;
dead[1] = y;
return 1;
}
else if (mine[x][y] == '-')
{
printf("此位置无效, 再次");
goto Reput;
}
else
{
expand(mine, show, row, col, x, y);
}
}
else
{
printf("输入位置不合法,再次");
goto Reput;
}
return 0;
}
该函数对玩家第一次选择以后的位置做出合法性、该位置是否已排过检查,若通过检查,则调用 expand 函数进行判断扩展,达到类似爆炸一样的效果。
Player_Do 调用的 expand
int expand(const char mine[MROW][MCOL], char show[MROW][MCOL], const int row, const int col, const int x, const int y)
{
if ( Check_Legal(row, col, x, y) !=1 )
{
return -1;
}
else if (mine[x][y] == '1' || mine[x][y] == '-')
{
return -2;
}
else
{
Mark_MineCount(mine, show, row, col, x, y);
if(show[x][y] == ' ')
{
expand(mine, show, row, col, x-1, y-1);
expand(mine, show, row, col, x-1, y);
expand(mine, show, row, col, x-1, y+1);
expand(mine, show, row, col, x, y-1);
expand(mine, show, row, col, x, y+1);
expand(mine, show, row, col, x+1, y-1);
expand(mine, show, row, col, x+1, y);
expand(mine, show, row, col, x+1, y+1);
}
}
return 0;
}
首先进行的仍是对传入的位置进行检查,若排查位置超出板子范围Check_Legal(row, col, x, y) !=1、排查位置有雷mine[x][y] == ‘1’、排查位置已排查过(mine[x][y] == ‘-‘),则函数返回以作为递归结束条件。 这里分别返回 -1和-2 是为了在编写代码的过程中思路更清晰,其实该函数的返回值并没有在后续的过程中用到,只是起到结束递归的作用。
写成这样也可以:
if(Check_Legal(row, col, x, y) !=1 || mine[x][y] == '1' || mine[x][y] == '-')
return 0;
甚至还可以将 expand 函数的返回类型设为 viod ,返回时写
if(Check_Legal(row, col, x, y) !=1 || mine[x][y] == '1' || mine[x][y] == '-')
return;
如果通过检查,就调用 Mark_MineCount 函数标记该位置周围8个位置雷的数量,然后在该位置周围雷数为0的情况下再递归地调用 expand 函数对传入位置的周围8个位置进行扩展。为什么是 “在该位置周围雷数为0的情况下” 呢? 因为在扫雷的过程中,如果一个位置本身没有雷但它周围8个位置有雷,那么这个位置应该显示出一个数字,并且停止扩展。
expand 调用的 Mark_MineCount
static void Mark_MineCount(char mine[MROW][MCOL], char show[MROW][MCOL], const int row, const int col, const int x, const int y)
{
char c = '0';
if ( Check_Legal(row, col, x, y) !=1 )
{
return;
}
else if (mine[x][y] == '1' || mine[x][y] == '-' || show[x][y]!='#')
{
return;
}
else
{
if(mine[x-1][y-1] !='-') (c = c + mine[x-1][y-1] - '0');
if(mine[x-1][y] !='-') (c = c + mine[x-1][y] - '0');
if(mine[x-1][y+1] !='-') (c = c + mine[x-1][y+1] - '0');
if(mine[x][y-1] !='-') (c = c + mine[x][y-1] - '0');
if(mine[x][y+1] !='-') (c = c + mine[x][y+1] - '0');
if(mine[x+1][y-1] !='-') (c = c + mine[x+1][y-1] - '0');
if(mine[x+1][y] !='-') (c = c + mine[x+1][y] - '0');
if(mine[x+1][y+1] !='-') (c = c + mine[x+1][y+1] - '0');
if(c == '0') show[x][y] = ' ';
else show[x][y] = c;
mine[x][y] = '-';
}
}
这个函数的参数太多,足足有6个,这样做没有错,但是非常不提倡。因为函数的参数一旦多了之后,就会使别人容易看不懂你的代码。我这里的 Mark_MineCount 是静态函数,仅供 game.c 内部使用,6个参数勉强能说的过去。。。 但身为开发人员,应尽量避免这种问题。
这里对参数做出解释:
char mine[MROW][MCOL] : mine板子的最大范围
char show[MROW][MCOL] : show板子的最大范围
const int row : 实际使用的板子的行数
const int col : 实际使用的板子的列数
const int x : 想要进行周围雷数标记的位置的所在行
const int y : 想要进行周围雷数标记的位置的所在列
函数一开始进行的仍是检查: 检查位置的合法性、检查该位置是否有雷、是否已经排过,如果没有通过检查,那么说明这个位置上不能用数字标示出它周围8个位置的雷数; 若通过检查,就以变量 C 来记录周围的雷数。并且如果雷数为0,说明这个位置周围没有雷,可以进行扩展。 最后的C
将 mine 矩阵的[x][y]位置标记为’-‘用来告诉 expand 函数这个位置不是雷并且已经排查过了。
mine[x][y] = '-';
源代码
扫雷程序的完成代码放在了 GitHub 上,点击这里获取完整代码