室友说你怎么才学到数组,然后我用C语言做了个扫雷(递归展开+难度选择+优化打印)
扫雷
创作不易,还请观众老爷耐心看完儿!
目录
1.扫雷框架
2.初始化棋盘
这是用户看到的棋盘,9*9大小,需要自己去排查雷
为了程序员更好地去控制扫雷,我们需要两个数组
- 程序员看的雷区数组 -- 里面存放着雷的位置 -- 11*11大小
- 用户看的展示数组 -- 全是*号,需要用户去排查 -- 11*11大小
使这两个数组一样大的原因:是为了使我们后面设计的接口函数更加兼容,即使展示的数组本质是11*11大小,但是我们打印的时候打印9*9就可以了
为了可以提高代码可维护性和兼容性,我们将数组的行数和列数,使用宏来替换
#define ROWS 11
#define COLS 11
#define ROW 9
#define COL 9
void InitBoard(char board[ROWS][COLS], int rows, int cols,char set)
{
for (int i = 0; i < rows; ++i)
{
for (int j = 0; j < cols; ++j)
{
board[i][j] = set;
}
}
}
我们将如果初始化的内容以字符的形式作为参数传进去,上述代码就可以很好地解决了兼容性,这一个函数可以解决雷区棋盘和展示用户的棋盘的初始化。
3.打印棋盘
打印棋盘大家可以充分发挥自己的艺术细胞,根据自己的喜好去设置棋盘的打印格式。下面博主提供两种。
格式1:
void print_mineline(int row)//打印分割线
{
for (int i = 0; i < row; ++i)
{
if (i == row / 2)
{
printf("扫雷");
}
printf("==");
}
printf("n");
}
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
print_mineline(row);//先打印一行分割线
//打印列号
for (int j = 0; j <= col; ++j)
{
if (0 == j)//将列对齐
{
printf(" ");
continue;
}
printf("%d ", j);
}
printf("n");
for (int i = 1; i <= row; ++i)
{
printf("%d ", i);
for (int j = 1; j <= col; ++j)
{
printf("%c ", board[i][j]);
}
printf("n");
}
print_mineline(row);//最后再打印一行分割线
printf("n");
}
效果:
格式2:
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
//打印列号
for (int j = 0; j <= col; ++j)
{
if (j == 0)
{
printf(" ");
continue;
}
printf(" %d ", j);
}
printf("nn");
for (int i = 1; i <= row; ++i)
{
//打印列号
//1.打印第一部分
printf(" %d ", i);
for (int j = 1; j <= col; ++j)
{
printf (" %c ", board[i][j]);
if (j <= col -1)
printf("|");
}
printf("n");
//2.打印第二部分
if (i <= row - 1)
{
printf(" ");
for (int j = 1; j <= col; ++j)
{
printf("---");
if (j <= col - 1)
printf("+");
}
}
printf("n");
}
}
效果:
但是格式2的缺点是如果棋盘是9*9以上大小,那么存在一些对齐问题,当然厉害的同学可以改善一下。这是提供的两个思路
4.布置雷
根据雷的个数n,随机布置n个雷,雷的个数可以根据用户的选择来定,例如
1.简单 -- 10个雷
2.普通 -- 20个雷
3.困难 -- 40个雷
4.疯狂 -- 80个雷
当然以上的布置雷都是限制在了9*9的棋盘中,大家也可以根据难度设计棋盘的大小,尺寸
为了代码的可维护性以及可读性,我们将不同难度下的雷的个数也使用宏来替换,以及可以使用枚举来帮助我们实现难度选择
//雷的个数
#define COUNT_EASY 10
#define COUNT_ORD 20
#define COUNT_DIF 40
#define COUNT_FRE 80
//难度等级
enum degree
{
EASY = 1,//简单
ORD,//普通
DIF,//困难
FRE//疯狂
};
void menu_degree()//难度选择菜单 -- 跟我们的枚举常量值一致
{
printf("==========1.简单==========n");
printf("==========2.普通==========n");
printf("==========3.困难==========n");
printf("==========4.疯狂==========n");
}
int SetMine(char board[ROWS][COLS], int row, int col)
{
int count = 0;//雷的个数
system("cls");//我们游戏开始部分肯定会要菜单,所以这里使用一个清屏功能
int input = 0;
do
{
menu_degree();
printf("请选择扫雷难度n");
scanf("%d", &input);
switch (input)
{
case EASY:
count = COUNT_EASY;
break;
case ORD:
count = COUNT_ORD;
break;
case DIF:
count = COUNT_DIF;
break;
case FRE:
count = COUNT_FRE;
break;
default:
printf("输入错误,请重新输入");
break;
}
} while (input != EASY && input != ORD &&
input != DIF && input != FRE);
int _count = count;//保存count的值
while (count)
{
//随机生成十个雷,放在中间的9*9中
int x = rand() % row + 1;
int y = rand() % col + 1;
//判断该位置是否已经布置了雷
if (board[x][y] == '0')//没布置
{
board[x][y] = '1';
--count;
}
}
return _count;//这里我们将布置雷的个数返回了,之后要用
}
解释:布置雷只需要在9*9中布置,所以咱们传参传的是row,col而不是rows,cols
5.排查雷
游戏的核心部分:
1.玩家输入需要排查雷的坐标
2.判断是否有效
3.根据雷区判断,如果是雷,游戏结束
4.如果不是雷,则在显示区棋盘中赋上雷的个数
5.如果雷的个数是0,还是考虑将周围8个位置进行递归展开
5.1排查雷的功能
在排查雷的函数中,mine -- 雷区棋盘,show -- 展示区棋盘,是需要配合使用的。例如判断一个位置是否是雷,我们要去雷区棋盘找,而即使修正展示区的位置内容,我们需要去show棋盘中修改,并且即使打印出来给用户观看。
判断输赢:
- 如果输入的坐标位置是雷,则输掉游戏
- 如果不是雷的位置都被排查完了,那么玩家赢得游戏胜利
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col,int count)
{
//雷的个数通过SetMine的返回值可以得到,然后我们通过参数的形式,传给FineMine
//因为我们的版本雷的个数不固定
int chance = row*col - count;//记录不是雷的个数
while (chance)
{
system("cls");
DisplayBoard(show, row, col);//展示show棋盘
printf("请输入需要排查雷的坐标n");
int x = 0, y = 0;
scanf("%d %d", &x, &y);
//检查坐标的合法性
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
//不是雷--显示雷的个数
if (mine[x][y] != '1')//没点到雷
{
//递归展开该位置
boom_mine(mine, show, x, y, &chance);//将该位置周围的展开,传坐标
//展开雷chance是会减少的,使用址传递,在函数里面修改chance
}
//如何是雷--爆炸
else
{
printf("很遗憾,你被炸死了n");
DisplayBoard(mine, row, col);
break;
}
}
else
{
printf("坐标不合法n");
}
}
//判断是否赢了-- 将所有不是雷的地方都排查出来了
if (chance == 0)
{
printf("排雷成功,游戏胜利n");
}
Sleep(2000);//停顿两秒看下结果
}
5.2递归展开功能bool_mine
查出该位置周围8个位置的雷的个数
- 0个以上的雷 -- 将这个show棋盘对应的位置赋值为该数字字符
- 0个雷 -- 先这个show棋盘对应的位置赋值为该数字字符,再对该位置周围8个位置进行递归展开
防止出现死递归,我们思考一下递归限制条件:
- 如果该位置越界,那么不做任何处理
- 如果该位置已经被排查过了就不做任何处理
如果你的代码出现了错误,调式一看发现有个stack overflow这个报错,原因之一就是你写出死递归了
void boom_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y, int* pchance)
{
//递归限制条件
//1.如果越界了,直接返回
if (x == 0 || x == ROW+1 || y == 0 || y == COL+1)
{
return;
}
//2.如果该位置已经展开,直接返回
if (show[x][y] != '*')
{
return;
}
//3.剩下的就是需要展开的情况
int count = get_mine(mine, x, y);//计算该位置的雷的个数
if (count)//如果雷的个数不为0,直接展开
{
show[x][y] = count + '0';
--(*pchance);//不是雷的个数也减一
return;//这里需要返回,不然无限递归下去了
}
else//如果雷的个数是0
{
//对周围八个位置进行展开操作
show[x][y] = count + '0';
--(*pchance);
boom_mine(mine, show, x - 1, y - 1, pchance);
boom_mine(mine, show, x - 1, y, pchance);
boom_mine(mine, show, x - 1, y + 1, pchance);
boom_mine(mine, show, x, y - 1, pchance);
boom_mine(mine, show, x, y + 1, pchance);
boom_mine(mine, show, x + 1, y - 1, pchance);
boom_mine(mine, show, x + 1, y, pchance);
boom_mine(mine, show, x + 1, y + 1, pchance);
}
}
5.3计算该位置的雷的个数
这里就很好地体现了我们在雷区棋盘中放字符0和字符1的好处。
我们将 周围8个位置的字符加进来 - 8*字符0 = 雷的个数
int get_mine(char mine[ROWS][COLS], int x, int y)
{
//周围八个位置的内容 - 8 * '0' == 周围雷的个数
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');
}
6.扫雷的全部代码
上述的操作是讲解扫雷的核心函数的实现,下面我们进行分块编程,并且设计一些菜单,将他们整合在一起。
game.h
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<windows.h>
//操作棋盘的时候用的
#define ROWS 11
#define COLS 11
//用户看到的
#define ROW 9
#define COL 9
//雷的个数
#define COUNT_EASY 10
#define COUNT_ORD 20
#define COUNT_DIF 40
#define COUNT_FRE 80
enum degree
{
EASY = 1,//简单
ORD,//普通
DIF,//困难
FRE//疯狂
};
//初始化棋盘
void InitBoard(char board[ROWS][COLS],int rows,int cols,char set);
//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col);
//布置雷
int SetMine(char board[ROWS][COLS], int row, int col);
//排查类
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col,int count);
game.c
打印的两个方式都放在里面了
#include"game.h"
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols,char set)
{
for (int i = 0; i < rows; ++i)
{
for (int j = 0; j < cols; ++j)
{
board[i][j] = set;
}
}
}
//打印棋盘--将行列号也打印出来
//void print_mineline(int row)
//{
// for (int i = 0; i < row; ++i)
// {
// if (i == row / 2)
// {
// printf("扫雷");
// }
// printf("==");
// }
// printf("n");
//}
//void DisplayBoard(char board[ROWS][COLS], int row, int col)
//{
// print_mineline(row);
// //打印列号
// for (int j = 0; j <= col; ++j)
// {
// if (0 == j)//将列对齐
// {
// printf(" ");
// continue;
// }
// printf("%d ", j);
// }
// printf("n");
//
//
// for (int i = 1; i <= row; ++i)
// {
// printf("%d ", i);
// for (int j = 1; j <= col; ++j)
// {
// printf("%c ", board[i][j]);
// }
// printf("n");
// }
// print_mineline(row);
// printf("n");
//}
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
//打印列号
for (int j = 0; j <= col; ++j)
{
if (j == 0)
{
printf(" ");
continue;
}
printf(" %d ", j);
}
printf("nn");
for (int i = 1; i <= row; ++i)
{
//打印列号
//1.打印第一部分
printf(" %d ", i);
for (int j = 1; j <= col; ++j)
{
printf (" %c ", board[i][j]);
if (j <= col -1)
printf("|");
}
printf("n");
//2.打印第二部分
if (i <= row - 1)
{
printf(" ");
for (int j = 1; j <= col; ++j)
{
printf("---");
if (j <= col - 1)
printf("+");
}
}
printf("n");
}
}
void menu_degree()
{
printf("==========1.简单==========n");
printf("==========2.普通==========n");
printf("==========3.困难==========n");
printf("==========4.疯狂==========n");
}
//布置雷
//mine中,是雷就布置1,不是雷就是0
//布置雷的函数返回值 -- 返回雷的个数
int SetMine(char board[ROWS][COLS], int row, int col)
{
int count = 0;//雷的个数
system("cls");
int input = 0;
do
{
menu_degree();
printf("请选择扫雷难度n");
scanf("%d", &input);
switch (input)
{
case EASY:
count = COUNT_EASY;
break;
case ORD:
count = COUNT_ORD;
break;
case DIF:
count = COUNT_DIF;
break;
case FRE:
count = COUNT_FRE;
break;
default:
printf("输入错误,请重新输入");
break;
}
} while (input != EASY && input != ORD &&
input != DIF && input != FRE);
int _count = count;//保存count的值
while (count)
{
//随机生成十个雷,放在中间的9*9中
int x = rand() % row + 1;
int y = rand() % col + 1;
//判断该位置是否已经布置了雷
if (board[x][y] == '0')//没布置
{
board[x][y] = '1';
--count;
}
//布置了
}
return _count;
}
//设计排查雷 -- 展开功能 -- 返回展开的雷的个数
//排查雷
int get_mine(char mine[ROWS][COLS], int x, int y)
{
//周围八个位置的内容 - 8 * '0' == 周围雷的个数
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');
}
void boom_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y, int* pchance)
{
//递归限制条件
//1.如果越界了,直接返回
if (x == 0 || x == ROW+1 || y == 0 || y == COL+1)
{
return;
}
//2.如果该位置已经展开,直接返回
if (show[x][y] != '*')
{
return;
}
//3.剩下的就是需要展开的情况
int count = get_mine(mine, x, y);//计算该位置的雷的个数
if (count)//如果雷的个数不为0,直接展开
{
show[x][y] = count + '0';
--(*pchance);//不是雷的个数也减一
return;//这里需要返回,不然无限递归下去了
}
else//如果雷的个数是0
{
//对周围八个位置进行展开操作
show[x][y] = count + '0';
--(*pchance);
boom_mine(mine, show, x - 1, y - 1, pchance);
boom_mine(mine, show, x - 1, y, pchance);
boom_mine(mine, show, x - 1, y + 1, pchance);
boom_mine(mine, show, x, y - 1, pchance);
boom_mine(mine, show, x, y + 1, pchance);
boom_mine(mine, show, x + 1, y - 1, pchance);
boom_mine(mine, show, x + 1, y, pchance);
boom_mine(mine, show, x + 1, y + 1, pchance);
}
}
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col,int count)
{
int chance = row*col - count;//记录不是雷的个数
while (chance)
{
system("cls");
DisplayBoard(mine, row, col);
DisplayBoard(show, row, col);
printf("请输入需要排查雷的坐标n");
int x = 0, y = 0;
scanf("%d %d", &x, &y);
//检查坐标的合法性
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
//如何是雷--爆炸
//不是雷--显示雷的个数
if (mine[x][y] != '1')//没点到雷
{
boom_mine(mine, show, x, y, &chance);//将该位置周围的展开,传坐标
}
else
{
printf("很遗憾,你被炸死了n");
DisplayBoard(mine, row, col);
break;
}
}
else
{
printf("坐标不合法n");
}
}
//判断是否赢了-- 将所有不是雷的地方都排查出来了
if (chance == 0)
{
printf("排雷成功,游戏胜利n");
}
Sleep(2000);
}
test.c
#include"game.h"
void game()//游戏函数
{
srand((unsigned int)time(NULL));
char mine[ROWS][COLS] = { 0 };//11*11的数组便于操作
char show[ROWS][COLS] = { 0 };//show数组与mine数组尺寸类型一样,可以使函数更兼容
//1.初始化棋盘
InitBoard(mine, ROWS, COLS,'0');//将mine初始化为字符0
InitBoard(show, ROWS, COLS, '*');//将show初始化为*
//3.布置雷
int count = SetMine(mine,ROW,COL);//布置雷是在mine中布置
//是在9*9中布置
//4.排查雷
FindMine(mine, show, ROW, COL,count);
}
void menu()
{
printf("=================================n");
printf("====== 1. play ======n");
printf("====== 0. exit ======n");
printf("=================================n");
}
void test()//测试函数
{
int input = 0;
do
{
system("cls");
menu();
printf("请输入->:");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("游戏退出n");
break;
default:
printf("错误输出n");
Sleep(1000);
break;
}
} while (input);
}
int main()
{
test();
return 0;
}
7.作者试玩环节
打完一把还是需要点时间的,博主就不玩完了。
当然了,上述代码肯定还存在着很多不足的地方,如果大家有什么好的建议欢迎在评论区留言,我们下期见吧。