前言:
大家好,今天我们来开一个新的篇章,我们在chapter6和chapter7分别介绍了C语言的数组和函数,本篇博客则是对前两篇博客内容,数组和函数内容的实践,我们来写一个简易的扫雷小游戏
在下面的内容中,我将呈现两版的扫雷游戏
第一版是简单版,即对扫雷游戏基本逻辑的实现
下面是一个游戏的界面
第二版是改进版,这版的代码运行起来后会更加贴近我们在电脑网页游玩的扫雷游戏
经过改进,这个扫雷项目对于附近没有雷的部分可以实现展开
下面是一个游戏的界面
所以本篇博客将用两种方法(简单的和改进的)来实现这个扫雷的小游戏,
下面让我们一起来看一下吧!
为了方便大家学习,相关代码截屏以及可复制代码我都会一并提供
本博客制作不易,希望大家点个赞支持一下这个萌新博主吧!
特别注意:本篇博客对扫雷的实现仅从功能玩法上考虑,而不追求像网页那样的图像界面
如果对那一方面有兴趣的朋友可以自己去了解了解哦
版本一
1.思维理解
下面是一个扫雷游戏的界面,我们先来玩一下,来感受这个游戏的玩法
(这里我也给出这个扫雷游戏的在线网页版的链接
http://www.minesweeper.cn/ 大家先去体验一下吧!)
相信各位都已经玩过了,我这里也给出我的一次游戏界面,然后我们来分析一下这个游戏的策划
游戏的内容(流程)
1.在地图上布置了10个雷(基础)
2.排查雷:如果点到的位置是雷,玩家被炸死,游戏结束,就像我上面玩失败的那张图(笑)
如果点到的不是雷,就统计这个坐标周围雷的个数(即此位置周围的8个位置)并显示
3.如果我们把所有非雷的位置找到,游戏胜利并结束
通过游玩网页版扫雷游戏,我们对它获得了一个大概的认知,下面为了复刻实现这个游戏
我们来对这个游戏的设计进行一个深入的理解与思维,来得到一个可以用代码实现的框架
下面我们从零开始,在VS2022上先创建一个新的项目,一步步实现扫雷游戏的版本一!
零:在搭建这个项目时,我采用的是模块化设计,分为3个部分:test.c,game.c,game.h
(test.c是完成扫雷游戏的大的逻辑,game的源文件和头文件则是游戏模块)
因为模块化设计很大程度上让代码逻辑更加清晰,形成良好的代码风格习惯
在实现game游戏模块之前,我们先在把整个扫雷游戏的开始与结束流程走一遍,这个逻辑步骤与我在本专栏的chapter 5将分支循环时最后做的猜数字小游戏一致,用到了do while 循环,这里就不再赘述,直接上代码
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:
printf("开始游戏\n");
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,重新选择!\n");
break;
}
} while (input);
return 0;
}
我们这里对menu这个打印菜单的函数进行了一个封装,这样可以使我们main函数里的逻辑尽可能的清晰,这些也是我们在之前的文章分享过的一些东西
下面我们来看看上面的代码的运行效果
我们这种先写主干再写细节,写一点调试运行一下的写法适合代码量大的工程,
就比如我们的扫雷游戏,整个工程的代码量在200行左右,是我们从零学习C语言来迄今为止最大的工程量了,千万不敢等所有的200行代码全写完才开始测试结果,那时报出的错误可能多到数不胜数!所以我们写一点,遇到一个问题就解决一个问题这种写代码的思路是很好的
好,题外话说到这,下面我们来继续写我们的扫雷工程
为了实现一个基础的9x9的扫雷小游戏
我们来实现具体的game功能
我们按照上面写menu()函数的思路,也把游戏的实现封装成一个game()的函数来代替原来代码中的printf("开始游戏\n");
然后我们在main()上面定义game()
下面我们要真正开始实现扫雷游戏的核心功能
要实现这个游戏逻辑的话要包括
1.生成棋盘并初始化棋盘
2.打印
3.布置雷
4.排查雷
等步骤
那么,首先,我们就要先有一个空的棋盘来承载我们的数据
这个棋盘,我们可以用C语言的二维数组来实现(对应了chapter 6的知识)
我们可以通过访问二维数组来选择我们想扫雷的位置
下面是对棋盘的定义
但这样可以满足我们的需求吗?
所以,在定义数组前,让我们再想两件事(非常关键!)
第一,如果我们在这个空棋盘中用 0 表示非雷,1表示雷
而我们开始游戏时如果这个位置的周围有0个或者1个雷,它也会显示0和1,这是否就会导致歧义?因为我们在这个数组中放的类型太多
第二,我们定义的是一个9x9的数组,但我们在玩游戏时如果点到了边缘的位置,再程序统计它四周8个位置的时候是否就会导致越界?因为最外面的一圈没有定义
所以为了解决上面两个问题,我们会向下面的代码那样去定义数组
我们定义了两个数组,数组的大小也在原有的基础上扩大了一圈
这里我们定义了一个11行11列的数组,但考虑到代码的延展性,如果我们以后想要修改代码,在下面涉及行和列的地方都要重新修改,就很麻烦,所以我们可以用#define来定义常量
请看下面的代码
我们在game.h中定义了常量,然后在test.c中包含这个头文件 #include “game.h”
(注意:我们自己定义的头文件加的是""而不是<> )
这样我们以后想修改行和列的数值就会变得非常容易
OK有了我们的棋盘,下一步该对其进行初始化并打印了
所以我们创建了以下两个函数来实现
这里的 ' 0 ' 和 ' * ' 我们初始化想让棋盘呈现的格式, ' 0 ' 表示一开始的数组mine全是无雷的
' * ' 是让玩家排查时看的棋盘更具神秘感
在test.c中写出了这个函数,接下来我们在game的头文件中进行一个声明
当然,这个函数的功能最终也是由我们实现的,我们把它定义在game的源文件中
这是初始化的函数定义的代码
这是打印棋盘的函数的代码
注意:我们在定义函数时实参与形参最好命名不要重复,应当做出区分
所以这里将ROWS,COLS换成了 rows,cols
还有一点,我们在打印棋盘这个函数DisplayBoard()中用的参数是row,col,而没有加s是因为我们只希望玩家在玩游戏是只能看到里面的9x9的样式,所以我们不用打印最外面的那一圈
下面的循环打印棋盘i从1开始也是这个道理
而这个函数上面的循环用来打印行列号来使我们更容易知道我们想扫雷的位置所对应的坐标
所以i从0开始
下面我们看一下打印的结果
这样二维数组的各个坐标就很直观了吧
不过,我们在游戏过程中,下面的mine数组是不打印的,因为这里放着存储雷位置的信息
所以,我们接下来去写布置雷的函数
布置雷的核心在于随机生成坐标,这涉及到了rand和srand函数,我们也在chapter5的猜数字游戏中讲过,大家可以从本专栏目录进入chapter5进行阅读哦
知道了这个,那么这个函数(我们暂且称之为SetMine函数)写起来就很简单了
下面是这个函数的代码
不过如果我们想改变雷的个数,跟前面的ROW,COL的问题一样我们在game.h中定义一个常量COUNT 然后将int count = 10;的10改成COUNT就可以了
最后我们来实现排查雷的函数吧!(我们可以命名为FindMine()函数)
排查雷会涉及两个数组,我们显示在mine数组中排雷,再将结果返回到show数组中
这里为了统计该坐标周围有几个雷,所以我们要为FindMine()函数再设计一个函数,即函数的嵌套调用,来统计该坐标周围有几个雷
下面是这个统计附近雷个数的函数GetMineCount()的代码
下面是函数FindMine()的代码
好辣,我们将所有的东西进行汇总,就能得到扫雷的全部代码啦
我们对代码运行一下,看一下结果吧
2.代码的汇总
下面是各个文件的代码,对上面的过程进行了一个汇总
1.test.c
#define _CRT_SECURE_NO_WARNINGS
#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');//'0' 初始化棋盘
InitBoard(show, ROWS, COLS, '*');//'*' 初始化棋盘
//DisplayBoard(show, ROW, COL); //打印棋盘
//DisplayBoard(mine, 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;
}
2.game.h
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define COUNT 10
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
void InitBoard(char arr[ROWS][COLS], int rows, int cols, char set);//初始化棋盘
void DisplayBoard(char arr[ROWS][COLS], int row, int col);//打印棋盘
void SetMine(char arr[ROWS][COLS],int row,int col);//布置雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row,int col);//排查雷
3.game.c
#define _CRT_SECURE_NO_WARNINGS
#include "game.h"
void InitBoard(char arr[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++)
{
arr[i][j] = set; //set为我们想初始化的字符
}
}
}
void DisplayBoard(char arr[ROWS][COLS], int row, int col)
{
int i = 1;
printf("------------------扫雷------------------\n");
for (i = 0; i <= row; i++)
{
printf("%d ", i); //打印列号
}
printf("\n");
for (i = 1; i <= row; i++)
{
int j = 0;
printf("%d ", i); //打印行号
for (j = 1; j <= col; j++)
{
printf("%c ", arr[i][j]);
}
printf("\n");
}
printf("------------------扫雷------------------\n");
}
void SetMine(char arr[ROWS][COLS], int row, int col)
{
int count = COUNT;
while (count)
{
//布置雷
int x = rand() % row + 1;//1~9
int y = rand() % col + 1;//1~9
//布置成功一个雷,count--
if (arr[x][y] == '0')
{
arr[x][y] = '1';
count--;
}
}
}
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 - 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 n = GetMineCount(mine, x, y);
show[x][y] = n + '0';
DisplayBoard(show, ROW, COL);
win++;
}
}
else
{
printf("坐标非法,请重新输入\n");
}
}
if (win == row * col - COUNT)
{
printf("游戏胜利!\n");
DisplayBoard(mine, ROW, COL);
}
}
版本二
我们在版本一中只是做到了如果选的位置不是雷,就显示它附近的雷的个数,
但细心的同学一定注意到了,如果我们点到的这个位置附件8个位置都没有雷,那么会实现展开,所以展开的这个功能就成为我们版本二实现优化的关键
我们先来运行一下
没错,附近无雷的点都用 / 来替代了0
下面我们来做这个改进版的扫雷
这里的代码模块与版本一一样,还是分成3个部分
1.test.c
#define _CRT_SECURE_NO_WARNINGS
#include"game.h"
void menu() //菜单打印函数
{
printf("************************\n");
printf("**** 1. play ****\n");
printf("**** 0. exit ****\n");
printf("************************\n");
}
void game() //游戏功能实现函数
{
char mine[ROWS][COLS] = {0}; //定义棋盘1
char show[ROWS][COLS] = {0}; //定义棋盘2
InitBoard(mine,ROWS, COLS, '0');//初始化棋盘1
InitBoard(show, ROWS, COLS, '*');//初始化棋盘2
//布置雷
SetMine(mine, ROW, COL);
DisplayBoard(show, 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;
}
2.game.h
#include<stdio.h>
#include <stdlib.h>
#include <time.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define COUNT 10 //雷的个数
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);//初始化棋盘
void DisplayBoard(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);//排查雷
void Open(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);//展开功能优化
3.game.c
#define _CRT_SECURE_NO_WARNINGS
#include"game.h"
int Win(char show[ROWS][COLS])
{
int i = 0;
int j = 0;
int win = 0;
for (i = 1; i <= ROW; i++)
{
for (j = 1; j <= COL; j++)
{
if (show[i][j] == '*')
{
win++;
}
}
}
return win;
}
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 SetMine(char board[ROWS][COLS], int row, int col)
{
int count = COUNT;
while (count)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (board[x][y] == '0')
{
board[x][y] = '1';
count--;
}
}
}
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 1;
printf("-------------扫雷-------------\n");
for (i = 0; i <= row; i++)
{
printf("%d ", i);
}
printf("\n");
for (i = 1; i <= row; i++)
{
int j = 0;
printf("%d ", i);
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
printf("-------------扫雷-------------\n");
}
static int get_mine_count(char board[ROWS][COLS], int x, int y)
{
return (board[x - 1][y] + board[x - 1][y - 1] + board[x][y - 1] +
board[x + 1][y - 1] + board[x + 1][y] + board[x + 1][y + 1]
+ board[x][y + 1] + board[x - 1][y + 1] - 8 * '0');
}
void Open(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{
int count = get_mine_count(mine, x, y);
if (count == 0)
{
show[x][y] = '/';
int i = 0;
int j = 0;
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
if (show[i][j] == '*' && i > 0 && i < 10 && j>0 && j < 10)
{
Open(mine, show, i, j);
}
}
}
}
else
{
show[x][y] = count + '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 < COUNT + 1)
{
printf("请输入坐标:>");
scanf("%d%d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (mine[x][y] == '1')
{
printf("很遗憾,你被炸死了!\n");
DisplayBoard(show, ROW, COL);
break;
}
else
{
Open(mine, show, x, y);
DisplayBoard(show, ROW, COL);
}
}
else
{
printf("输入坐标非法,重新输入\n");
}
if (Win(show) == COUNT)
{
printf("恭喜你,赢了!\n");
break;
}
}
}
三.再优化与拓展
那么经过了两个阶段,这个扫雷游戏还能不能进一步优化?
当然可以,比如
是否可以标记雷
是否可以加上排雷的时间显示
等等
这些就交给聪明的各位去钻研了
结语:
今天的分享就到这里了
这篇博客花费了我Humble很长的时间,希望大家点个赞或者关注吧(感谢感谢)
让我们在接下来的时间里一起成长,一起进步吧!