扫雷C语言实现[史上最详细版本!][看完包会!][清晰讲解!]
本文介绍✅: 相信大家都非常熟悉扫雷游戏,基本上都玩过,但是它里面的具体逻辑到底是怎样实现的呢?所以本文将一步步讲解扫雷逻辑的具体C语言实现,希望能给大家带来收获🧡
目录
下面先给大家展示咱们熟悉的老朋友
很明显除了一个棋盘之外,左边数字代表能使用的旗子🚩数量,右边是我们玩的时间
所以下面开始我们的扫雷游戏创造之旅吧!🔥
一、 棋盘初始化🧮
定义Init_board函数,传入二维数组,行、列,以及set(就是我们要将棋盘内容赋值成什么)
咱们需要两个棋盘来实现,一个mine二维数组来放雷、一个show二维数组给玩家展示
- mine初始化内容为 ‘0’
- show初始化内容为 ‘*’
初始化棋盘
void Init_board(char arr[ROWS][COLS], int rows, int cols, char set)
{
int i = 0;
for (i = 0; i < rows; i++)
{
int j = 0;
for (j = 0; j < cols; j++)
{
arr[i][j] = set;
}
}
}
通过打印---
、|
间隔符,以及合适的换行打印棋盘,这里就不一一赘述了(重点不在这儿😂)
具体代码如下:
// 展示棋盘
void show_board(char arr[ROWS][COLS], int row, int col)
{
printf("------------------扫雷-------------------\n");
int i = 0;
for (i = 0; i <= row; i++)//打印行号
{
printf(" %d ", i);
}
printf("\n");
printf("\n");
for (i = 1; i <= col; i++)
{
int j = 0;
printf(" %d ", i);//打印列号
//打印棋子行
for (j = 1; j <= col; j++)
{
printf(" %c ", arr[i][j]);
if (j <= col - 1)
{
printf("|");
}
}
printf("\n");
//打印分隔行
if (i <= row - 1)
{
printf(" ");
for (j = 1; j <= col; j++)
{
printf("---");
if (j <= col - 1)
{
printf("|");
}
}
printf("\n");
}
}
printf("------------------扫雷-------------------\n");
printf("\n");
}
效果图如下
二、 布置雷💣
通过调用rand函数,获取随机值,布雷
这里不知道rand函数怎么使用的,可以看作者的这一篇文章,里面有详细介绍:三子棋-最完整版本(C语言实现)
// 布置雷
void set_mine(char mine[ROWS][COLS], int row, int col)
{
int count = EASY_COUNT;
int x = 0;
int y = 0;
while (count)
{
x = rand() % row + 1;
y = rand() % col + 1;
if (mine[x][y] == '0')
{
mine[x][y] = '1'; // 布置雷
count--;
}
}
}
三、 排查雷💣
从这张图我们可以看到,里面展开的格子有的是空的,有的有数字,当然玩过扫雷的都知道,空的格子代表周围没有雷,数字代表这个格子周围八个格子共有几个雷,OK,有了这条信息就好办了。
3.1 统计坐标周围的雷💣
// 统计输入坐标周围有几个雷
int get_mine_count(char mine[ROWS][COLS], int x, int y)
{
// '0' - '0' = 0; '1' - '0' = '1'
// 周围的
return mine[x - 1][y] +
mine[x - 1][y - 1] +
mine[x][y - 1] +
mine[x + 1][y - 1] +
mine[x + 1][y] +
mine[x + 1][y + 1] +
mine[x][y + 1] +
mine[x - 1][y + 1] - 8 * '0';
}
字符相加是相加的ASCII码值,因为字符0与字符1的ASCII码值分别为48和49,所以最后要减去8 * ‘0’
3.2 爆炸式展开💥
这个坐标是否爆炸式展开咱们必须要知道它的逻辑
实现扫雷游戏炸开的条件✅
- 1、该坐标不是雷
- 2、该坐标周围八个个坐标也不是雷
- 3、该坐标未被排查过
// 炸开式展开
void expand_show(char show[ROWS][COLS], char mine[ROWS][COLS], int x, int y, int row, int col)
{
// SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
// 前面判断过坐标的合法性了,这里就不需要重复判断了
int count = get_mine_count(mine, x, y); // 统计周围地雷数量
if (count == 0) // 当这个坐标的周围都没有雷,就开始递归
{
// 当count == 0 由这个点展开的过程中,此时win会少+1
// 因为当这个点周围有雷的时候,win才会++
show[x][y] = ' '; // 将周围坐标没有雷的坐标赋值为空格
// 查看周围的坐标的周围有没有雷,直到有雷停止递归
// 递归展开
int i = 0;
for (i = x - 1; i <= x + 1; i++)
{
int j = 0;
for (j = y - 1; j <= y + 1; j++)
{
if (show[i][j] == '*') // 当这个位置未被排查过, 并且解决边界坐标越界问题
{
expand_show(show, mine, i, j, row, col);
}
}
}
}
// 若四周有雷将雷的个数放到这个格子中去
else
{
show[x][y] = count + '0';
}
}
3.3 标记雷🚩
当我们能一次性连续展开一片空间后,这个时候我们就需要是否来标记一个坐标是否是雷,具体逻辑如下:
- 1、这个位置是雷,我们就插上一个🚩
- 2、当不确定这个位置是雷的时候,我们也可以插上🚩
- 3、当没有🚩的时候,提醒玩家旗子用完了
- 4、若这个地方插上了🚩,但这个地方不是雷,后面还可以展开这个空间
🚩加上一点💥排坐标和插旗子的逻辑是一样的,所以我们要给玩家自行选择的权力。
有了上面的逻辑思路,具体代码也就可以实现了:
void sign_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int* count_mine, int* count)
{
int x = 0;
int y = 0;
// 初始化我们能使用的旗帜数量
while (1)
{
printf("请输入你要标记雷的坐标: ");
scanf("%d %d", &x, &y);
if (x > 0 && x <= row && y > 0 && y <= col) // 判断坐标是否合法
{
if (show[x][y] == '*' && (*count) >= 1) // 当这个位置没被标记过,且旗子大于0的时候才给这个位置标记为!
{
(*count)--;
if (mine[x][y] == '1') // 当标记的位置是真正的雷,count_mine,这个信息不会告诉给玩家
{
//printf("标记成功\n");
(*count_mine)++;
}
if ((*count) < 1)
{
printf("你现在没有旗子啦,不能标记了\n");
break;
}
if ((*count) >= 1 && (*count) <= EASY_COUNT)
{
printf("你现在还剩下%d个旗子\n", (*count));
}
show[x][y] = '!'; // 只要这个位置没有被展开过就插上旗帜 : '!'
break;
}
else if (show[x][y] == '!')
{
printf("此坐标已经标记过了,请重新输入\n");
}
else if (show[x][y] == '*' && (*count) < 1)
{
printf("你没有旗子呐!不能标记了\n");
}
}
else
{
printf("输入的坐标有误请重新输入:\n");
}
}
}
3.4 统计时间
我们还需要一个函数去统计玩家玩这个游戏的时间,逻辑非常好实现,需要注意的是不要忘了调用库函数<time.h>
,具体代码如下:
// 获得当前的时间(秒为单位)
int gain_time()
{
// 创建三个变量 存储当前时间的 -> 时、分、秒
int ps = 0; // s
int pm = 0; // min
int ph = 0; // h
int sums = 0;
time_t timep;
time(&timep);
struct tm* p;
p = gmtime(&timep);
ps = p->tm_sec; // 当前时间 秒
pm = p->tm_min; // 当前时间 分
ph = p->tm_hour; // 当前时间 时
sums = ps + pm * 60 + ph * 3600;
return sums; // 获取当前时间的总秒数
}
四、 判断游戏胜利🏆
当实现了上面主要的游戏部分,现在要实现怎么去判定玩家胜利
玩过扫雷的都知道,想要胜利无非就是两种情况
4.1 游戏胜利逻辑
- 1、当我们把每一个旗子都插到有雷的格子中,即
(用旗子插上的格子数量 == 总的雷的数量)
,这种情况就可以直接跳出函数,直接判断游戏胜利 - 2、当我们
所有格子的数量 - 打开格子的数量(包含插上旗子的格子) == (总的雷的数量 - 成功标记雷的数量)
确实有点绕这个逻辑,那我们直接来看这两个语言逻辑对应的代码
// win 1:
if (count_mine == EASY_COUNT)
{
gain_win(mine, show, ROW, COL, start_time);
return 0; // 当胜利的时候直接跳出函数
}
// win 2:
if ((ROW * COL - space) == (EASY_COUNT - count_mine)) // 当剩下的空间(没排查的空间) == 剩余雷的数量
{
gain_win(mine, show, ROW, COL, start_time);
return 0; // 当胜利的时候直接跳出函数
}
4.2 统计展开空间数量
当有了上面的两个逻辑,咱们就需要的一个数据就是space
,用它来统计展开的空间数量,只需要在show二维数组中判断这个位置是否是'*'
(代表这个空间还没开辟),不是的话,count++
就行了,具体代码如下
// 统计问展开空间的数量 (方面后续游戏胜利逻辑判断)
int count_space(char show[ROWS][COLS], int row, int col)
{
int count = 0;
int i = 0;
for (i = 1; i <= row; i++) // show 二维元组是 11*11的 ,咱们不需要同统计周围那一圈的数据
{
int j = 0;
for (j = 1; j <= col; j++)
{
if (show[i][j] != '*')
{
count++;
}
}
}
return count;
}
🏳️🌈需要格外注意的就是,在训练show数组的时候,咱们默认玩家不知道数组是从0开始的,只知道玩的时候几行几列,并且show数组是11X11的,玩家看到的只是9X9的,周围还有的空间,是看不到的,所以我们需要从列表的第二个元素开始循环,来统计空间
五 游戏总逻辑🌳
实现了上面的几个子函数逻辑,最后咱们只需要把它放到主函数逻辑中,最后实现如下
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
void menu()
{
printf("***************************\n");
printf("**********1. play**********\n");
printf("**********0. exit**********\n");
printf("***************************\n");
}
void come_game()
{
// mine 数组是用来存放布置好的雷的信息
char mine[ROWS][COLS] = { 0 };
// show 数组是用来存放排查出的雷的信息
char show[ROWS][COLS] = { 0 };
// 初始化棋盘
Init_board(mine, ROWS, COLS, '0');
Init_board(show, ROWS, COLS, '*');
// 打印棋盘
//show_board(mine, ROW, COL);
show_board(show, ROW, COL);
// 布置雷
set_mine(mine, ROW, COL);
//show_board(mine, ROW, COL); // 查看雷的位置
// 排查雷
fine_mine(mine, show, ROW, COL);
}
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
// 使用do while 语句至少循环一次
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hConsole, FOREGROUND_GREEN | FOREGROUND_INTENSITY);
do
{
menu();
printf("请输入(1:进入游戏 0:退出游戏):");
scanf("%d", &input);printf("\n");
switch (input)
{
case 1:
come_game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误\n");
}
} while (input);
return 0;
}
六 完整的代码 CODE
为什么要分成三个文件呢?
这是因为在C语言中,通常将程序分为多个文件来组织代码,以便更好地管理和维护代码。test.c
文件通常包含主函数,用于测试和调用其他函数;game.c
文件包含实现游戏逻辑的函数;game.h
文件包含函数声明和结构体定义等头文件信息,供其他文件调用。这种分离的方式可以提高代码的可读性和可维护性。
6.1 test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
void menu()
{
printf("***************************\n");
printf("**********1. play**********\n");
printf("**********0. exit**********\n");
printf("***************************\n");
}
void come_game()
{
// mine 数组是用来存放布置好的雷的信息
char mine[ROWS][COLS] = { 0 };
// show 数组是用来存放排查出的雷的信息
char show[ROWS][COLS] = { 0 };
// 初始化棋盘
Init_board(mine, ROWS, COLS, '0');
Init_board(show, ROWS, COLS, '*');
// 打印棋盘
//show_board(mine, ROW, COL);
show_board(show, ROW, COL);
// 布置雷
set_mine(mine, ROW, COL);
//show_board(mine, ROW, COL); // 查看雷的位置
// 排查雷
fine_mine(mine, show, ROW, COL);
}
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
// 使用do while 语句至少循环一次
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hConsole, FOREGROUND_GREEN | FOREGROUND_INTENSITY);
do
{
menu();
printf("请输入(1:进入游戏 0:退出游戏):");
scanf("%d", &input);printf("\n");
switch (input)
{
case 1:
come_game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误\n");
}
} while (input);
return 0;
}
6.2 game.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define EASY_COUNT 10 // 简单版本生成10个雷
// 初始化棋盘
void Init_board(char arr[ROWS][COLS], int rows, int cols, char set);
// 打印
void show_board(char arr[ROWS][COLS], int row, int col);
// 布置雷
void set_mine(char mine[ROWS][COLS], int row, int col);
// 排查雷
int fine_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
6.3 game.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
// 初始化棋盘
void Init_board(char arr[ROWS][COLS], int rows, int cols, char set)
{
int i = 0;
for (i = 0; i < rows; i++)
{
int j = 0;
for (j = 0; j < cols; j++)
{
arr[i][j] = set;
}
}
}
// 展示棋盘
void show_board(char arr[ROWS][COLS], int row, int col)
{
printf("------------------扫雷-------------------\n");
int i = 0;
for (i = 0; i <= row; i++)//打印行号
{
printf(" %d ", i);
}
printf("\n");
printf("\n");
for (i = 1; i <= col; i++)
{
int j = 0;
printf(" %d ", i);//打印列号
//打印棋子行
for (j = 1; j <= col; j++)
{
printf(" %c ", arr[i][j]);
if (j <= col - 1)
{
printf("|");
}
}
printf("\n");
//打印分隔行
if (i <= row - 1)
{
printf(" ");
for (j = 1; j <= col; j++)
{
printf("---");
if (j <= col - 1)
{
printf("|");
}
}
printf("\n");
}
}
printf("------------------扫雷-------------------\n");
printf("\n");
}
// 显示雷的位置
void shows_board(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int i = 0;
for (i = 0; i <= row; i++)
{
int j = 0;
for (j = 0; j <= col; j++)
{
if (mine[i][j] == '1')
{
show[i][j] = '@'; // 用@代表雷的信息,展示给玩家
}
}
}
show_board(show, ROW, COL);
}
// 布置雷
void set_mine(char mine[ROWS][COLS], int row, int col)
{
int count = EASY_COUNT;
int x = 0;
int y = 0;
while (count)
{
x = rand() % row + 1;
y = rand() % col + 1;
if (mine[x][y] == '0')
{
mine[x][y] = '1'; // 布置雷
count--;
}
}
}
// 统计输入坐标周围有几个雷
int get_mine_count(char mine[ROWS][COLS], int x, int y)
{
// '0' - '0' = 0; '1' - '0' = '1'
// 周围的
return mine[x - 1][y] +
mine[x - 1][y - 1] +
mine[x][y - 1] +
mine[x + 1][y - 1] +
mine[x + 1][y] +
mine[x + 1][y + 1] +
mine[x][y + 1] +
mine[x - 1][y + 1] - 8 * '0';
}
// 实现扫雷游戏炸开的条件
// 1、该坐标不是雷
// 2、该坐标周围八个个坐标也不是雷
// 3、该坐标未被排查过
//
// 炸开式展开
void expand_show(char show[ROWS][COLS], char mine[ROWS][COLS], int x, int y, int row, int col)
{
// SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
// 前面判断过坐标的合法性了,这里就不需要重复判断了
int count = get_mine_count(mine, x, y); // 统计周围地雷数量
if (count == 0) // 当这个坐标的周围都没有雷,就开始递归
{
// 当count == 0 由这个点展开的过程中,此时win会少+1
// 因为当这个点周围有雷的时候,win才会++
show[x][y] = ' '; // 将周围坐标没有雷的坐标赋值为空格
// 查看周围的坐标的周围有没有雷,直到有雷停止递归
// 递归展开
int i = 0;
for (i = x - 1; i <= x + 1; i++)
{
int j = 0;
for (j = y - 1; j <= y + 1; j++)
{
if (show[i][j] == '*') // 当这个位置未被排查过, 并且解决边界坐标越界问题
{
expand_show(show, mine, i, j, row, col);
}
}
}
}
// 若四周有雷将雷的个数放到这个格子中去
else
{
show[x][y] = count + '0';
}
}
// 统计问展开空间的数量 (方面后续游戏胜利逻辑判断)
int count_space(char show[ROWS][COLS], int row, int col)
{
int count = 0;
int i = 0;
for (i = 1; i <= row; i++) // show 二维元组是 11*11的 ,咱们不需要同统计周围那一圈的数据
{
int j = 0;
for (j = 1; j <= col; j++)
{
if (show[i][j] != '*')
{
count++;
}
}
}
return count;
}
// 标记雷的位置,并用静态变量来固定地雷的剩余值
// 当我们标记了一个位置的时候,不管这个地方是否为雷,旗帜的数量都减少为1
void sign_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int* count_mine, int* count)
{
int x = 0;
int y = 0;
// 初始化我们能使用的旗帜数量
while (1)
{
printf("请输入你要标记雷的坐标: ");
scanf("%d %d", &x, &y);
if (x > 0 && x <= row && y > 0 && y <= col) // 判断坐标是否合法
{
if (show[x][y] == '*' && (*count) >= 1) // 当这个位置没被标记过,且旗子大于0的时候才给这个位置标记为!
{
(*count)--;
if (mine[x][y] == '1') // 当标记的位置是真正的雷,count_mine,这个信息不会告诉给玩家
{
//printf("标记成功\n");
(*count_mine)++;
}
if ((*count) < 1)
{
printf("你现在没有旗子啦,不能标记了\n");
break;
}
if ((*count) >= 1 && (*count) <= EASY_COUNT)
{
printf("你现在还剩下%d个旗子\n", (*count));
}
show[x][y] = '!'; // 只要这个位置没有被展开过就插上旗帜 : '!'
break;
}
else if (show[x][y] == '!')
{
printf("此坐标已经标记过了,请重新输入\n");
}
else if (show[x][y] == '*' && (*count) < 1)
{
printf("你没有旗子呐!不能标记了\n");
}
}
else
{
printf("输入的坐标有误请重新输入:\n");
}
}
}
// 获得当前的时间(秒为单位)
int gain_time()
{
// 创建三个变量 存储当前时间的 -> 时、分、秒
int ps = 0; // s
int pm = 0; // min
int ph = 0; // h
int sums = 0;
time_t timep;
time(&timep);
struct tm* p;
p = gmtime(&timep);
ps = p->tm_sec; // 当前时间 秒
pm = p->tm_min; // 当前时间 分
ph = p->tm_hour; // 当前时间 时
sums = ps + pm * 60 + ph * 3600;
return sums; // 获取当前时间的总秒数
}
void gain_win(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int start_time)
{
int end_time = gain_time();
printf("恭喜你赢了,成功排查了所有的雷,游玩时长%ds\n", end_time - start_time);
printf("下面是->雷<-的坐标( @ 代表雷哦)\n");
shows_board(mine, show, ROW, COL);
}
// 开始扫雷
int fine_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
// win的逻辑
// 当 res 为0的时候,表明我们的雷已经排完了,也代表游戏胜利
// 当剩余空间全是雷的时候,也表示赢了
int x = 0;
int y = 0;
int count_mine = 0; // 统计成功排出的雷
char str = 0; //
int speed_time = 0; // 统计玩家游戏玩了多久
int start_time = gain_time();
int count = EASY_COUNT; // 初始化为10个旗子
while (1)
{
int input = 0;
int space = 0; // 统计展开的空间
printf("输入1将标记雷的位置,输入2将排查坐标:");
scanf("%d", &input);
switch (input)
{
// 标记雷
case 1:
//printf("需要标注地雷就输入:Y,不需要标注地雷则输入:N\n");
while ((str = getchar()) != '\n'); //清空缓冲区
sign_mine(mine, show, ROW, COL, &count_mine, &count); //标记雷的位置
//printf("count_mine = %d\n", count_mine);
expand_show(show, mine, x, y, ROW, COL);
//system("cls");//清屏
show_board(show, ROW, COL);
//printf("现在res = %d\n", res);
if (count_mine == EASY_COUNT)
{
gain_win(mine, show, ROW, COL, start_time);
return 0; // 当胜利的时候直接跳出函数
}
break;
// 排查坐标
case 2:
printf("请输入要排查的坐标:");
scanf("%d %d", &x, &y); // 这个位置被插入旗子也可以重新排查
if (x >= 1 && x <= row && y >= 1 && y <= col && (show[x][y] == '*' || show[x][y] == '!'))
{
if (mine[x][y] == '1') // 是雷
{
int end_time = gain_time();
printf("你踩到雷了,游戏结束,游玩时长%ds\n", end_time - start_time);
printf("下面显示雷的位置(@代表雷哦),祝你下次好运\n");
shows_board(mine, show, ROW, COL);
return 0;
}
else // 不是雷的情况
{
// 当剩余空间全是雷的时候,也表示赢了
expand_show(show, mine, x, y, ROW, COL);
//system("cls");//清屏
space = count_space(show, ROWS, COLS);
//printf("space = %d\n", count_space(show, ROW, COL));
show_board(show, ROW, COL);
// space 统计此时的已经探索了的空间数量
if ((ROW * COL - space) == (EASY_COUNT - count_mine)) // 当剩下的空间(没排查的空间) == 剩余雷的数量
{
gain_win(mine, show, ROW, COL, start_time);
return 0; // 当胜利的时候直接跳出函数
}
break;
}
}
else
{
printf("坐标输入错误,请重新输入\n");
}
default:
printf("输入错误,请重新输入\n");
break;
}
}
}
七 游戏过程 🎮
最后再给大家看一下我的游戏过程吧!
感谢大家的观看,希望我的这篇文章对你有所帮助或有所启发,有什么疑问,评论区留言吧 ⬇
别忘了三连,大佬们❗❗❗