目录

一.前言

二.扫雷游戏规则

三.游戏设计思路

四.代码实现过程

①设置菜单

②创建并初始化棋盘

③布置地雷后打印棋盘

④排查坐标

五.完整源代码!!!


一.前言

几乎每一台电脑上都有一款系统自带的扫雷游戏,相信许多接触过计算机的人都有玩过这款游戏,如果没有玩过也没关系,下面的链接可以让你在线体验这款游戏。扫雷游戏网页版 – 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 = 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#include#include//游玩棋盘的长宽#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 ");scanf("%d%*c%d", &x, &y);if (x >= 1 && x = 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',得到的差值即是地雷数}