C语言扫雷的显微镜级别讲述
分析
很久之前写过这个
现在做一个详细复述从源头出发
首先我们想写扫雷 最基本的框架
1(外部).这个游戏可以玩完之后再玩一次
2.(内部)首先是要创建一个游戏场地
3.(内部) 电脑随机布雷
4.(内部)玩家可以点击排雷 获取周围雷的数量信息
5.(内部) 雷最好是那种点击就炸开的情况
6.(内部)可以做一半标记信息
这里我觉得最难点在于对雷炸开 还有我们点击之后能标记信息
我们现在先开始创建两个源文件和一个头文件即可
其中project.c是主函数写main
test.c存放各类很长的函数 声明在project.h然后直接调用
(内心os:其实我可以把每部分功能都拆开写比较直观,但是就像我上次写的模块化三子棋其实写的有点问题,但是就算把所有.h合并在一起之后还是感觉很长,所以我以后写这种扫雷的小工程文件,还是把所有外部的函数全部写在test.c里吧)
实践
主函数
好的我们先设计我们的主函数
project.c
int main()
{
int input = 0;
do
{
menu();//打印菜单栏
printf("请输入数字进行选择:");
scanf("%d", &input);
switch (input)
{
case 1:
printf("游戏开始了:\n");
// play();
break;
case 0:
printf("结束\n");
break;
default:
printf("请重新输入:\n");
break;
}
} while (input);
}
这大概就是我们main函数的框架 通过一个人do while循环 (为什么暂时把play 注释 因为我先跑一遍看看是否有错 )
这个做法是先打印出一个菜单,然后接收用户给出的1还是0 当你输入的不是 1或者是0 时会提示重新输入
为什么用do while 循环 因为input非0即真,那么我们输入的只要是非0 就可以重新开始游戏 继续玩一把了 非常方便
我们在test.c和project.h中分别写入对menu()的声明即可
void menu()
{
printf("************************\n");
printf("************************\n");
printf("******1.游戏开始********\n");
printf("******0.退出游戏********\n");
printf("************************\n");
printf("************************\n");
}
play模块
接下来我们研究游戏本身
写出play模块
首先我们应该创建两个棋盘是不是
为什么 因为一个棋盘用来埋雷 对雷进行操作另外一个用来对我们玩家进行操作
因为你不太好在一张表格上完成雷的排布,再去点击测试是否有雷,当然我说的这是内部场景 外部了肯定只显示用户操作的那一张 是否有雷
如果你听不懂没关系我也觉得这一块没讲好。就这么说吧,你去扫雷有一个显示块,那么其实是不是他背后还有一张藏雷的,我们就把玩家操作和雷模块抽象剥离出来
还有一个问题
我们再创建棋盘的时候也需要考虑到一个问题 ,就是我们再扫雷之后会读取周围9个格子剩余的雷数,那么为了保证全面性我们和不再那么复杂(不然你想四个顶角和边还有内部 这就有3种复杂的情况了)我们要是考虑这个3种情况那么计算量就很大,为什么不在开始设计棋盘的时候就扩大一圈呢。
类似于这样那么对于格点上的位置也能很好的处理
我们在头文件里定义行列ROWS和COLS 这样以后修改方便一点
#define ROWS 11
#define COLS 11
#define ROW 9
#define COL 9
#define number 10
这就是project.h当前的文件
行列 一个是雷的行列 另外ROW COL 主要用在显示块里 然后定义的define 就用来 表示雷的数目
初始化模块
void initboard(char board[ROWS][COLS], int rows, int cols, char ret)
{
int i = 0;
int j = 0;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
board[i][j] = ret;
}
}
}
写到这里我们不知道正确与否先显示一下
display这里有点小技巧
因为我们得输入数字 不太好一个一个点着看 那么我们可以在开头带上1 到9
//显示模块 显示模块 显示模块
void displayboard(char board[ROWS][COLS], int row, int col)
{
//依次打印值喽
int i = 0;
int j = 0;
for (i = 0; i <= row; i++)//第一行打印 数字序列 1到9
{
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");
}
}
显示完成之后开始埋雷
埋雷有两个点 1 是我们是在1到9埋雷的,不能埋雷到我自定义的大框架上
2.埋雷此时需要引入一个随机数的概念
随机数用我们擅长的时间戳实现
srand((unsigned int)time(NULL));需要在主函数中加入随机的函数
并且#include<time.h>
//埋雷 埋雷 埋雷
void mailei(char board[ROWS][COLS], int row, int col)
{
//我们需要电脑生成随机数。随机生成地雷
int x = 0;
int y = 0;
int count = number;//头文件定义雷的数量是10个
while (count)//怎么保证有指定数量的雷呢,这里用count--
{
x = rand() % row + 1;//指的是1-9
y = rand() % col + 1;
if (board[x][y] == '0')
{
board[x][y] = '1';
count--;
}
}
}
3.埋雷之后加入我们最喜欢的最复杂的排雷
排雷需要注意一下三点
1.判断第一发是否就炸
2,如果不炸那么 会把周围不是的条件都激发出来
这个激发模块才是最难写的
3,标记是否有雷以便后续实现
//排雷模块
void pailei(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;
char ch = 0;
while (win < row * col - number)
{
printf("请输入雷区坐标:\n");//让用户输入坐标
scanf("%d%d", &x, &y);
//输入坐标我们首先判断是不是无效坐标
if (x >= 1 && y >= 1 && x <= row && y <= col)
{
if (mine[x][y] == '1')
{
printf("你输了\n");
displayboard(mine, ROW, COL);
break;
}
else
{
//此处不是地雷,我们需要激发周围的对象,就像原版扫雷一样,炸开直至看到是地雷的序号
//需要我们去递归进行操作
jifa(mine, show, row, col, x, y);
displayboard(show, row, col);
//新增加是否标记功能
printf("是否标记:Y,不需要标记:N\n");
while ((ch = getchar()) != '\n');//剔除掉我们最喜欢摁的回车
scanf("%c", &ch);
switch (ch)
{
case 'Y':
biaoji(show, row, col);
break;
default:
break;
}
}
}
else
{
printf("字符非法,请重新输入:");
}
}
if (win >= row * col - number)
{
printf("扫雷成功");
}
else
{
printf("扫雷失败\n");
}
}
我们在这里需要着重介绍的几个点
统计模块
激发模块
标记模块
标记模块和统计模块其实相对来说比较简单就是把判断是否是未知区域,是的话我们想标记那就改成! 仅此而已
//标记模块
void biaoji(char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
while (1)
{
printf("请输入要标记的坐标:");
scanf("%d%d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] == '*')
{
show[x][y] = '!';
break;
}
else
{
printf("输入非法,请重新输入:\n");
}
}
else
{
printf("输入非法,请重新输入:\n");
}
}
displayboard(show, row, col);
}
统计模块我用的是纯手算的方法
就是将他周围九宫格内所有的数字加以计算
//统计模块
// x - 1 y - 1 x - 1 y x - 1 y + 1
// x y - 1 x y + 1
// x + 1y - 1 x + 1 y x + 1 y + 1
int tongji(char mine[ROWS][COLS], int x, int y)
{
//因为我们是字符0,地雷为字符1 ,根据ASICII码值我们得出结论‘1’-‘0’=1
return mine[x - 1][y] +
mine[x - 1][y - 1] +
mine[x - 1][y + 1] +
mine[x][y - 1] +
mine[x - 1][y + 1] +
mine[x + 1][y - 1] +
mine[x + 1][y] +
mine[x + 1][y + 1] - 8 * '0';
}
这里有个小技巧 这也是为什么我们埋雷用的是字符1 记住是char 而不是int 因为相对ASIC码值来说我们‘1’ 和‘0’相减还是相差1 那不就是周围雷的数目
下面是最重要的激发模块
我们逐个逐个详细说明
//激发模块
void jifa(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
//激发面对的是如果
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
int z = tongji(mine, x, y);
if (z == 0)
{
//把附近没有地雷的位置变成字符 “空格”
show[x][y] = ' ';
int i = 0;
//向四周共8个位置递归调用
for (i = x - 1; i <= x + 1; i++)//3行
{
int j = 0;
for (j = y - 1; j <= y + 1; j++)//3列
{
if (show[i][j] == '*')
{
jifa(mine, show, row, col, i, j);
}
}
}
}
else
{
show[x][y] = z + '0';
}
}
}
就是怎么说这个函数我们既传递了地雷块也传递了显示模块我们先int z 加入当前点位 x y的统计数 然后递归激发 从当前点位触发3行3列为了以防撞到边界和对自身的重定义 边界的话我们就控制x y的大方位在 x >= 1 && x <= row && y >= 1 && y <= col 1到9 然后 防止重复激发就用
if (show[i][j] == '')
{
jifa(mine, show, row, col, i, j);
}
如果仅在位才能激发
完结下面是全部代码
project.c
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h >
#include<stdlib.h>
#include<time.h>
#include"project.h"
void play();
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();//打印菜单栏
printf("请输入数字进行选择:");
scanf("%d", &input);
switch (input)
{
case 1:
printf("游戏开始了:\n");
play();
break;
case 0:
printf("结束\n");
break;
default:
printf("请重新输入:\n");
break;
}
} while (input);
}
void play()
{
char mine[ROWS][COLS] = { 0 };//地雷模块
char show[ROWS][COLS] = { 0 };//用户模块
//设置完两个棋盘之后初始化一下
initboard(mine, ROWS, COLS, '0');//初始化埋雷表格,置入字符‘0’
initboard(show, ROWS, COLS, '*');
//displayboard(mine, ROW, COL);//地雷模块我们一般显示只是为了确认正确性
// displayboard(show, ROW, COL);//显示模块是显示在屏幕上的我们把自己增加的外围去掉用ROW
//显示完成之后开始埋雷 在mine上完成
mailei(mine, ROW, COL);//埋雷
// displayboard(mine, ROW, COL);
displayboard(show, ROW, COL);//display只是为了调试时候方便 最后哪个不好看,自己删去即可
//雷埋好了,也显示出来了我们需要去排雷
pailei(mine, show, ROW, COL);
}
project.h
#define ROWS 11
#define COLS 11
#define ROW 9
#define COL 9
#define number 10
//菜单
void menu();
//初始化
void initboard(char board[ROWS][COLS], int rows, int cols, char ret);
//显示
void displayboard(char board[ROWS][COLS], int row, int col);
//埋雷
void mailei(char board[ROWS][COLS], int row, int col);
//排雷模块
void pailei(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
//统计模块
int tongji(char board[ROWS][COLS], int row, int col);
//激发模块
void jifa(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y);
//标记模块
void biaoji(char show[ROWS][COLS], int row, int col);
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h >
#include<stdlib.h>
#include<time.h>
#include"project.h"
//菜单 菜单 菜单
void menu()
{
printf("************************\n");
printf("************************\n");
printf("******1.游戏开始********\n");
printf("******0.退出游戏********\n");
printf("************************\n");
printf("************************\n");
}
// 初始化 初始化 初始化
void initboard(char board[ROWS][COLS], int rows, int cols, char ret)
{
int i = 0;
int j = 0;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
board[i][j] = ret;
}
}
}
//显示模块 显示模块 显示模块
void displayboard(char board[ROWS][COLS], int row, int col)
{
//依次打印值喽
int i = 0;
int j = 0;
for (i = 0; i <= row; i++)//第一行打印 数字序列 1到9
{
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 mailei(char board[ROWS][COLS], int row, int col)
{
//我们需要电脑生成随机数。随机生成地雷
int x = 0;
int y = 0;
int count = number;//头文件定义雷的数量是10个
while (count)//怎么保证有指定数量的雷呢,这里用count--
{
x = rand() % row + 1;//指的是1-9
y = rand() % col + 1;
if (board[x][y] == '0')
{
board[x][y] = '1';
count--;
}
}
}
//排雷模块
void pailei(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;
char ch = 0;
while (win < row * col - number)
{
printf("请输入雷区坐标:\n");//让用户输入坐标
scanf("%d%d", &x, &y);
//输入坐标我们首先判断是不是无效坐标
if (x >= 1 && y >= 1 && x <= row && y <= col)
{
if (mine[x][y] == '1')
{
printf("你输了\n");
displayboard(mine, ROW, COL);
break;
}
else
{
//此处不是地雷,我们需要激发周围的对象,就像原版扫雷一样,炸开直至看到是地雷的序号
//需要我们去递归进行操作
jifa(mine, show, row, col, x, y);
displayboard(show, row, col);
//新增加是否标记功能
printf("是否标记:Y,不需要标记:N\n");
while ((ch = getchar()) != '\n');//剔除掉我们最喜欢摁的回车
scanf("%c", &ch);
switch (ch)
{
case 'Y':
biaoji(show, row, col);
break;
default:
break;
}
}
}
else
{
printf("字符非法,请重新输入:");
}
}
if (win >= row * col - number)
{
printf("扫雷成功");
}
else
{
printf("扫雷失败\n");
}
}
//统计模块
// x - 1 y - 1 x - 1 y x - 1 y + 1
// x y - 1 x y + 1
// x + 1y - 1 x + 1 y x + 1 y + 1
int tongji(char mine[ROWS][COLS], int x, int y)
{
//因为我们是字符0,地雷为字符1 ,根据ASICII码值我们得出结论‘1’-‘0’=1
return mine[x - 1][y] +
mine[x - 1][y - 1] +
mine[x - 1][y + 1] +
mine[x][y - 1] +
mine[x - 1][y + 1] +
mine[x + 1][y - 1] +
mine[x + 1][y] +
mine[x + 1][y + 1] - 8 * '0';
}
//激发模块
void jifa(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
int z = tongji(mine, x, y);
if (z == 0)
{
//把附近没有地雷的位置变成字符 “空格”
show[x][y] = ' ';
int i = 0;
//向四周共8个位置递归调用
for (i = x - 1; i <= x + 1; i++)//3行
{
int j = 0;
for (j = y - 1; j <= y + 1; j++)//3列
{
if (show[i][j] == '*')
{
jifa(mine, show, row, col, i, j);
}
}
}
}
else
{
show[x][y] = z + '0';
}
}
}
//标记模块
void biaoji(char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
while (1)
{
printf("请输入要标记的坐标:");
scanf("%d%d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] == '*')
{
show[x][y] = '!';
break;
}
else
{
printf("输入非法,请重新输入:\n");
}
}
else
{
printf("输入非法,请重新输入:\n");
}
}
displayboard(show, row, col);
}