目录
前言
扫雷都玩过吧,没玩过也总得听说过吧。这篇文章将详细讲解扫雷游戏的代码实现,带领大家探索扫雷游戏背后有趣的奥秘。
(网页扫雷游戏:http://www.minesweeper.cn/)
一、游戏规则
盘面上有许多方格,方格中随机分布着一些雷。 你的目标是避开雷,打开其他所有格子。 一个非雷格中的数字表示其相邻8格中的雷数,你可以利用这个信息推导出安全格和雷的位置。 你可以用右键在你认为是雷的地方插旗(称为标雷)。 你可以用左键打开安全的地方,左键打开雷将被判定为失败。
二、程序大体思路
(1)多文件编程
扫雷游戏的代码较多,而且需要很多自定义函数。应用多文件编程,在另一个文件中定义这些函数,通过引用文件,可以在主文件中直接调用函数。这样可以使编程界面整洁、思路清晰,有利于查找并修正错误,提高效率。
各个功能模块分成多个文件同时编辑,可以有效的提高团队开发的分工协作效率,养成多文件编程的好习惯,有利于未来学习和工作。
在该项目中,需要创建三个文件。一个.h结尾的头文件,两个.c结尾的源文件
minesweeper.c 负责编写程序主体(整体构架),含有main函数,是程序运行的入口。
game.c 负责对程序中几个重要函数进行定义。
game.h 引用多个标准库的头文件,对程序中几个重要的变量进行宏定义,声明game.c中的函 数,在主文件中只需要引用game.h即可。
(2)设计思路
我们第一个目标是9*9的最基础版扫雷(只能逐个展开,无法标记雷)。
棋盘的选择与制定
棋盘要通过二维数组来实现,我们先考虑一个9*9二维数组
游戏规则规定,若查找的位置没有地雷,需要显示周围8格中地雷的个数,对于棋盘边缘的格子而言,它的周围不足8个格子,这样会导致写代码很麻烦,而且很容易出错(越界)。因此,我们可以考虑在棋盘周围再加一圈格子,即11*11,但无需在最外圈布置地雷,这样就完美解决了越界问题。
用字符0表示没有雷,用字符1表示有雷。进行排查后会显示雷的数量信息,若只在一个棋盘上操作,会扰乱原本信息而出错。因此,我们可以定义两个棋盘,一个用于布置雷,储存雷的信息,是隐藏起来的(mine棋盘),另外一个用于显示给玩家,存放排查后的雷的信息(show棋盘)。
(需要用到字符,方便起见,两个棋盘都定义为字符型二维数组,因此mine棋盘中用的是字符0和字符1)
所需函数
game.c:
1.对棋盘进行初始化的函数
2.打印棋盘的函数
3.随机布置雷的函数
4.玩家排查雷的函数(主体)
5.计算周围雷的个数的函数
minesweeper.c:
1.打印游戏菜单函数(menu())
2.进入游戏的函数(game())
3.随机布置雷,需要rand(),srand(),time()函数
4.main函数
大致流程
1.打印菜单,玩家选择是否进入游戏
2.进入游戏,对两个棋盘进行处理,玩家开始游戏
3.游戏:玩家输入坐标,若是雷,则游戏结束,并打印mine棋盘;若不是雷,显示周围雷数 量,打印更新后的show棋盘,继续输入坐标。若最后游戏成功,结束游戏,打印mine棋盘
三、程序代码实现
(1)菜单menu函数
“1”开始游戏,“0”退出游戏
代码:(minesweeper.c)
void menu()
{
printf("********************\n");
printf("****** 1.play ******\n");
printf("****** 0.exit ******\n");
printf("********************\n");
}
(2)设计main函数
1.玩家要做出选择,需要定义变量 input
2.
随机数
后期需要随机埋雷,要用到随机数,这里介绍一下。
rand函数
函数原型:
int rand(void);
rand函数会返回一个伪随机数(多次运行后发现每一次的“随机数”相同),这个随机数的范围是在0~RAND_MAX之间,这个RAND_MAX的大小是依赖编译器上实现的,但是大部分编译器上是32767。
其实rand函数是对⼀个叫“种子”的基准值进行运算生成的随机数。之所以每次运行程序产生的随机数序列是一样的,那是因为rand函数生成随机数的默认种子是1。
rand函数的使用需要包含⼀个头文件是:stdlib.h
srand函数
函数原型:
void srand (unsigned int seed);
用于改变前面所说的“种子”,但这样还不够,因为种子一旦确定了,rand函数返回的依旧是“伪随机数”。
time函数
函数原型:
time_t time (time_t* timer);
在程序中我们一般是使用程序运行的时间作为种子的,因为时间时刻在发生变化的。 在C语言中有一个函数叫time,就可以获得这个时间。
time函数会返回当前的日历时间,其实返回的是1970年1月1日0时0分0秒到现在程序运行时间之间的差值,单位是秒。time函数返回的这个时间差也被叫做:时间戳。
time函数的时候需要包含头文件:time.h
(此处不再细讲,上网查找即可)
3. do...while循环实现玩家选择进入游戏或退出游戏。
代码:(minesweeper.c)
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");
}
}while(input);
return 0;
}
(3)设计game函数
1.创建两个棋盘,即两个11*11字符型二维数组,ROWS表示行数,COLS表示列数
之后会常用到这两个数据,所以在头文件game.h中做如下宏定义
#define ROW 9 #define COL 9 #define ROWS ROW + 2 #define COLS COL + 2
2.对两个棋盘进行初始化,打印show棋盘,布置雷,扫雷
全部通过调用函数实现
代码:(minesweeper.c)
void game()
{
char mine[ROWS][COLS];
char show[ROWS][COLS];
InitBoard(mine, ROWS, COLS, '0');//初始化mine
InitBoard(show, ROWS, COLS, '*');//初始化show
PrintBoard(show, ROW, COL);//打印show
SetMine(mine, ROW, COL);//在mine布置雷
FindMine(mine, show, ROW, COL);//扫雷
}
(4)棋盘初始化InitBoard函数
四个形参:字符型二维数组,行,列,初始化的字符
设计思路是:通过双重for循环(行,列)为所有格子赋值,这里我们为mine棋盘所有格子赋 值字符‘0’,为show棋盘所有格子赋值字符‘*’。
代码:(game.c)
void InitBoard(char board[ROWS][COLS], int rows, int cols, char mark)
{
int i = 0, j = 0;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
board[i][j] = mark;
}
}
}
(5)打印棋盘PrintBoard函数
三个形参:字符型二维数组,行,列
设计思路:打印棋盘内层9*9格子,为了优化输出效果,可以表明行、列序号,方便查找。 另外,还可以在棋盘前面加上"------扫雷游戏------"这样的标头,整洁且直观。
代码:(game.c)
void PrintBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0, j = 0;
printf("------扫雷游戏------\n");
for (i = 0; i <= col; i++)//第一行,列标(‘0’用于空出第一列行标)
{
printf("%d ", i);
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%d ", i);//第一列,行标
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
(6)布置地雷SetMine函数
三个形参:字符型二维数组,行,列
设计思路:1.设置地雷个数,现在头文件中宏定义,再在.c文件中定义、初始化一个变量
game.h中
game.c中
2.while循环中布置雷,每布置一个,number--,减到零结束循环。
3.制造随机位置:
行列坐标x、y都是随机且独立的(1-9),对随机数函数rand函数进行如下操 作,先除以9取余(rand()%9),这样得到的余数是0-8,再加一(rand() %9+1),得到1-9的随机数。(函数中,我们对row和col进行取余)
4.布置雷,若随机坐标没有雷,就埋一个雷。
代码(game.h)
#define Mine_Number 10
代码(game.c)
void SetMine(char board[ROWS][COLS], int row, int col)
{
int number = Mine_Number;
while(number)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if(board[x][y] == '0')
{
board[x][y] = '1';
number--;
}
}
}
(7)周围雷个数MineCount函数
三个形参:字符型二维数组,行坐标,列坐标
设计思路:通过双重for循环和if语句,对目标坐标为中心的九宫格进行统计,每有一个雷, 变量number++。
注意这是一个int类型函数,最后要返回int类型值number。
代码:(game.c)
int MineCount(char mine[ROWS][COLS], int x, int y)
{
int number = 0;
for(int i = x-1; i <= x+1; i++)
{
for(int j = y-1; j <= y+1; j++)
{
if(mine[i][j] == '1')
{
number++;
}
}
}
return number;
}
(8)排查雷FindMine函数
四个形参:两个字符型二维数组,行,列
设计思路:1.定义三个变量,x(行坐标),y(列坐标),win(记录已排查数)
2.while循环,保证玩家排查出所有非雷区域后,显示游戏成功,正常退出。
3.玩家输入坐标,判断坐标合法性,若是雷,游戏结束,跳出循环;若不是雷, 显示雷个数,继续排查。若坐标非法,让玩家重新输入。循环结束后,判断是 否成功。
4.需要调用SetMine函数。
代码:(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 - Mine_Number)
{
printf("请输入要排查的坐标:>");
scanf("%d %d", &x, &y);
if(x >= 1 && x <= row && y >= 1 && y <= col)
{
if(mine[x][y] == '1')
{
printf("嘿嘿嘿,你被炸死了\n");
PrintBoard(mine, ROW, COL);
break;
}
else
{
int count = MineCount(mine, x, y);
show[x][y] = count + '0';//将个数(整型数字)转换为字符
PrintBoard(show, ROW, COL);//展示当前状况
win++;
}
}
else
{
PrintBoard(show, ROW, COL);
printf("输入坐标错误,请重新输入\n");
}
}
if(win == row*col - Mine_Number)
{
printf("666,排雷成功\n");
PrintBoard(mine, ROW, COL);
}
}
(9)头文件中函数声明
再头文件game.h中对minesweeper.c需要调用的函数进行声明。
代码:(game.h)
void InitBoard(char board[ROWS][COLS], int rows, int cols, char mark);//初始化
void PrintBoard(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);//扫雷
四、全部代码(基础版)
minesweeper.c
#include"game.h"
void menu()
{
printf("********************\n");
printf("****** 1.play ******\n");
printf("****** 0.exit ******\n");
printf("********************\n");
}
void game()
{
char mine[ROWS][COLS];
char show[ROWS][COLS];
InitBoard(mine, ROWS, COLS, '0');//初始化mine
InitBoard(show, ROWS, COLS, '*');//初始化show
PrintBoard(show, ROW, COL);//打印show
SetMine(mine, ROW, COL);//在mine布置雷
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");
}
}while(input);
return 0;
}
game.c
#include"game.h"
//对棋盘进行初始化函数
void InitBoard(char board[ROWS][COLS], int rows, int cols, char mark)
{
int i = 0, j = 0;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
board[i][j] = mark;
}
}
}
//打印棋盘,注意只打印内层
void PrintBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0, j = 0;
printf("------扫雷游戏------\n");
for (i = 0; i <= col; i++)
{
printf("%d ", i);
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%d ", i);
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
//在mine棋盘上布置雷
void SetMine(char board[ROWS][COLS], int row, int col)
{
int number = Mine_Number;
while(number)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if(board[x][y] == '0')
{
board[x][y] = '1';
number--;
}
}
}
//排查点周围雷的个数
int MineCount(char mine[ROWS][COLS], int x, int y)
{
int number = 0;
for(int i = x-1; i <= x+1; i++)
{
for(int j = y-1; j <= y+1; j++)
{
if(mine[i][j] == '1')
{
number++;
}
}
}
return number;
}
//最重要的函数,游戏的主体
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 - Mine_Number)
{
printf("请输入要排查的坐标:>");
scanf("%d %d", &x, &y);
if(x >= 1 && x <= row && y >= 1 && y <= col)
{
if(mine[x][y] == '1')
{
printf("嘿嘿嘿,你被炸死了\n");
PrintBoard(mine, ROW, COL);
break;
}
else
{
int count = MineCount(mine, x, y);
show[x][y] = count + '0';//将个数(整型数字)转换为字符
PrintBoard(show, ROW, COL);//展示当前状况
win++;
}
}
else
{
PrintBoard(show, ROW, COL);
printf("输入坐标错误,请重新输入\n");
}
}
if(win == row*col - Mine_Number)
{
printf("666,排雷成功\n");
PrintBoard(mine, ROW, COL);
}
}
game.h
#pragma once
#include<stdio.h>
#include<time.h>//time()函数需要
#include<stdlib.h>//rand()函数需要
#define ROW 30
#define COL 16
#define ROWS ROW + 2
#define COLS COL + 2
#define Mine_Number 99
void InitBoard(char board[ROWS][COLS], int rows, int cols, char mark);//初始化
void PrintBoard(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);//扫雷
五、运行实操
输入0、1以外的数:
提示重新输入
输入1,进入游戏
输入非法坐标,提示错误,重新输入
没有踩雷,显示周围地雷个数,打印最新show棋盘,继续输入
多次操作后:
失败结局:(失败后,显示结果)
成功结局:
游戏结束后会再次显示菜单,输入0,退出游戏
六、扩展版(仅供参考)
(1)清屏效果
运行实操中,可以发现已经“过时”的show棋盘依旧显示在屏幕上,导致输出界面繁杂。因此,考虑使用清屏,只显示当前最新棋盘。
清屏要用到system函数,需要在适当位置添加以下语句:
system("cls");
调用system函数,需要头文件windows.h,在game.h文件中引用即可
插入位置:需要在玩家输入坐标后清屏,并显示新棋盘,可以将插入到FindMine函数“输入xy语句”之后。
代码
minesweeper.c文件FindMine函数中:
printf("请输入要排查的坐标:>");
scanf("%d %d", &x, &y);
system("cls");//新加入的
game.h文件中:
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#include<windows.h>//新加入的
效果展示:
(2)标记地雷
目标:通过输入,将show棋盘目标坐标的‘*’改为‘#’,并打印出来。通过再次输入,可将‘#’改 回‘*’。标记功能,有利于游戏体验。
修改位置:FindMine函数
思路:再定义变量z,输入x.y.z,在xy合法情况下,z=0则正常排雷,z=1修改字符,其余重 新输入。
代码(FindMine部分)
int x = 0;
int y = 0;
int z = 0;//判断是否进行标记
int win = 0;
while (win < row * col - Mine_Number)
{
printf("请输入坐标:>");
scanf("%d %d", &x, &y);
printf("请选择要进行的操作(0:排查,1:标记或去除标记):>");
scanf("%d", &z);
system("cls");
if (x >= 1 && x <= row && y >= 1 && y <= col && z == 0)//z为0情况
{
if (mine[x][y] == '1')
{
printf("嘿嘿嘿,你被炸死了\n");
PrintBoard(mine, ROW, COL);
break;
}
else
{
int count = MineCount(mine, x, y);
show[x][y] = count + '0';//将个数(整型数字)转换为字符
PrintBoard(show, ROW, COL);//展示当前状况
win++;
}
}
else if (x >= 1 && x <= row && y >= 1 && y <= col && z == 1)//z为1情况
{
if (show[x][y] == '*')
{
show[x][y] = '#';
PrintBoard(show, ROW, COL);
}
else if (show[x][y] == '#')
{
show[x][y] = '*';
PrintBoard(show, ROW, COL);
}
}
else
{
printf("输入坐标错误,请重新输入\n");
}
}
(3)展开周围一片
目标:当查找坐标不是雷,而且周围连续一片也都不是雷时,会将这一片区域全部展示出来
只排查红框格子,实际排查出的是一大片。
流程:每次对一个格子进行排查时,若周围8个没有雷,则展示这9个格子,并且会对其 周围8个格子进行同样操作,以此类推。当周围有雷时,停止。
设计思路:使用递归函数,注意越界问题,注意避免进入死循环递归。
步骤一:定义“展开函数”Unfold()(在game.c文件中)
void Unfold(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y, int row, int col)
{
if (x<1 || x>row || y<1 || y>col)//坐标非法,退出函数
{
return;
}
if (show[x][y] != '*')//已被排查过,退出函数,这里一定要注意,避免进入死循环
{
return;
}
int count = MineCount(mine, x, y);
if (count != 0)//周围有雷,显示雷数量
{
show[x][y] = count + '0';
return;
}
else if(count == 0)//周围没有雷
{
show[x][y] = ' ';//将‘0’改为空格,是游戏画面更简洁、易分辨,增强游戏体验
for (int i = x - 1; i <= x + 1; i++)对周围8格进行处理
{
for (int j = y - 1; j <= y + 1; j++)
{
Unfold(mine, show, i, j, ROW, COL);
}
}
}
}
步骤二:对FindMine函数进行修改,这里只展示一部分代码(在game.c文件中)
if (x >= 1 && x <= row && y >= 1 && y <= col && z == 0)
{
if (mine[x][y] == '1')
{
printf("嘿嘿嘿,你被炸死了\n");
PrintBoard(mine, ROW, COL);
break;
}
else
{
Unfold(mine, show, x, y, ROW, COL);//先进行连续展开操作
PrintBoard(show, ROW, COL);//然后打印show棋盘
int i;
int j;
for (win = 0, i = 1; i <= ROW; i++)//每一次通过遍历数组更新win
{
for (j = 1; j <= COL; j++)
{
if (show[i][j] != '*' && show[i][j] != '#')
{
win++;
}
}
}
}
}
(4)选择游戏难度
目标:设置三种难度,供玩家选择
简单9*9棋盘,10个雷中等16*16棋盘,40个雷
困难30*16棋盘,99个雷
问题一:按照之前的代码,输出16*16:
可以发现,由于在PrintBoard函数中,只在%d和%c后加了一个空格,导致当行、列是两位数时,出现格式混乱的情况。若将空格换为\t,又会导致空格太多,运行界面盛不下。因此,考虑修改域宽,并进行缩进。
代码如下:
for (i = 0; i <= col; i++) { printf("%-3d", i);//改为%-3d,即占三个格,向左缩进,多余位置打印空格 } printf("\n"); for (i = 1; i <= row; i++) { printf("%-3d", i);//%-3d for (j = 1; j <= col; j++) { printf("%-3c", board[i][j]);//%-3c } printf("\n"); }
效果:
问题二:如何选择难度?
不怕大家笑话,我指针还没学明白,只能用了一个非常麻烦、非常笨的办法:
我几乎把整个所有代码又复制了两份(包括宏定义、函数声明...),也就是将之前的ROW改成了ROW1、ROW2、ROW3......
这里就不再拿全部代码献丑了,只放几张截图吧。
唯一值得说的是,我又定义了两个函数,让玩家做出选择,再根据玩家输入通过switch语句调用相应的函数。
//难度选项菜单
void DifficultyMenu()
{
printf("***************************\n");
printf("**** 1.简单 9*9 10雷*****\n");
printf("**** 2.中等 16*16 40雷*****\n");
printf("**** 3.困难 30*16 99雷*****\n");
printf("***************************\n");
printf("请选择难度:>");
}
//选择难度
int SelectDifficulty()
{
int x;
scanf("%d", &x);
system("cls");
return x;
}
七、扩展版运行视频
扫雷(扩展版)
八、尾声
我用了完完整整的三天时间来完成的扫雷游戏代码以及这篇博客,这期间遇到了很多很多困难,毕竟我的知识储备太有限了。
一开始我的目标是不仅完成基础的代码,还要实现很多扩展功能。但连续三天不停写代码、修bug,对我这个初学者来说简直就是地狱般的折磨。我多次想过放弃,但我终究坚持了下来。
因为,我想,既然做了,就做到最好,不辜负最初那个目标坚定的自己。
最后我想说,非常非常非常非常感谢大家的支持!