目录
五.完整源代码!!!
一.前言
几乎每一台电脑上都有一款系统自带的扫雷游戏,相信许多接触过计算机的人都有玩过这款游戏,如果没有玩过也没关系,下面的链接可以让你在线体验这款游戏。扫雷游戏网页版 - Minesweeper
对于初学C语言的同学来说,实现一个简单的游戏不仅可以检验自己的能力,也能够在敲代码的过程中激发更多新奇的灵感,加深对知识的理解,那么接下来我会带大家一步一个脚印实现一个属于自己的扫雷游戏!
希望这篇文章对你有所帮助,你的点赞、收藏和评论是对博主的最大支持!有可以改进的地方欢迎讨论(*^▽^*)
二.扫雷游戏规则
扫雷界面一共有n*n个方格,以下9*9为例,左上角的数字显示其中一共隐藏着10颗地雷。(如图一)
玩家选择其中一个方块,如果踩中地雷,则地雷引爆,玩家失败。(如图二)
如果成功逃脱,则此方块会显示以此方块为中心四周八个方块一共有多少个地雷,将所有安全地带探索完毕,则玩家胜利!(如图三)
↑图一 ↑图二 ↑图三
三.游戏设计思路
1.编程方式采取模块化编程,因为在编程过程中需要多行代码、多个函数,如果都存放在一个文件里,会影响代码的可阅读性,维护、修改起来也更加繁琐,同时也不利于程序员在写代码的过程中保持清晰的思路,故采取模块化编程,对于不同文件代码,只需要在源文件中包含自定义的头文件即可
2.打印游戏菜单,让玩家选择进入游戏还是退出游戏
3.创建并初始化棋盘
4.布置地雷后打印棋盘
5.排查坐标
如何设置棋盘:
扫雷的过程中涉及到数据存储和坐标,自然而然就会想到利用二维数组构成棋盘,其中有雷和无雷用不同的信息表示,可以用1表示有雷,0表示无雷,如下图
将有雷设置为1,无雷设置为0有一个好处,在后续打印某点周围地雷数的时候,直接将某点四周的元素值相加,通过简单的计算即可得出地雷数
但同时也存在弊端,有雷的时候显示1,四周只有一颗雷的时候也显示1,未排查的区域显示0,四周只有一颗雷的时候也显示0,使玩家在游玩的时候很容易被搞晕,那么为了同时解决字符冲突的问题,又保留1和0便于计算的特点,我们可以构建两个棋盘,一个是存放地雷的棋盘,用‘ 1 ’和‘ 0 ’表示;一个是玩家游玩时的棋盘,用 ' * ' 表示未探索的格子,‘ ! ’ 表示地雷,数字表示四周地雷的数量,如下图
←mine棋盘
←show棋盘
看似已经完成,但这个棋盘仍然有弊端,当探索棋盘边上的格子的时候,需要计算四周数组元素的值,这个时候会发生栈溢出!导致计算的结果出错,为了避免这一种情况,给棋盘的四周加上一圈,这一圈全都设置为' 0 ',这样就解决了栈溢出的问题,如图所示
←mine棋盘
←show棋盘
注:最外面的一圈只是起到防止栈溢出的作用,在实际游玩过程中1~9行/列才是真正的格子
四.代码实现过程
注:写代码一定要一步一步来!每完成一个小模块就要验证是否能运行,问题早发现早解决!
①设置菜单
需求:菜单包含两个选项,1.开始游戏;0.退出游戏
如何实现:创建menu函数,先在屏幕上打印出来菜单选项,在引导玩家输入选项,根据不同的选项进入不同的分支
void menu()
{
printf("**************************\n");
printf("******* 1.开始游戏 ********\n");
printf("******* 0.结束游戏 ********\n");
printf("**************************\n");
}
do //使用do-while循环的原因既然打开了游戏,那么至少进入一次菜单界面
{
menu();//打印出菜单
printf("请选择>");//引导玩家输入选项
scanf("%d", &input);//输入选项
switch (input)//根据不同选项,进入不同分支
{
case 1: //选项 1 ,开始游戏
game();
break;
case 0: //选项 0 ,结束游戏
break;
default: //输入1和0以外的字符,提示错误
printf("输入错误!请重新输入\n");
}
} while (input);//根据给input赋的值是否为0判断是否进入下一次循环
②创建并初始化棋盘
需求:(1)创建两个棋盘,棋盘的大小用宏定义常量表示,以便日后添加修改棋盘改变难度的功能;(2)初始化棋盘,将一个棋盘元素全部初始化为‘ 0 ’,另一个棋盘全部元素全部初始化为‘ * ’(3)打印棋盘,验证是否成功初始化
void game()
{
//创建棋盘,一个是用于存放雷的mine棋盘,一个是用于存放排查出的周边雷的数量的show棋盘
char mine[ROWS][COLS];
char show[ROWS][COLS];
//初始化棋盘
InitBoard(mine, ROWS, COLS, '0');//将mine棋盘全部初始化为 '0'
InitBoard(show, ROWS, COLS, '*');//将show棋盘全部初始化为 '*'
//写代码过程中用于测试棋盘是否成功初始化,实际游玩时记得屏蔽掉调用过此函数的语句
TeatDisplayBoard(mine, ROWS, COLS);
TeatDisplayBoard(show, ROWS, COLS);
}
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)
{
int i = 0, j = 0;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
board[i][j] = set;
}
}
}
//写代码过程中用于测试棋盘是否成功初始化,实际游玩时记得屏蔽掉调用过此函数的语句
void TeatDisplayBoard(char board[ROWS][COLS], int rows, int cols)
{
int i = 0, j = 0;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
验证:输入1,输出结果如下
可见初始化成功,屏蔽掉用于验证的打印棋盘代码,进入下一步
③布置地雷后打印棋盘
需求:(1)随机生成地雷位置,地雷的数量用宏定义常量表示,以便日后实现改变地雷数量的功能;(2)打印出show棋盘,要求有坐标,方便玩家输入坐标
如何实现:利用srand()、time()和 rand()函数生成随机坐标
//布置地雷
void SetMine(char board[ROWS][COLS], int row, int col)
{
srand((unsigned int)time(NULL));
int count = EASY_COUNT; //地雷的数量
while (count)
{
int x = rand() % row + 1; //生成行的随机数
int y = rand() % col + 1; //生成列的随机数
if (board[x][y] == '0') //若该点未布置雷
{
board[x][y] = '1'; //则布置雷
count--; //并且开始下一次布雷,知道EASY_COUNT颗雷全部布置完,count==0时循环才会结束
}
}
}
利用TeatDisplayBoard()函数验证是否布置成功,如图,无论重复布置多少次,都是在第1~9行,1~9列随机布置10个地雷,说明布雷成功,第一个需求,满足了,接下来实现第二个需求
//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
printf("————扫雷————\n");//游戏标题,同时分割线能够使界面更整洁
int i = 0, j = 0;
for (i = 0; i <= row; 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");
}
}
验证:成功打印出show棋盘
④排查坐标
需求:玩家输入坐标后,有四种结果,(1)踩到地雷,游戏结束;(2)没有踩到地雷,显示周围地雷数量;(3)该坐标已排查过,重新输入坐标;(4)超出棋盘范围,报错后重新输入坐标
如何实现:
玩家输入坐标后,判断mine棋盘中对应坐标是否为1,(1)如果为1,打印出mine棋盘,游戏失败;(2)如果为0,进入一个新的函数,计算并返回周边地雷的数量,赋值给show棋盘对应的坐标并打印show棋盘;(3)如果该坐标已经排查过,提示后重新输入坐标;(4)如果超出棋盘范围,提示超过范围后重新输入坐标。
当排查出( ROW*COL-count )个安全坐标的时候,循环结束,玩家胜利
//排查地雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col,int count)
{
int ret = 0; //接收GetMineCount()的返回值,也就是地雷的数量
int win = ROW * COL - count;
while (win != 0)
{
int x = 0, y = 0;//横坐标和纵坐标
printf("请输入坐标>");
scanf("%d%*c%d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)//限定输入坐标的范围必须在棋盘内
{
system("cls");//清屏,使界面更整洁
if (mine[x][y] == '1') //踩到雷了
{
printf("踩到雷了!游戏结束!\n");//踩到雷
DisplayBoard(mine, ROW, COL);//打印mine棋盘显示所有雷的位置
break;
}
else if (show[x][y] == '*')//该点还未被排查过,执行排查
{
if (mine[x][y] != '1');
{
ret = GetMineCount(mine, x, y);//调用函数计算坐标周围地雷数
if (ret + '0' == '0')//为了使游戏界面更加整洁,如果得到的地雷数为0,则将该位置清空
{
show[x][y] = ' ';
}
else//得到的地雷数不为0
{
show[x][y] = ret + '0'; //将地雷数转化为字符类型的地雷数
}
DisplayBoard(show, ROW, COL);
win--;
}
}
else if (show[x][y] != '*')//该坐标已经不是未知数,并且此时游戏还在继续,说明该点已经被排查过了
{
system("cls");
printf("该坐标以及排查过了,请重新输入!\n");
DisplayBoard(show, ROW, COL);
}
}
else//输入坐标超过范围,重新输入
{
system("cls");//清屏,使界面更整洁
printf("输入坐标超过棋盘范围!请重新输入!\n");
DisplayBoard(show, ROW, COL);
}
}
if (win == 0)
{
printf("恭喜胜利!\n");
}
}
//计算坐标周围地雷数
GetMineCount(char mine[ROWS][COLS], int x, int y)
{
return (mine[x - 1][y - 1] + mine[x - 1][y] + mine[x - 1][y + 1]
+ mine[x][y - 1] + mine[x][y + 1]
+ mine[x + 1][y - 1] + mine[x + 1][y] + mine[x + 1][y + 1] - 8 * '0');
//返回地雷数,注意这是一个字符类型的数组,要得到地雷数字,需要减去8*'0',得到的差值即是地雷数
}
验证:可以在游戏开始后打印mine棋盘,开始上帝视角,便于快速完成游戏,测试代码是否可行
←上帝视角
测试1:输入重复坐标 测试2:输入超出范围的坐标
测试3:踩雷 测试4:排查出所有雷
到这里整一个扫雷游戏就完成啦!
五.完整源代码!!!
game.c
#define _CRT_SECURE_NO_WARNINGS 0
#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 game();
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
//写代码过程中用于测试棋盘是否成功初始化,实际游玩时记得屏蔽掉调用过此函数的语句
//void TeatDisplayBoard(char board[ROWS][COLS], int rows, int cols);
//布置地雷
void SetMine(char board[ROWS][COLS], int row, int col);
//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col);
//排查地雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int count);
//计算坐标周围地雷数
GetMineCount(char mine[ROWS][COLS], int x, int y);
test.c
#define _CRT_SECURE_NO_WARNINGS 0
#include"game.h"
void menu();//菜单
void game();//游戏运行流程
int main()
{
int input = 0;//菜单选项
do //使用do-while循环的原因既然打开了游戏,那么至少进入一次菜单界面
{
menu();//打印出菜单
printf("请选择>");//引导玩家输入选项
scanf("%d", &input);//输入选项
switch (input)//根据不同选项,进入不同分支
{
case 1: //选项 1 ,开始游戏
game();
break;
case 0: //选项 0 ,结束游戏
break;
default: //输入1和0以外的字符,提示错误
printf("输入错误!请重新输入\n");
}
} while (input);//根据给input赋的值是否为0判断是否进入下一次循环
return 0;
}
void menu()
{
printf("**************************\n");
printf("******* 1.开始游戏 *******\n");
printf("******* 0.结束游戏 *******\n");
printf("**************************\n");
}
void game()
{
//创建棋盘,一个是用于存放雷的mine棋盘,一个是用于存放排查出的周边雷的数量的show棋盘
char mine[ROWS][COLS];
char show[ROWS][COLS];
//初始化棋盘
InitBoard(mine, ROWS, COLS, '0');//将mine棋盘全部初始化为 '0'
InitBoard(show, ROWS, COLS, '*');//将show棋盘全部初始化为 '*'
//布置地雷
SetMine(mine, ROW, COL);
//上帝视角,测试代码的时候用
DisplayBoard(mine, ROW, COL);
//写代码过程中用于测试棋盘是否成功初始化,实际游玩时记得屏蔽掉调用过此函数的语句
//TeatDisplayBoard(mine, ROWS, COLS);
//TeatDisplayBoard(show, ROWS, COLS);
//打印棋盘
DisplayBoard(show, ROW, COL);
//排查地雷
FindMine(mine, show, ROW, COL, EASY_COUNT);
}
game.c
#define _CRT_SECURE_NO_WARNINGS 0
#include"game.h"
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)
{
int i = 0, j = 0;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
board[i][j] = set;
}
}
}
////写代码过程中用于测试棋盘是否成功初始化,实际游玩时记得屏蔽掉调用过此函数的语句
//void TeatDisplayBoard(char board[ROWS][COLS], int rows, int cols)
//{
// int i = 0, j = 0;
// for (i = 0; i < rows; i++)
// {
// for (j = 0; j < cols; j++)
// {
// printf("%c ", board[i][j]);
// }
// printf("\n");
// }
//}
//布置地雷
void SetMine(char board[ROWS][COLS], int row, int col)
{
srand((unsigned int)time(NULL));
int count = EASY_COUNT; //地雷数量
while (count)
{
int x = rand() % row + 1; //生成行的随机数
int y = rand() % col + 1; //生成列的随机数
if (board[x][y] == '0') //若该点未布置雷
{
board[x][y] = '1'; //则布置雷
count--; //并且开始下一次布雷,知道EASY_COUNT颗雷全部布置完,count==0的时候,循环才会结束
}
}
}
//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
printf("————扫雷————\n");//游戏标题,同时分割线能够使界面更整洁
int i = 0, j = 0;
for (i = 0; i <= row; 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");
}
}
//排查地雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col,int count)
{
int ret = 0; //接收GetMineCount()的返回值,也就是地雷的数量
int win = ROW * COL - count;
while (win != 0)
{
int x = 0, y = 0;//横坐标和纵坐标
printf("请输入坐标>");
scanf("%d%*c%d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)//限定输入坐标的范围必须在棋盘内
{
system("cls");
if (mine[x][y] == '1') //踩到雷了
{
printf("踩到雷了!游戏结束!\n");//踩到雷
DisplayBoard(mine, ROW, COL);//打印mine棋盘显示所有雷的位置
break;
}
else if (show[x][y] == '*')//该点还未被排查过,执行排查
{
if (mine[x][y] != '1');
{
ret = GetMineCount(mine, x, y);//调用函数计算坐标周围地雷数
if (ret + '0' == '0')//为了使游戏界面更加整洁,如果得到的地雷数为0,则将该位置清空
{
show[x][y] = ' ';
}
else//得到的地雷数不为0
{
show[x][y] = ret + '0'; //将地雷数转化为字符类型的地雷数
}
DisplayBoard(show, ROW, COL);
win--;
}
}
else if (show[x][y] != '*')//该坐标已经不是未知数,并且此时游戏还在继续,说明该点已经被排查过了
{
system("cls");
printf("该坐标以及排查过了,请重新输入!\n");
DisplayBoard(show, ROW, COL);
}
}
else//输入坐标超过范围,重新输入
{
system("cls");
printf("输入坐标超过棋盘范围!请重新输入!\n");
DisplayBoard(show, ROW, COL);
}
}
if (win == 0)
{
printf("恭喜胜利!\n");
}
}
//计算坐标周围地雷数
GetMineCount(char mine[ROWS][COLS], int x, int y)
{
return (mine[x - 1][y - 1] + mine[x - 1][y] + mine[x - 1][y + 1]
+ mine[x][y - 1] + mine[x][y + 1]
+ mine[x + 1][y - 1] + mine[x + 1][y] + mine[x + 1][y + 1] - 8 * '0');
//返回地雷数,注意这是一个字符类型的数组,要得到地雷数字,需要减去8*'0',得到的差值即是地雷数
}