前言:本人为C语言初学者,学识尚浅,研究程度存在很大的局限性,眼界很窄。以下所有观点仅代表个人见解和思路,各位游刃有余的前辈可以给予批评和指正!各位与鄙人同路的学子可相互探讨、发表看法,交换观点!
注意:本篇以上篇 《三子棋 - 全思路剖析》 为基础而作,因其过程具有很大的相似性,一些思考方式也大同小异,所以本篇篇幅相对较小(确实少了一丢丢),在三子棋的基础上可以对本篇可以有快速理解。若想明晰逐过程分析,请先移步:https://blog.csdn.net/Thepale2022/article/details/126222228?spm=1001.2014.3001.5501
整体框架
1.main.c - 主函数功能实现,负责调用和游戏逻辑实现
2.game.c - 游戏功能实现函数
3.game.h - 函数的声明、头文件的包括、定义常量等
MAIN.C
首先,如果大伙儿玩过扫雷,应该多少对扫雷有一些印象,空间是 x * y 格的:
这一次,我们以 9 * 9 的空间为例,来讲解扫雷。
对于C语言来说,我们必须先给用户交互选项,所以我们还是照常要打印一个菜单,并且实现能让用户反复游玩游戏的功能:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
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;
}
这一步和三子棋是一模一样的。
然后,我们如果要让用户扫雷,那必须有数组来存储雷的信息和非雷的信息,毋庸置疑,我们肯定是用一个二维数组来搞定。于是很多小伙伴直接就咬定了:char arr[9][9]; 但其实如果我们稍加思考,扫雷每点一下,会提示周围一圈有几个雷,所以我们必然有一个函数负责遍历周围一圈的数据。如果刚好用户选择了顶点坐标,那么必然会出现数组越界,于是,为了避免这一情况发生,我们不如把数组整个扩大一圈,避免越界的问题。如果你无法理解,请见图:
所以我们应该定义这样的数组:char arr[11][11] 当然,为了保证通用性,这里的11会用define定义,这是为了代码的通用性
这个游戏对于用户而言,一开始的信息全是未知的,换句话说,我们给用户看到的数组和我们实际布雷的数组肯定是不一样的,为了方便,我们可以开辟两个数组,一个用来存放雷的信息(mine)一个用来存放用户看到的信息(show),那么我扫雷的时候,在mine中读取信息,然后反馈到show中,只要把这两个数组关联,这个问题就解决了。
所以一旦开始游戏,我们就应该定义两个数组,这里我用0当作无雷,用1当作雷,用户展示界面用*代替。当然,这里完全是自定义的,你想改成什么就改成什么。
细心的小伙伴可能发现,我之前定义数组的时候就制定了char类型,char类型可以直接存储我要展示的字符,你总不能用int存储 * 然后再强制转换为char类型输入吧....(如果你有这闲工夫,当我没说)。我们完全可以先看一眼效果:
我们打印出来的肯定是 9 * 9 的格子,11 * 11是我们后台所制定的一种规定,不需要用户可以看到。所以,说到这儿,常量起码要定义4个了...(其实是5个)
打印数组,先初始化,这样,第一个函数终于要来了:初始化数组函数
GAME.C
初始化数组函数,不需要返回值,只是初始化就好了,所以定义是这样的:
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
(偷偷说一句,这里我已经在game.h里定义了如下内容:)
#include <stdio.h>
#define ROWS 11
#define COLS 11
#define ROW 9
#define COL 9
#define MINE 10
这里我传过去的数组是11*11的数组,初始化肯定是每个都要初始化,这和我把雷设置成1把非雷设置成0是相互应和的,我把数组全初始化为‘0’,只对中间的9*9布雷,这样我在检索顶点坐标时,周围伪数组越界的部分默认判断是没有雷的,懂了不~
初始化也就是两个for循环,这里不赘述了:
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;
}
}
}
初始化完成,我得打印出来瞅瞅,但是这里注意,我初始化的是11*11的,但是我要打印的实际上是9*9的,这里的函数应该这样声明:
void DisplayBoard(char board[ROWS][COLS], int row, int col);
这里传过来的数组是11*11的,但是我们也把要打印的行列信息9*9传过来了,为了方便打印:
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0, j = 0;
for (i = 1; i <= row; i++)
{
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
putchar('\n');
}
}
这样打印出来的结果实在是令人唏嘘......
我们少了哪些东西?对比成品:1.分割线 2.行号列号
两者实现都非常简单,我不赘述了,直接看代码:
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0, j = 0;
printf("------扫雷游戏------\n");
for (j = 0; j <= row; j++) //打印列号
{
printf("%d ", j);
}
putchar('\n');
for (i = 1; i <= row; i++)
{
printf("%d ", i); //打印行号
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
putchar('\n');
}
}
这样就舒服多了:
下一步,我们需要在mine区布雷,雷肯定是随机的,所以我们这里又要取随机值,srand((unsigned int)time(NULL));放在主函数里,然后用rand取随机值,这个我就不多说了,以及game.h里又要来几个新伙伴:
#include <stdlib.h>
#include <time.h>
设置雷函数,实际上是改变数组数据,也不需要返回值,且实际操作的是9*9空间:
void SetMine(char board[ROWS][COLS], int row, int col);
如何让随机数取到1~9之间的值呢?这里可以先思考一下再看代码:
void SetMine(char board[ROWS][COLS], int row, int col)
{
int count = MINE; //记录雷数,布一个减一个
while (count != 0)
{
int x = rand() % row + 1; //行数是1~9
int y = rand() % col + 1; //列数是1~9
if (board[x][y] == '0')
{
board[x][y] = '1';
count--;
}
}
}
这样就可以随机布置10个雷辣!
最关键的时刻要来了...
我们需要实现的功能是:用户给我们一个坐标,然后我们判断这个坐标是否是雷,如果是雷,则游戏结束。如果不是雷,我们需要计算这个坐标周围八个坐标的雷数然后反馈到show数组里打印一次告诉用户周围有几个雷,且游戏继续。
中间还有些许细节问题:
1.用户坐标输入必须合法
2.判断扫雷次数,达到最大row*col-MINE要结束游戏并判断用户胜利
所以这个函数肯定是要挂钩两个数组的,由于判断8个坐标的部分比较繁琐,我们可以单独拿出来实现,所以这个函数也可以定义成void,如果我们把这个函数定义成int,那相关判断就要在主函数内了,不是很方便!在此函数内部实现最好!
void FindMine (char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
while (1)
{
int x = 0;
int y = 0;
int ret = 0;
printf("请输入坐标:");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= 9 && y >= 1 && y <= 9) //检测坐标合法性
{
if (mine[x][y] != '1') //判断该坐标是不是雷
{
ret = Search(mine, x, y);
show[x][y] = ret + '0';
DisplayBoard(show, row, col);
}
else
{
printf("您踩到了雷!游戏结束!\n");
DisplayBoard(mine, row, col);
break;
}
}
else
{
printf("坐标输入非法!请重新输入!\n");
}
}
}
这里定义了Search函数来负责遍历8个坐标,方法很多,你甚至可以用8个if来判断,但是我这里用了for循环遍历,仅供大家参考的一种思路:
int Search(char mine[ROWS][COLS], int x, int y)
{
int i = 0, j = 0;
int sum = 0;
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
sum = sum + (int)mine[i][j];
}
}
return sum - 9 * '0';
}
最后只剩输赢判断了:
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int win = row * col - MINE;
while (win > 0)
{
int x = 0;
int y = 0;
int ret = 0;
printf("请输入坐标:");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= 9 && y >= 1 && y <= 9) //检测坐标合法性
{
if (mine[x][y] != '1') //判断该坐标是不是雷
{
ret = Search(mine, x, y);
show[x][y] = ret + '0';
DisplayBoard(show, row, col);
win--;
}
else
{
printf("您踩到了雷!游戏结束!\n");
DisplayBoard(mine, row, col);
break;
}
}
else
{
printf("坐标输入非法!请重新输入!\n");
}
}
if (win == 0)
{
printf("恭喜您!获胜了!\n");
DisplayBoard(mine, row, col);
}
}
改掉while的循环条件,加上一个变量记录我们行走的步数,走完最后一步的时候自然就获胜了!
最后加上了if判断,其实是为了防止踩到雷游戏结束跳出的时候触发获胜的判断
MAIN.C
main.c :终于想起我了吗?
现在是main中的逻辑实现环节
首先,我们定义数组然后初始化是毋庸置疑的;然后,只需要打印出show数组,mine数组对于用户来讲必须是隐藏的;其次,调用一次FindMine函数即可,因为循环已经在函数内部实现了;然后,然后什么,然后写完了!(突然感觉在写论证思路题)
void game()
{
char mine[ROWS][COLS];
char show[ROWS][COLS];
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
DisplayBoard(show, ROW, COL);
SetMine(mine, ROW, COL);
FindMine(mine, show, ROW, COL);
}
其实还有很多功能可以加装:比如每次要求输入坐标的时候给一个选择,是否标记雷,可以让程序具有标记雷功能(就是在show填一个其他的符号);或者如果用户走的第一步就是雷,能不能尝试改变这个雷的位置不要让用户一开始就死呢?很多可以优化的地方,大家可以打开脑洞勇敢尝试!