1. 难度选择:
提供了easy,normal,hard三种难度。棋盘大小,雷数随难度增加而增大。
2. 为棋盘动态分配内存,模拟实现二维数组。
3. 自动调整窗口大小:
使窗口容纳下相应难度的棋盘
4. 标记雷位:
玩家可以对自己确定的雷位进行标记,已经标记的雷位不能翻开。防止遗忘和误输造成的错误
5. 递归展开
1>> 检查选定的位置周围有没有雷
2>> 如果没有雷自动翻开周围的位置
3>> 重复递归进行以上步骤,就可以造成翻开一片的效果
6. 优化游戏界面,提高游戏体验
1>> 两个棋盘:一个棋盘负责向玩家展示,另一个棋盘放置炸弹。
1>> 分配内存:难度不同,棋盘的大小也不同,所以应该根据需要动态分配棋盘的内存空间
2>> 初始化:分配好内存空间后,初始化两个棋盘的内容
3>> 放置炸弹:开始游戏前,在随机位置安放预定数量的炸弹。
4>> 释放内存:不同于一维动态内存空间,二维的动态内存应先释放低维空间,再释放高维空间
//文件包含
#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
//定义标识符,统一控制空格
#define MENUBLANK "\t\t"
#define BOARDBLANK "\t"
处理玩家选项,决定游戏难度或是退出程序。
enum option{
EXIT,
EASY,
NORMAL,
HARD
};
int main(){
enum option input;
srand((unsigned)time(NULL));
do
{
//**修改窗口颜色,大小**
GameInterface(0);
//打印菜单
Menu();
//处理玩家选项
printf("请选择:");
fflush(stdin);
scanf("%d", &input);
switch (input)
{
case EASY:
//根据玩家所选难度调整窗口大小,以容纳整个棋盘
GameInterface(EASY);
//Playgame函数的三个参数:行,列,雷数
Playgame(9, 9, 5);
system("pause");
break;
case NORMAL:
GameInterface(NORMAL);
Playgame(16, 16, 30);
system("pause");
break;
case HARD:
GameInterface(HARD);
Playgame(16, 30, 3);
system("pause");
break;
case EXIT:
break;
default:
printf("输入错误,请重新输入!\n");
system("pause");
break;
}
} while (input);
}
根据游戏难度调整窗口大小,修改窗口背景色
void GameInterface(int cmd){
switch (cmd)
{
//难度不同,棋盘大小也不同
case EASY:
system("mode con cols=55 lines=30");
break;
case NORMAL:
system("mode con cols=85 lines=45");
break;
case HARD:
system("mode con cols=140 lines=45");
break;
default:
system("color B0");
system("mode con cols=55 lines=25");
break;
}
}
打印菜单以供玩家选择
void Menu(){
system("cls");
printf("\n\n\n\n\n");
printf(MENUBLANK" Mine Sweeper\n");
printf("\n\n");
printf(MENUBLANK"*************************\n");
printf(MENUBLANK"****** 1.Easy *****\n");
printf(MENUBLANK"*************************\n");
printf(MENUBLANK"****** 2.Normal *****\n");
printf(MENUBLANK"*************************\n");
printf(MENUBLANK"****** 3.Hard *****\n");
printf(MENUBLANK"*************************\n");
printf(MENUBLANK"****** 0.Exit *****\n");
printf(MENUBLANK"*************************\n");
printf("\n");
}
动态内存分配,初始化棋盘,调用函数开始游戏,释放内存空间
void Playgame(int row, int col, int minenum){
char** mineboard;//雷区
char** showboard;//展示区
//为“二维数组”动态分配内存
//多分配了两行两列是为了方便后续排雷
//边上的一圈和中间的位置就可以统一处理了
mineboard = MallocArr2d(row + 2, col + 2);
if (mineboard == NULL){
exit(EXIT_FAILURE);
}
showboard = MallocArr2d(row + 2, col + 2);
if (showboard == NULL){
exit(EXIT_FAILURE);
}
//初始化雷区和展示区
Initboard(mineboard, row + 2, col + 2, ' ');
Initboard(showboard, row + 2, col + 2, '#');
//为雷区安放炸弹
Setmine(mineboard, row, col, minenum);
//排雷
Findmine(showboard, mineboard, row, col, minenum);
//释放动态分配的内存
FreeArr2d(mineboard, row + 2);
mineboard = NULL;
FreeArr2d(showboard, row + 2);
showboard = NULL;
}
分配二维动态内存,模拟实现二维数组
char** MallocArr2d(int x, int y){
char** arr2d;
//先为第一维(行)分配内存
arr2d = malloc(x * sizeof(char*));//第一维的每个元素都为指针类型
if (arr2d == NULL){
perror("Arrmalloc2d");
return NULL;
}
//再为第二维(列)分配内存
int i = 0;
for ( i = 0; i < x; i++)
{
arr2d[i] = malloc(y*sizeof(char));//第二维的元素才是char类型
if (arr2d[i] == NULL){
perror("Arrmalloc2d");
return NULL;
}
}
return arr2d;
}
释放二维动态内存
void FreeArr2d(char** arr2d, int x){
int i = 0;
//释放内存时,由内而外。
//先释放第二维内存空间
for ( i = 0; i < x; i++)
{
free(arr2d[i]);
}
//再释放第一维内存空间
free(arr2d);
}
打印棋盘:包括行号,列号,分割线
void Displayboard(char** board, int row, int col){
system("cls");
printf("\n\n");
int i, j;
//打印列号
printf(BOARDBLANK" ");
for ( i = 1; i <= col; i++)
{
printf("| %-2d", i);
if (i == col)
{
printf("|");
}
}
printf("\n");
for ( i = 1; i <= row; i++)
{
//打印横线
printf(BOARDBLANK"---");
for ( j = 1; j <= col; j++)
{
printf("|---");
if (j == col)
{
printf("|");
}
}
printf("\n");
//打印行号
printf(BOARDBLANK"%2d ", i);
//打印棋盘内容
//因为棋盘多分配了两行两列的内存
//而打印,安放炸弹,排雷等操作的范围不包括外围的一圈
//所以行列坐标都应该是从1开始的闭区间
for (j = 1; j <= col; j++)
{
printf("| %c ",board[i][j]);
if (j == col)
{
printf("|");
}
}
printf("\n");
//最后一行打印最后一条横线
if (i == row)
{
printf(BOARDBLANK"---");
for (j = 1; j <= col; j++)
{
printf("|---");
if (j == col)
{
printf("|");
}
}
}
}
printf("\n\n");
}
初始化棋盘
void Initboard(char** board, int rows, int cols, char ch){
int i, j;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++){
board[i][j] = ch;
}
}
}
为雷区安放炸弹
void Setmine(char** mineboard, int row, int col, int minenum){
int i;
int x, y;
for (i = 0; i < minenum;)
{
x = rand() % row+1;
y = rand() % col+1;
if (mineboard[x][y] != '*')
{
mineboard[x][y] = '*';
i++;
}
}
}
体现游戏设计思路的主体函数,通过调用游戏具体实现的函数,控制游戏的整个流程。分标记雷位;排雷;判断输赢三大块内容
void Findmine(char** showboard, char** mineboard, int row, int col, int minenum){
int x, y;
while (1)
{
//打印展示区
Displayboard(showboard, row, col);
//接受玩家输入
printf("请输入排雷坐标:");
fflush(stdin);
scanf("%d %d", &x, &y);
// # 标记雷位 #
if (x == 0 && y == 0){
SIGN:
printf("请输入标记坐标:");
fflush(stdin);
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)//判断坐标是否在范围内
{
if (showboard[x][y] == '#' || showboard[x][y] == '!')//判断坐标是否被排除或已经被标记
{
showboard[x][y] = showboard[x][y]=='!'?'#':'!';
//如果未被标记就标记,反之取消标记
continue;
}
else
{
printf("此坐标已排除!\n");
goto SIGN;
}
}
else
{
printf("坐标非法!\n");
goto SIGN;
}
}
// # 排雷 #
if (x >= 1 && x <= row && y >= 1 && y <= col)//判断坐标是否在范围内
{
if (showboard[x][y] == '#')//判断坐标是否被排除
{
if (mineboard[x][y] != '*')//判断是否是雷
{
Chain_reflect(showboard, mineboard, row, col, x, y);//触发链式反应
}
else
{
//排雷失败打印棋盘
Displayboard(showboard, row, col);
printf("Bang!\n");
printf("排雷失败!\n");
break;
}
}
else
{
printf("此坐标已排除!\n");
system("pause");
}
}
else
{
printf("坐标非法!\n");
system("pause");
}
// # 判断是否完成排雷 #
if (Iswin(showboard, row, col, minenum) == 1)
{
Displayboard(showboard, row, col);
printf("恭喜你,排雷成功!\n");
break;
}
}
}
检查给定坐标周围位置的雷数,并将雷数转换为char型返回
char get_minenum(char** mineboard, int x, int y){
int i, j;
int count = 0;
for (i = -1; i <= 1; i++)
{
for (j = -1; j <= 1; j++)
{
if (mineboard[x + i][y + j] == '*')
count++;
}
}
return count + '0';//count + '0'将int转换为char型返回
}
连锁反应,实现递归展开的功能
void Chain_reflect(char** showboard, char** mineboard, int row, int col, int x, int y){
int i, j;
//检查坐标是否在范围内
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
//判断坐标是否被排除
if (showboard[x][y] == '#')
{
//判断周围是否有雷
if (get_minenum(mineboard, x, y) == '0')
{
//将雷位置为空格
showboard[x][y] = ' ';
//传递周围雷位
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
Chain_reflect(showboard, mineboard, row, col, i, j);
}
}
}
else
{
showboard[x][y] = get_minenum(mineboard, x, y);
}
}
}
return;
}
判断是否排雷成功
int Iswin(char** showboard, int row, int col, int minenum){
//如何判断输赢:遍历展示区统计'#'(未排)和'!'(标记)的数量
//如果总数等于雷数,证明玩家已经找出所有的雷位,排雷成功
int i, j;
int count = 0;
for (i = 1; i <= row; i++){
for (j = 1; j <= col; j++)
{
if (showboard[i][j] == '#' || showboard[i][j] == '!'){
count++;
}
}
}
if (count == minenum){
return 1;
}
else
{
return 0;
}
}
1. 我们知道二维数组名是其元素一维数组的地址,即二维数组名的数据类型为 char(*arr)[10](数组指针)。
由于我们并不能确定到底有多少列,也就是说不能确定一维数组的元素个数。所以不能将mineboard和showboard设置成数组指针的类型。
2. 但是我们可以模仿二维数组的实现方式,即二维数组名是一维数组的地址(char(*arr)[10])
而一维数组的地址就是数组首元素的地址(char*),虽然数据类型不同,但两个地址完全相同。
3. 因此我们可以将mineboard和showboard设置为 char** (二级指针),以showboard为例(假设m行n列):
先为showboard分配m个char*类型的元素空间,再为每个char*元素分配n个char类型的元素空间
system("cls"); |
清屏 |
system("pause"); |
程序暂停 |
system("color 5e"); |
修改背景和字体颜色 |
system("mode con cols=60 lines=30"); |
调整控制台大小 |
system("Shutdown -s -t 60"); |
设置60秒后自动关机 |
system("Shutdown -a"); |
取消关机计划 |
颜色属性由两个十六进制数字指定
第一个:对应于背景,第二个:对应于前景。
每个数字可以为以下任何值:
0 = 黑色 |
1 = 蓝色 |
2 = 绿色 |
3 = 浅绿色 |
4 = 红色 |
5 = 紫色 |
6 = 黄色 |
7 = 白色 |
8 = 灰色 |
9 = 淡蓝色 |
A = 淡绿色 |
B = 淡浅绿色 |
C = 淡红色 |
D = 淡紫色 |
E = 淡黄色 |
F = 亮白色 |
答:调用get_minenum函数统计坐标周围雷数时,如果位置恰好在边脚就会造成越界访问。(Chain_reflect函数也会有类似情况)
与其分各种情况讨论,不如在最外层多分配一圈内存空间,这样就统一了边角和中间位置的访问方法。
在下面的问题中,我把最外层多分配的一圈元素简称为“外层元素”。
答:在递归展开的过程中,可能外层元素的坐标传递给下一层函数,如果不加判断直接调用get_minenum函数会造成越界访问。
答:在递归展开的过程中,可能会将其他递归线路已经展开的坐标再次传递给下一层函数,如果不进行判断的话就会出现栈溢出的问题(stack overflow)
六、分享交流
最后将完整源码分享给大家,希望大家能与我在交流中共同学习,共同进步:
https://gitee.com/zty857016148/C_OS_Project/tree/master/Mine_Sweeperhttps://gitee.com/zty857016148/C_OS_Project/tree/master/Mine_Sweeper
如果大家还有什么问题欢迎提问,我抽空一定回答!
若是各位大佬发现了代码中的错误或是遗漏之处还请海涵,最好在评论区与我交流助我一臂之力!
上一篇:【初级C语言】详解井字棋游戏(电脑下棋算法优化)https://blog.csdn.net/zty857016148/article/details/126721652