大家好,在这篇文章中我将分享在VS2022中自己做扫雷小游戏的经验,若有错误还请大家指出。
首先,简单的介绍扫雷游戏j基础玩法:
1.扫雷要把所有非地雷的格子打开就算胜利,踩到地雷格子就失败。
2.非雷方格被打开并显示出方格中的数字,方格中数字则表示其周围的8个方格隐藏了几颗雷,如果点开的格子周围有0颗雷,则周围格子自动打开。
接下来要在VS2022中准备好三个文件,一是 game.h 头文件(游戏函数的声明与引用),二是 game.c 源文件(游戏函数的实现与更改),三是 test.c 源文件(游戏界面与大致框架)。
一、扫雷游戏的界面实现
制作简单的界面,也需要将各个函数分配清楚。这一部分我们只要准备好四个函数在 test.c源文件 中就可以将框架完成:1.主函数 main函数 、2.游戏流程 test函数 、3.游戏菜单 menu函数 、 4.游戏执行 game函数。
当然,为了自定义函数和库函数的方便使用,应该将库函数头文件放入 game.h 头文件中,在其他两个源文件中引用 game.h 头文件。
1.主函数 main函数
main函数是程序的入口,很重要,所以里面的内容要精简点:
2.游戏流程 test函数
游戏流程函数的实现要从玩家视角去思考:首先玩家打开游戏会出现游戏界面,然后玩家选择玩游戏则进入游戏执行函数,玩家选择退出游戏则程序结束:
玩家选择操作用一个变量来实现,而由于游戏流程至少要执行一次,恰好do-while循环有这个特点,所以选择do-while循环,紧接着用switch语句来实现分支操作,最后 test函数 不需要返回值则用 void 类型。
这里的 do-while循环 和 switch语句 能做到一个较好的联系:当 input变量 为1,执行到 while判断 时则为真,就进入下一次循环,当 input变量 为0,执行到 while判断 时则为假,就跳出循环。
3.游戏菜单 menu函数
游戏菜单只需使用 printf库函数 便可实现,也不用返回值,但要注意与 test函数 中 swich语句 实现的功能相对应:
4.游戏执行 game函数
游戏执行 game函数 包含两个基本函数:数据初始化函数(initBoard)、打印画面函数(display)。还有实现扫雷游戏核心的 布置雷函数(setMine)、找雷函数(findMine) 和 展开函数(spread)。这里只是梳理一下 game函数 的大致内容,由于会联系到扫雷的核心代码,所以细致的讲解会在下一部分。
总之,界面实现部分很简单,这个界面也适用于其他游戏,不需要费多少脑筋。
二、扫雷游戏的基础内容实现
这一部分会实现扫雷的核心代码,主要分为:1.扫雷的前期布置、2.初始化函数(initBoard)、3.打印函数(display)、4.布置雷函数(setMine)、5.找雷函数(findMine)、6.展开函数(spread)。
1.扫雷的前期布置
我们以简单模式(9×9)的扫雷来讲解:
在9×9的扫雷模式里增加边框(红色图形,也就是增加数组大小),可方便边角计算雷的数量,防止越界,且利于玩家输入的坐标与数组下标对应。
所以接下来:
使用 #define 定义ROW、COL、ROWS、COLS这四个标识符,方便后面括大或缩小棋盘。
而扫雷的棋盘准备两个:一个是 mine 放置雷,二个是 show 展示给玩家看。当然,两者数组长度应该保持一样为11×11:
2.初始化函数(initBoard)
初始化函数目标是将 mine 与 show 初始化为游戏开始的状态,所以这里将 mine 所有的元素(11×11)初始化成字符 ' 0 ' ,show 所有的元素(11×11)则初始化为字符 ' * '。前者初始化成字符 ' 0 ' ,是方便随机放入字符 ' 1 ' 的雷时利于计算,而后者初始化的字符可自己随意设计。
在 game.h头文件 中:
在 game.c源文件 中:
在 test.c源文件 中:
这个函数要注意的是初始化 mine 和 show 时的字符,只需增加一个参数 char set 来接收要用于初始化的字符即可。
3.打印函数(display)
打印函数的目的是将棋盘呈现给玩家,传参的个数应该为9×9。
在 game.h头文件 中:
在 game.c源文件 中:
在 test.c源文件 中:
程序执行的效果:
4.布置雷函数(setMine)
布置雷函数目的是在 mine 初始化后随机地放置雷在 mine 中,需要设置srand与rand函数。
在 game.h头文件 中:
在 game.c源文件 中:
在 test.c源文件 中:
程序执行效果:
5.找雷函数(findMine)
找雷函数中内容包含:①.玩家操作、②.计算雷的个数函数(mineCount)、③.游戏输赢判定。
先布置完毕框架:
在 game.h头文件 中:
在 game.c源文件 中:
在 test.c源文件 中:
以下实现找雷函数的代码都在 game.c源文件 中:
①.玩家操作
玩家查找时输入的坐标需要判定:一.是否超出扫雷方格的范围、二.是否被占用、三.是否是雷。
②.计算雷的个数函数(mineCount)
要计算坐标周围的雷的数量,我们需要知道其周围的坐标,如图:
通过图片,我们就可以清楚地写出代码了:
程序展示:
注意,计算雷的个数时要加上字符 ' 0 ' ,因为 count 得到的是整型数字的雷的个数,要把整型数字转为字符数字要加上' 0 ' (字符' 0 '的ASCII为48,换成加48也一样)
③.游戏输赢判定
游戏进行时有三种状态:1.查找的坐标不是雷,坐标也没查找完、2.查找到雷,游戏失败、3.查找了所有的非雷坐标,游戏胜利。
这里设置了 win变量 计算查找到非雷的坐标数量,row * col - EASY_COUNT = 71,则需要找到非雷的71个坐标。 ( 9 × 9 - 10 )= 71
程序展示(这里将EASY_COUNT改成80,只需要找1个非雷坐标):
6.展开函数(spread)
展开函数目的是缩短找雷时间,这里用递归可以很方便的完成,但要明确条件:
1.当查找的坐标不是雷且周围没有雷则进入递归。
2.递归时遇到周围有雷用数字标记的坐标则停下。(注意坐标仅限于9×9,不要把边框计算在内)
3.递归一次后要改变坐标为非字符' * ',防止反复递归同一坐标进入死循环。
在 game.h头文件 中:
在 game.c源文件 中:
程序展示:
★注意要把计算非雷数量的 win变量 的地址传进去,在递归一次后解引用 spread函数 中的 win指针变量 让其+1,不然即便找到所有的非雷坐标也不会赢。
7.扫雷游戏基础内容源代码
game.h头文件:
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <time.h> // time 函数需要的头文件
#include <stdlib.h> // rand 与 srand 函数需要的头文件
#define ROW 9
#define COL 9
#define ROWS ROW + 2
#define COLS COL + 2
#define EASY_COUNT 10 // 雷的数量
void initBoard(char arr[ROWS][COLS], int rows, int cols, char set);
void display(char arr[ROWS][COLS], int row, int col);
void setMine(char mine[ROWS][COLS], int row, int col);
void findMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
void spread(char mine[ROWS][COLS], char show[ROWS][COLS], int y, int x, int* win);
game.c源文件:
#include "game.h"
void initBoard(char arr[ROWS][COLS], int rows, int cols, char set)
{
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++)
arr[i][j] = set;
}
void display(char arr[ROWS][COLS], int row, int col)
{
int i, j;
printf("上-----扫雷游戏-----上\n");
for (i = 0; i <= col; i++) // 打印列的数量
printf("%d ", i);
printf("\n"); // 换行
for (i = 1; i <= row; i++)
{
printf("%d ", i); // 打印当前的行
for (j = 1; j <= col; j++) // 打印扫雷内容
printf("%c ", arr[i][j]);
printf("\n"); // 换行
}
printf("下-----扫雷游戏-----下\n");
}
void setMine(char mine[ROWS][COLS], int row, int col)
{
int count = 0;
while (count < EASY_COUNT) // 布置雷的数量,数量到达时则跳出循环
{
int x = rand() % col + 1;
int y = rand() % row + 1;
if (mine[y][x] == '0') // 若为 '0',则放置雷
{
mine[y][x] = '1';
count++;
}
}
}
int mineCount(char mine[ROWS][COLS], int y, int x)
{
int count = 0;
for (int i = -1; i <= 1; i++)
for (int j = -1; j <= 1; j++)
if (mine[y + i][x + j] == '1')
count++;
return count;
}
void findMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int y = 0;
int x = 0;
int win = 0;
while (win < row * col - EASY_COUNT)
{
printf("请输入坐标:>");
scanf("%d %d", &y, &x);
if (y >= 1 && y <= row && x >= 1 && x <= col) // 判断输入的坐标是否在9行9列的范围内
{
if (show[y][x] == '*') // 判断输入的坐标是否被占用
{
if (mine[y][x] == '1') // 判断输入的坐标是否是雷
{
printf("很遗憾,你被炸死了\n");
display(mine, ROW, COL); // 打印布置雷的棋盘
break; // 跳出循环
}
else
{
int count = mineCount(mine, y, x);
show[y][x] = count + '0';
win++;
spread(mine, show, y, x, &win);
display(show, row, col);
}
}
else
{
printf("坐标被占用,请重新输入\n");
}
}
else
{
printf("坐标非法,请重新输入\n");
}
}
if (win == row * col - EASY_COUNT)
printf("你赢了\n");
}
void spread(char mine[ROWS][COLS], char show[ROWS][COLS], int y, int x, int* win)
{
if (show[y][x] == '0') // 若周围没有雷才进入
{
for (int i = -1; i <= 1; i++) // 行
{
for (int j = -1; j <= 1; j++) // 列
{
if (y + i >= 1 && y + i <= ROW && x + j >= 1 && x + j <= COL) // 防止超出9×9的范围
{
if (show[y + i][x + j] == '*') // 防止反复递归同一个坐标
{
int count = mineCount(mine, y + i, x + j);
show[y + i][x + j] = count + '0'; // 将已经递归过的坐标显示它周围雷的数量,防止反复递归同一个坐标
(*win)++; // 增加查找非雷坐标的数量
spread(mine, show, y + i, x + j, win); // 进入下一次递归
}
}
}
}
}
}
test.c源文件:
#include "game.h"
void menu()
{
printf("************************\n");
printf("**** 1.play 0.exit ****\n");
printf("************************\n");
}
void game()
{
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
initBoard(mine, ROWS, COLS, '0');
initBoard(show, ROWS, COLS, '*');
setMine(mine, ROW, COL);
display(show, ROW, COL);
//display(mine, ROW, COL);
findMine(mine, show, ROW, COL);
}
void test()
{
int input = 0;
srand((unsigned int)time(NULL)); // 使rand函数产生伪随机数
do
{
menu();
printf("请输入操作:>");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}
以上的内容已经可以实现扫雷游戏。由于我是以函数的视角来讲述扫雷,接下来一些细致的内容会修改之前讲的部分函数,若放在一起讲又不能将整体的逻辑阐述明白,所以我会放在下一部分。
三、扫雷游戏的补充内容实现
这一部分会补充扫雷游戏内容,分别为:1.避雷实现、2.插旗操作、3.数字展开函数(numberSpread)、4.整体界面操作与代码优化。
1.避雷实现
避雷实现指的是第一次查找坐标不会直接找到雷而失败,这里可以将布置雷函数(setMine)放在第一次查找后再布置雷,布置时并避开其查找的3×3的坐标(所以要加查找的坐标参数)。要注意的是,若将 setMine 修改为 “布置雷坐标与第一次查找坐标相等就重新选择行与列”这种功能,这样的效率其实可以改进。
我们可以这样分两种情况:
1.当第一次查找的行和其旁边两行不同于布置雷的行(蓝色为相同,橙色为不同),则列就没有避雷限制:
以第一次查找的坐标6行5列举例子,若在1、2、3、4、8、9行布置雷就不需要限制列了。
2.当第一次查找的行或其旁边两行有等于布置雷的行,则列需要避雷的限制:
以第一次查找的坐标6行5列举例子,若在5、6、7行布置雷就需要限制列不能在4、5、6列。
知道原理,就可以用代码实现了。
在 game.h头文件 中:
在 game.c源文件 中:
在 test.c源文件 中(删除setMine):
·
2.插旗操作
想要插旗操作,则应该在坐标输入后进行选择。
3.数字展开函数(numberSpread)
插旗操作主要是服务于 数字展开函数(numberSpread),而数字展开意思为大于字符 ' 0 ' 的数字坐标会展开周围没插旗的未知坐标。我们要明确这个函数的使用条件:
1.当数字字符(即周围雷的数量)周围未知坐标数量等于自身则不会展开。
2.当数字字符周围旗的数量小于自身则不会展开。
3.当数字字符周围未知且未插旗的坐标为零则不会展开。
在 game.h头文件 中:
在 game.c源文件 中:
对于 numberSpread:
对于 findMine:
红色1、2、4数字处改变了游戏输赢逻辑,红色数字3中的第五个参数 show[y][x] - ' 0 ' 是因为传过去比较的 num变量 是整型,其减字符' 0 ',刚好转换成对应的数字,与计算雷的个数函数(mineCount)同理 。
4.整体界面操作与代码优化
现在的界面操作过于简陋,而且代码中含有一些不必要的 if-else语句嵌套,可更改一下。
①.在行列旁增加 " | "
其方便分清行列与坐标:
程序效果:
②.插入清屏函数。
其所需要的头文件 stdlib.h 已包含,只需在 game.c源文件 的display函数加上一句:
这里要注意,它会把之前的图像与文字清除,若发现一些文字或图像没有出现,看看是不是将display函数放在了其后面,如数字展开函数中讲过的:
程序效果:
③.改变数字颜色
使用 控制台\033方式 改变数字字符的数字颜色:
程序效果:
④.代码优化
这里我实在是想不出其他地方的改动,只将 wantDo变量 周围的代码改动了一下,如果大神能出手提醒还请直接说出来。
5.静态扫雷的源代码
在 game.h头文件 中:
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <time.h> // time 函数需要的头文件
#include <stdlib.h> // rand 与 srand 函数需要的头文件
#define ROW 9
#define COL 9
#define ROWS ROW + 2
#define COLS COL + 2
#define EASY_COUNT 10 // 雷的数量
void initBoard(char arr[ROWS][COLS], int rows, int cols, char set);
void display(char arr[ROWS][COLS], int row, int col);
void setMine(char mine[ROWS][COLS], int row, int col, int y, int x);
void findMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
void spread(char mine[ROWS][COLS], char show[ROWS][COLS], int y, int x, int* win);
void numberSpread(char mine[ROWS][COLS], char show[ROWS][COLS], int y, int x, int num, int* win);
在 game.c源文件 中:
#include "game.h"
void initBoard(char arr[ROWS][COLS], int rows, int cols, char set)
{
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++)
arr[i][j] = set;
}
void display(char arr[ROWS][COLS], int row, int col)
{
system("cls");
int i, j;
printf("上-----扫雷游戏-----上\n|");
for (i = 0; i <= col; i++) // 打印列的数量
printf("%d|", i);
printf("\n"); // 换行
for (i = 1; i <= row; i++)
{
printf("|%d|", i); // 打印当前的行
for (j = 1; j <= col; j++) // 打印扫雷内容
{
if (arr[i][j] == '*')
printf("%c ", arr[i][j]);
if (arr[i][j] == '0')
printf("\033[30m%c\033[0m ", arr[i][j]); // 黑色
if (arr[i][j] == '1')
printf("\033[36m%c\033[0m ", arr[i][j]); // 浅蓝色
if (arr[i][j] == '2')
printf("\033[34m%c\033[0m ", arr[i][j]); // 蓝色
if (arr[i][j] == '3')
printf("\033[33m%c\033[0m ", arr[i][j]); // 黄色
if (arr[i][j] == '4')
printf("\033[31m%c\033[0m ", arr[i][j]); // 红色
if (arr[i][j] == '5')
printf("\033[35m%c\033[0m ", arr[i][j]); // 紫色
if (arr[i][j] == '6')
printf("%c ", arr[i][j]);
if (arr[i][j] == '7')
printf("%c ", arr[i][j]);
if (arr[i][j] == 'F')
printf("\033[32m%c\033[0m ", arr[i][j]); // 绿色
}
printf("\n"); // 换行
}
printf("下-----扫雷游戏-----下\n");
}
void setMine(char mine[ROWS][COLS], int row, int col, int y, int x)
{
int count = 0;
int i, j;
// 记录输入行和旁边的两行
int judgeRow[3] = { 0 };
for (int k = -1, a = 0; k <= 1; k++, a++)
judgeRow[a] = k + y;
// 记录输入列和旁边的两列
int judgeCol[3] = { 0 };
for (int k = -1, a = 0; k <= 1; k++, a++)
judgeCol[a] = k + x;
while (count < EASY_COUNT) // 布置雷的数量,数量到达时则跳出循环
{
i = rand() % col + 1; // 行
// 当输入行且旁边两行与布置行不同时
if (i != judgeRow[0] && i != judgeRow[1] && i != judgeRow[2])
j = rand() % row + 1; // 列
// 当输入行或旁边两行与布置行相同时
else
{
do
{
j = rand() % col + 1;
// 若输入列或旁边两列与布置列相同则进入循环
} while (j == judgeCol[0] || j == judgeCol[1] || j == judgeCol[2]);
}
if (mine[i][j] == '0') // 若为 '0',则放置雷
{
mine[i][j] = '1';
count++;
}
}
}
int mineCount(char mine[ROWS][COLS], int y, int x)
{
int count = 0;
for (int i = -1; i <= 1; i++)
for (int j = -1; j <= 1; j++)
if (mine[y + i][x + j] == '1')
count++;
return count;
}
void select()
{
printf("************************************************\n");
printf("**** 0.返回 1.查找 2.插旗 3.取旗 4.数字展开 ****\n");
printf("************************************************\n");
}
void operation(char mine[ROWS][COLS], char show[ROWS][COLS], int y, int x, int* first, int* win)
{
int wantDo = 0;
do
{
printf("请输入操作:>");
scanf("%d", &wantDo);
switch (wantDo)
{
case 1:
if (show[y][x] == '*') // 判断输入的坐标是否被占用
{
if (*first)
{
setMine(mine, ROW, COL, y, x);
*first = 0;
}
if (mine[y][x] == '1') // 判断输入的坐标是否是雷
{
*win = -9;
}
else
{
int count = mineCount(mine, y, x);
show[y][x] = count + '0';
(*win)++;
spread(mine, show, y, x, win);
display(show, ROW, COL);
}
wantDo = 0;
}
else
printf("坐标被占用,请重新输入\n");
break;
case 2:
if (show[y][x] == '*')
{
show[y][x] = 'F';
display(show, ROW, COL);
wantDo = 0;
}
else
printf("非未知坐标,请重新输入\n");
break;
case 3:
if (show[y][x] == 'F')
{
show[y][x] = '*';
display(show, ROW, COL);
wantDo = 0;
}
else
printf("非插旗坐标,请重新输入\n");
break;
case 4:
if (show[y][x] >= '1' && show[y][x] <= '7')
{
numberSpread(mine, show, y, x, show[y][x] - '0', win);
wantDo = 0;
}
else
printf("非有效坐标,请重新选择\n");
break;
case 0:
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (wantDo);
}
void findMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int y = 0;
int x = 0;
int win = 0;
int first = 1;
while (win < row * col - EASY_COUNT && win >= 0) // 当win为负数意思为被雷炸死
{
printf("请输入坐标:>");
scanf("%d %d", &y, &x);
if (y >= 1 && y <= row && x >= 1 && x <= col) // 判断输入的坐标是否在9行9列的范围内
{
select();
operation(mine, show, y, x, &first, &win);
}
else
printf("坐标非法,请重新输入\n");
}
if (win == row * col - EASY_COUNT)
printf("你赢了\n");
else
{
display(mine, ROW, COL);
printf("很遗憾,你被炸死了\n");
}
}
void spread(char mine[ROWS][COLS], char show[ROWS][COLS], int y, int x, int* win)
{
if (show[y][x] == '0') // 若周围没有雷才进入
{
for (int i = -1; i <= 1; i++) // 行
{
for (int j = -1; j <= 1; j++) // 列
{
if (y + i >= 1 && y + i <= ROW && x + j >= 1 && x + j <= COL) // 防止超出9×9的范围
{
if (show[y + i][x + j] == '*') // 防止反复递归同一个坐标
{
int count = mineCount(mine, y + i, x + j);
show[y + i][x + j] = count + '0'; // 将已经递归过的坐标显示它周围雷的数量,防止反复递归同一个坐标
(*win)++; // 增加查找非雷坐标的数量
spread(mine, show, y + i, x + j, win); // 进入下一次递归
}
}
}
}
}
}
void numberSpread(char mine[ROWS][COLS], char show[ROWS][COLS], int y, int x, int num, int* win)
{
int unknown = 0; // 未知坐标的数量
int unknownBlank = 0;// 未知坐标且未插旗的数量
int FCount = 0; // 插旗坐标的数量
// 记录周围三者的数量
for (int i = -1; i <= 1; i++)
{
for (int j = -1; j <= 1; j++)
{
if (show[y + i][x + j] == '*' || show[y + i][x + j] == 'F')
unknown++;
if (show[y + i][x + j] == 'F')
FCount++;
if (show[y + i][x + j] == '*')
unknownBlank++;
}
}
if (unknown > num) // 周围未知的坐标的数量要大于周围雷的数量
{
if (FCount >= num) // 周围旗子的数量要大于等与周围雷的数量
{
if (unknownBlank != 0) // 周围未知坐标且未插旗的数量不能为零
{
for (int i = -1; i <= 1; i++) // 行
{
for (int j = -1; j <= 1; j++) // 列
{
// 防止进入边框坐标
if (y + i >= 1 && y + i <= ROW && x + j >= 1 && x + j <= COL)
{
// 数字展开时扫到雷
if (mine[y + i][x + j] == '1' && show[y + i][x + j] == '*')
{
(*win) = -9;
}
// 数字展开扫到非雷
else if (show[y + i][x + j] == '*' && mine[y + i][x + j] == '0')
{
int count = mineCount(mine, y + i, x + j);
show[y + i][x + j] = count + '0';
(*win)++;
spread(mine, show, y + i, x + j, win);
}
}
}
}
if (*win > 0)
display(show, ROW, COL);
}
else
printf("周围没有未知且未插旗的坐标,不能展开\n");
}
else
printf("周围旗子数量小于周围雷的数量\n");
}
else
printf("未知坐标数量不大于周围雷的数量\n");
}
在 test.c源文件 中:
#include "game.h"
void menu()
{
printf("************************\n");
printf("**** 1.play 0.exit ****\n");
printf("************************\n");
}
void game()
{
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
initBoard(mine, ROWS, COLS, '0');
initBoard(show, ROWS, COLS, '*');
display(show, ROW, COL);
//display(mine, ROW, COL);
findMine(mine, show, ROW, COL);
}
void test()
{
int input = 0;
srand((unsigned int)time(NULL)); // 使rand函数产生伪随机数
do
{
menu();
printf("请输入操作:>");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}
(Bug提醒):
setMine函数 中有一处错误:
对于行与列相同时此问题没有影响,但是接下来实现的代码行与列会变动,不改会出现Bug.
四.动态扫雷的内容实现
这一部分也会改动前面的代码,需要分开讲述,并且以其主要功能的角度讲解,内容为:1.空间的申请与释放、2.布雷准备、3.自定义行与列的实现、4.整体的代码改动。
1.空间的申请与释放
实现行与列的变动需要 malloc函数 ,头文件为 stdlib.h :
在 game.h头文件 中:
在 game.c源文件 中:
2.布雷准备
这里只需布雷函数:
在 game.h头文件 中:
在 game.c源文件 中:
3.自定义行与列的实现
这里要实现行与列由玩家自定义,则需要两个函数并提前声明输入界限:
在 game.h头文件 中:
在 game.c源文件 中:
4.整体的代码改动
整体的代码大部分都要改动:
在 game.h头文件 中:
在 test.c源文件 中:
在 game.c源文件 中:
上半:
下半:
5.动态扫雷源代码
在 game.h头文件 中:
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <time.h> // time 函数需要的头文件
#include <stdlib.h> // rand、 srand、malloc 函数需要的头文件
void initBoard(char** arr, int rows, int cols, char set);
void display(char** arr, int row, int col);
void setMine(char** mine, int row, int col, int y, int x);
void findMine(char** mine, char** show, int row, int col);
void spread(char** mine, char** show, int y, int x, int* win, int row, int col);
void numberSpread(char** mine, char** show, int y, int x, int num, int* win, int row, int col);
// 动态扫雷实现
char** apply(int* rows, int* cols);
void release(char** arr, int rows, int cols);
int getMine();
void myApply(int* rows, int* cols);
int myGetMine();
在 game.c源文件 中:
#include "game.h"
extern int getMineCount;
void initBoard(char** arr, int rows, int cols, char set)
{
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++)
arr[i][j] = set;
}
void display(char** arr, int row, int col)
{
int count = 0;
system("cls");
int i, j;
if (row <= 9 && col <= 9)
{
for (i = 0; i <= col / 2 - 1; i++)
printf("--");
printf("扫雷");
for (i = 0; i <= col / 2 - 1; i++)
printf("--");
printf("\n|");
for (i = 0; i <= col; i++) // 打印列的数量
printf("%d|", i);
printf("\n"); // 换行
for (i = 1; i <= row; i++)
{
printf("|%d|", i); // 打印当前的行
for (j = 1; j <= col; j++) // 打印扫雷内容
{
if (arr[i][j] == '*')
printf("%c ", arr[i][j]);
if (arr[i][j] == '0')
printf("\033[30m%c\033[0m ", arr[i][j]); // 黑色
if (arr[i][j] == '1')
printf("\033[36m%c\033[0m ", arr[i][j]); // 浅蓝色
if (arr[i][j] == '2')
printf("\033[34m%c\033[0m ", arr[i][j]); // 蓝色
if (arr[i][j] == '3')
printf("\033[33m%c\033[0m ", arr[i][j]); // 黄色
if (arr[i][j] == '4')
printf("\033[31m%c\033[0m ", arr[i][j]); // 红色
if (arr[i][j] == '5')
printf("\033[35m%c\033[0m ", arr[i][j]); // 紫色
if (arr[i][j] == '6')
printf("%c ", arr[i][j]);
if (arr[i][j] == '7')
printf("%c ", arr[i][j]);
if (arr[i][j] == 'F')
{
printf("\033[32m%c\033[0m ", arr[i][j]); // 绿色
count++;
}
}
printf("\n"); // 换行
}
for (i = 0; i <= col / 2 - 1; i++)
printf("--");
printf("雷:\033[31m%d\033[0m", getMineCount - count);
for (i = 0; i <= col / 2 - 1; i++)
printf("--");
printf("\n");
}
else
{
for (i = 0; i <= col / 2 - 1; i++)
printf("---");
printf("扫雷");
for (i = 0; i <= col / 2 - 1; i++)
printf("---");
printf("\n|");
for (i = 0; i <= col; i++) // 打印列的数量
printf("%2d|", i);
printf("\n"); // 换行
for (i = 1; i <= row; i++)
{
printf("|%2d|", i); // 打印当前的行
for (j = 1; j <= col; j++) // 打印扫雷内容
{
if (arr[i][j] == '*')
printf("%2c ", arr[i][j]);
if (arr[i][j] == '0')
printf("\033[30m%2c\033[0m ", arr[i][j]); // 黑色
if (arr[i][j] == '1')
printf("\033[36m%2c\033[0m ", arr[i][j]); // 浅蓝色
if (arr[i][j] == '2')
printf("\033[34m%2c\033[0m ", arr[i][j]); // 蓝色
if (arr[i][j] == '3')
printf("\033[33m%2c\033[0m ", arr[i][j]); // 黄色
if (arr[i][j] == '4')
printf("\033[31m%2c\033[0m ", arr[i][j]); // 红色
if (arr[i][j] == '5')
printf("\033[35m%2c\033[0m ", arr[i][j]); // 紫色
if (arr[i][j] == '6')
printf("%2c ", arr[i][j]);
if (arr[i][j] == '7')
printf("%2c ", arr[i][j]);
if (arr[i][j] == 'F')
{
printf("\033[32m%2c\033[0m ", arr[i][j]); // 绿色
count++;
}
}
printf("\n"); // 换行
}
for (i = 0; i <= col / 2 - 1; i++)
printf("---");
printf("雷:\033[31m%d\033[0m", getMineCount - count);
for (i = 0; i <= col / 2 - 1; i++)
printf("---");
printf("\n");
}
}
void setMine(char** mine, int row, int col, int y, int x)
{
int count = 0;
int i, j;
// 记录输入行和旁边的两行
int judgeRow[3] = { 0 };
for (int k = -1, a = 0; k <= 1; k++, a++)
judgeRow[a] = k + y;
// 记录输入列和旁边的两列
int judgeCol[3] = { 0 };
for (int k = -1, a = 0; k <= 1; k++, a++)
judgeCol[a] = k + x;
while (count < getMineCount) // 布置雷的数量,数量到达时则跳出循环
{
i = rand() % row + 1; // 行
// 当输入行且旁边两行与布置行不同时
if (i != judgeRow[0] && i != judgeRow[1] && i != judgeRow[2])
j = rand() % col + 1; // 列
// 当输入行或旁边两行与布置行相同时
else
{
do
{
j = rand() % col + 1;
// 若输入列或旁边两列与布置列相同则进入循环
} while (j == judgeCol[0] || j == judgeCol[1] || j == judgeCol[2]);
}
if (mine[i][j] == '0') // 若为 '0',则放置雷
{
mine[i][j] = '1';
count++;
}
}
}
int mineCount(char** mine, int y, int x)
{
int count = 0;
for (int i = -1; i <= 1; i++)
for (int j = -1; j <= 1; j++)
if (mine[y + i][x + j] == '1')
count++;
return count;
}
void select(int n)
{
if (n == 1)
{
printf("************************************************\n");
printf("**** 0.返回 1.查找 2.插旗 3.取旗 4.数字展开 ****\n");
printf("************************************************\n");
}
else if (n == 2)
{
printf("**********************\n");
printf("****** 你赢了 ******\n");
printf("**********************\n");
}
else if (n == 3)
{
printf("**********************\n");
printf("**很遗憾,你被炸死了**\n");
printf("**********************\n");
}
}
void operation(char** mine, char** show, int y, int x, int* first, int* win, int row, int col)
{
int wantDo = 0;
do
{
printf("请输入操作:>");
scanf("%d", &wantDo);
switch (wantDo)
{
case 1:
if (show[y][x] == '*') // 判断输入的坐标是否被占用
{
if (*first)
{
setMine(mine, row, col, y, x);
*first = 0;
}
if (mine[y][x] == '1') // 判断输入的坐标是否是雷
{
*win = -9;
}
else
{
int count = mineCount(mine, y, x);
show[y][x] = count + '0';
(*win)++;
spread(mine, show, y, x, win, row, col);
display(show, row, col);
}
wantDo = 0;
}
else
printf("坐标被占用,请重新输入\n");
break;
case 2:
if (show[y][x] == '*')
{
show[y][x] = 'F';
display(show, row, col);
wantDo = 0;
}
else
printf("非未知坐标,请重新输入\n");
break;
case 3:
if (show[y][x] == 'F')
{
show[y][x] = '*';
display(show, row, col);
wantDo = 0;
}
else
printf("非插旗坐标,请重新输入\n");
break;
case 4:
if (show[y][x] >= '1' && show[y][x] <= '7')
{
numberSpread(mine, show, y, x, show[y][x] - '0', win, row, col);
wantDo = 0;
}
else
printf("非有效坐标,请重新选择\n");
break;
case 0:
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (wantDo);
}
void findMine(char** mine, char** show, int row, int col)
{
int y = 0;
int x = 0;
int win = 0;
int first = 1;
while (win < row * col - getMineCount && win >= 0) // 当win为负数意思为被雷炸死
{
printf("请输入坐标:>");
scanf("%d %d", &y, &x);
if (y >= 1 && y <= row && x >= 1 && x <= col) // 判断输入的坐标是否在9行9列的范围内
{
select(1);
operation(mine, show, y, x, &first, &win, row, col);
}
else
printf("坐标非法,请重新输入\n");
}
if (win == row * col - getMineCount)
select(2);
else
{
display(mine, row, col);
select(3);
}
}
void spread(char** mine, char** show, int y, int x, int* win, int row, int col)
{
if (show[y][x] == '0') // 若周围没有雷才进入
{
for (int i = -1; i <= 1; i++) // 行
{
for (int j = -1; j <= 1; j++) // 列
{
if (y + i >= 1 && y + i <= row && x + j >= 1 && x + j <= col) // 防止超出9×9的范围
{
if (show[y + i][x + j] == '*') // 防止反复递归同一个坐标
{
int count = mineCount(mine, y + i, x + j);
show[y + i][x + j] = count + '0'; // 将已经递归过的坐标显示它周围雷的数量,防止反复递归同一个坐标
(*win)++; // 增加查找非雷坐标的数量
spread(mine, show, y + i, x + j, win, row, col); // 进入下一次递归
}
}
}
}
}
}
void numberSpread(char** mine, char** show, int y, int x, int num, int* win, int row, int col)
{
int unknown = 0; // 未知坐标的数量
int unknownBlank = 0;// 未知坐标且未插旗的数量
int FCount = 0; // 插旗坐标的数量
// 记录周围三者的数量
for (int i = -1; i <= 1; i++)
{
for (int j = -1; j <= 1; j++)
{
if (show[y + i][x + j] == '*' || show[y + i][x + j] == 'F')
unknown++;
if (show[y + i][x + j] == 'F')
FCount++;
if (show[y + i][x + j] == '*')
unknownBlank++;
}
}
if (unknown > num) // 周围未知的坐标的数量要大于周围雷的数量
{
if (FCount >= num) // 周围旗子的数量要大于等与周围雷的数量
{
if (unknownBlank != 0) // 周围未知坐标且未插旗的数量不能为零
{
for (int i = -1; i <= 1; i++) // 行
{
for (int j = -1; j <= 1; j++) // 列
{
// 防止进入边框坐标
if (y + i >= 1 && y + i <= row && x + j >= 1 && x + j <= col)
{
// 数字展开时扫到雷
if (show[y + i][x + j] == '*' && mine[y + i][x + j] == '1')
{
(*win) = -9;
}
// 数字展开扫到非雷
else if (show[y + i][x + j] == '*' && mine[y + i][x + j] == '0')
{
int count = mineCount(mine, y + i, x + j);
show[y + i][x + j] = count + '0';
(*win)++;
spread(mine, show, y + i, x + j, win, row, col);
}
}
}
}
if (*win > 0)
display(show, row, col);
}
else
printf("周围没有未知且未插旗的坐标,不能展开\n");
}
else
printf("周围旗子数量小于周围雷的数量\n");
}
else
printf("未知坐标数量不大于周围雷的数量\n");
}
char** apply(int* rows, int* cols)
{
// 申请二级指针(二维数组)行的数量
char** arr = (char**)malloc(sizeof(char*) * (*rows));
if (NULL == arr)
{
printf("游戏异常,已退出\n");
exit(-1);
}
// 申请每行之中列的数量
for (int i = 0; i < *rows; i++)
{
arr[i] = (char*)malloc(sizeof(char) * (*cols));
if (NULL == arr[i])
{
printf("游戏异常,已退出\n");
exit(-1);
}
}
// 返回首元素地址
return arr;
}
void release(char** arr, int rows, int cols)
{
// 先释放一级指针空间
for (int i = 0; i < rows; i++)
free(arr[i]);
// 后释放二级指针的空间
free(arr);
}
int getMine(int num)
{
switch (num)
{
case 1:
return 10;
case 2:
return 40;
case 3:
return 99;
case 4:
return myGetMine();
}
return 0;
}
void myApply(int* rows, int* cols)
{
printf("***********************************************\n");
printf("****************** 提前声明:******************\n");
printf("*自定义行和列建议至少全部都大于3,否者后果自负*\n");
printf("***********************************************\n");
printf("请输入行数:>");
scanf("%d", rows);
if (*rows < 1)
{
*rows = 4;
printf("行数小于1,自动改成4行\n");
}
printf("请输入列数:>");
scanf("%d", cols);
if (*cols < 1)
{
*cols = 4;
printf("列数小于1,自动改成4列\n");
}
(*rows) = (*rows) + 2;
(*cols) = (*cols) + 2;
}
int myGetMine()
{
int n = 0;
printf("***********************************************\n");
printf("****************** 提前声明:******************\n");
printf("******自定义雷的数量建议最多:行 × 列 - 9 *****\n");
printf("*****************否者后果自负******************\n");
printf("请布置雷的数量:>");
scanf("%d", &n);
if (n < 0)
{
printf("数量小于1,改为0");
n = 0;
}
return n;
}
在 test.c源文件 中:
#include "game.h"
int getMineCount = 0;
void menu(int n)
{
if (n == 1)
{
printf("************************\n");
printf("**** 1.play 0.exit ****\n");
printf("************************\n");
}
else if (n == 2)
{
printf("*************************\n");
printf("**** 1.初级(9×9) ****\n");
printf("**** 2.中级(16×16) ****\n");
printf("**** 3.高级(16×30) ****\n");
printf("**** 4.自定义 ****\n");
printf("*************************\n");
}
}
int option(int* rows, int* cols)
{
menu(2);
int choose = 0;
do
{
printf("请选择难度:>");
scanf("%d", &choose);
switch (choose)
{
case 1:
*rows = 11;
*cols = 11;
return 1;
case 2:
*rows = 18;
*cols = 18;
return 2;
case 3:
*rows = 18;
*cols = 32;
return 3;
case 4:
myApply(rows, cols);
return 4;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (choose);
return 1;
}
void game()
{
int rows = 0;
int cols = 0;
int num = option(&rows, &cols);
char** mine = apply(&rows, &cols);
char** show = apply(&rows, &cols);
getMineCount = getMine(num);
int row = rows - 2;
int col = cols - 2;
initBoard(mine, rows, cols, '0');
initBoard(show, rows, cols, '*');
display(show, row, col);
findMine(mine, show, row, col);
release(mine, rows, cols);
release(show, rows, cols);
}
void test()
{
int input = 0;
srand((unsigned int)time(NULL)); // 使rand函数产生伪随机数
do
{
menu(1);
printf("请输入操作:>");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}
结语:
以上是在VS2022中做扫雷小游戏的经验,若有错误等其他问题请大家在评论区指出或私信。
谢谢阅读