前言
所有的逻辑思考我都会放在代码注释中,大家只需要按照main函数一步一步走下去就能明白整体逻辑
game.h
我们一般在.h文件中放上所有需要的头文件、定义、结构体、函数声明
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<windows.h>//Sleep()
#define ROW 12
#define COL 12
#define ROWS ROW + 2
#define COLS COL + 2
#define init_mine '0'
#define init_show '*'
#define mine_number 20
void menu();//菜单
void board_init(char board[ROWS][COLS], int r, int c, char init_num);//初始化
void board_print(char board[ROWS][COLS], int r, int c);//打印棋盘
void board_mine_set(char board[ROWS][COLS], int r, int c);//设置雷
void sweep_mine(char board[ROWS][COLS], char show_board[ROWS][COLS]);//扫雷
int count_mine(char mine_board[ROWS][COLS], int x, int y);//计算周围8个位置雷的数量
void check_clear_mine(char mine_board[ROWS][COLS], char show_board[ROWS][COLS], int x, int y, int sign[ROWS][COLS], int* count);//检查周围的雷,自动扫雷
Main.c
Main.c文件中放整体的外层大逻辑
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
void sweep_game()
{
//我们后面在初始化棋盘和标记雷的时候会用到 * 和 # 所以为了统一,我用了char类型的数组存储棋盘
//此外,我将放雷的棋盘作为一个二维数组,我们自身扫雷的棋盘作为另一个二维数组
//为了方便修改棋盘大小,我在.h文件中define了棋盘的长和宽分别为ROW和COL,又考虑到在检测周围八个位置的数据时会涉及到超出棋盘的位置,
//我将棋盘的长和宽都扩大了两个元素,定义了ROWS和COLS,
//例如ROW×COL -> 9×9 对应起来 ROWS×COLS -> 11×11
char mine_board[ROWS][COLS] = { 0 };
char show_board[ROWS][COLS] = { 0 };
//input记录玩家选择
int input = 0;
do
{
//菜单
menu();
printf("请进行选择:>");
scanf("%d", &input);
switch (input)
{
//玩游戏
case 1:
//在我们选择玩游戏后,就不需要看见菜单了,所以用system("cls")清屏
system("cls");
//棋盘初始化 mine - '0' show - '*'
//init_mine和init_show都是define出来的量,分别就是'0' 和'*'
board_init(mine_board, ROWS, COLS, init_mine);
board_init(show_board, ROWS, COLS, init_show);
//打印棋盘
//这里打印棋盘是为了检测初始化棋盘是否正确,并且方便后期测试
//board_print(mine_board, ROW, COL);
//board_print(show_board, ROW, COL);
//布置雷
//布置雷,我们的基本处理就是rand随机数,如果后期想要在游戏性方面改进,就可以在这里下功夫,比如是下出第一步后再布置雷,保证第一步不会被炸
//或者雷的分布,是用均匀分布,还是正态分布,最终的目的都是为了能让玩家在排雷时可以通过逻辑,而不是猜测。
srand((unsigned)time(NULL));
board_mine_set(mine_board, ROW, COL);
//这里放了一个棋盘打印是为了后期调试,我们可以看着答案填写,做到心里有数
//board_print(mine_board, ROW, COL);
//扫雷
//具体扫雷过程在这个函数中,等等在game.c中详细介绍
sweep_mine(mine_board, show_board);
break;
case 0:
printf("退出游戏!\n");
break;
default:
printf("选择错误,请重新选择\n");
break;
}
} while (input);
}
int main()
{
//我们将整体大逻辑都放在main函数中,再打包放在sweep_game()函数中,这样的目的是:当我们在实现扫雷的过程中,想要单独测试一些函数,
//就不需要一次性注释一大片内容,只需要注释掉sweep_game()这一行内容,然后在主函数中用test1 test2 test3 ... 分别测试单独的函数,十分方便简洁
sweep_game();
return 0;
}
game.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 board_init(char board[ROWS][COLS], int r, int c, char init_num)
{
int i = 0;
for (i = 0; i < r; i++)
{
int j = 0;
for (j = 0; j < c; j++)
{
//让二维数组每一个位置都放上init_num
//放雷棋盘初始化为'0'
//玩家扫雷棋盘初始化为'*'
board[i][j] = init_num;
}
}
}
//打印棋盘
void board_print(char board[ROWS][COLS], int r, int c)
{
//为了棋盘美观
printf("+");
int cp = 0;
for (cp = 0; cp < COL - 1; cp++)
{
printf("-");
}
printf("play");
for (cp = 0; cp < COL - 1; cp++)
{
printf("-");
}
printf("+\n");
//辅助列标,让玩家不需要自己去数多少列
int k = 0;
printf("|");
for (k = 0; k <= c; k++)
{
//这里%10是为了美观,因为大于9的数会占两个位置,导致棋盘错位,因此都用0~9的数字行列行列
printf("%d ", k % 10);
}
printf("|\n");
//打印棋盘内容
int i = 0;
for (i = 1; i <= r; i++)
{
int j = 0;
printf("|");
printf("%d ", i % 10);//打印辅助行标
for (j = 1; j <= c; j++)
{
//打印数组中每个位置的内容
printf("%c ", board[i][j] );
}
printf("|");
printf("\n");
}
//美观
printf("+");
for (cp = 0; cp < COL - 1; cp++)
{
printf("-");
}
printf("play");
for (cp = 0; cp < COL - 1; cp++)
{
printf("-");
}
printf("+\n");
printf("\n");
}
//在放雷棋盘设置雷
void board_mine_set(char board[ROWS][COLS], int r, int c)
{
//count记录还需要布置的雷数量
int count = mine_number;
while (count)
{
// 我们只需要在内层ROW*COL布置雷
int x = rand() % r + 1;
int y = rand() % c + 1;
//如果该(x,y)无雷,就放雷,如果有雷就什么也不干
if (board[x][y] == '0')
{
//布置雷,并count--
board[x][y] = '1';
count--;
}
}
}
//检查周围8个位置有多少雷
int count_mine(char mine_board[ROWS][COLS], int x, int y)
{
//这里我们是用ASCII码的相关运算
//我们有雷就是'1',无雷就是'0',因此只需要将周围八个位置的ASCII码相加,再减去8个'0'的ASCII,就可以得到周围雷的数量
return (mine_board[x - 1][y - 1] + mine_board[x - 1][y] + mine_board[x - 1][y + 1] +
mine_board[x][y - 1] + mine_board[x][y + 1] + mine_board[x + 1][y - 1] +
mine_board[x + 1][y] + mine_board[x + 1][y + 1]) - 8 * '0';
}
//主要的扫雷函数
void sweep_mine(char mine_board[ROWS][COLS], char show_board[ROWS][COLS])
{
//一些提示
printf("若想要标记雷,则在扫雷过程中输入:-1 -1,进入标记模式\n");
printf("若想要退出标记模式,则在标记模式再次输入:-1 -1,回到扫雷模式\n");
printf("为了对称,坐标大于等于10的情况,我们都进行了取10的余数的操作\n");
printf("所以想要输入的坐标中出现大于等于10的情况时,多多注意\n");
Sleep(2000);
printf("明白的话请输入:1\n");
int input = 0;
while (1)
{
scanf("%d", &input);
if (input == 1)
{
printf("开始游戏!\n");
Sleep(1000);
//开始游戏后,将其他无关信息都清屏
system("cls");
printf("进入/退出标记模式:>> -1 -1\n");
//一开始先打印扫雷棋盘
board_print(show_board, ROW, COL);
break;
}
else
{
printf("输入错误,请重新输入\n");
}
}
//count是用来判断游戏何时胜利
int count = ROW * COL - mine_number;
//sign[ROWS][COLS]标记数组的作用后面会有详细解释
//count和sign会进行配合使用
int sign[ROWS][COLS] = { 0 };
while (count)
{
//(x, y)是排雷位置
int x = 0;
int y = 0;
//flag是用来判断是扫雷模式还是标记模式
int flag = 0;
printf("请输入扫雷坐标:>");
scanf("%d %d", &x, &y);
//合法性及模式(扫雷/标记)判断
if (x >= 1 && x <= ROW && y >= 1 && y <= COL && flag == 0)
{
//如果该位置没有被排
if (show_board[x][y] == '*')
{
//如果该位置在放雷棋盘中有雷,就被炸死
if (mine_board[x][y] == '1')
{
//离开count循环
break;
}
else
{
//为了美观的清屏
system("cls");
//每次排雷都需要考虑是否需要自动连消(一次消除一大片无雷的情况)
//判断连消我们是需要用相同方式判断周围8个位置,因此会用到递归,递归的时候会需要进行一个标记,来标记该坐标是否已经判断过,因此在前面定义了一个sign[ROWS][COLS],
//我们在最外面定义sign[ROWS][COLS]的原因是,我们每个位置不论是哪一次进行的连消和排雷操作,该位置,都只能被排雷一次
//进入连消和排雷,我们需要在函数内改变count的值,所以需要传递地址
check_clear_mine(mine_board, show_board, x, y, sign, &count);
//每一次连消和排雷后打印排雷后的棋盘
printf("进入/退出标记模式:>> -1 -1\n");
board_print(show_board, ROW, COL);
}
}
//如果该位置被标记,那么就不能进行排雷
else if(mine_board[x][y] == '#')
{
printf("该坐标被标记\n");
continue;
}
else
{
//已经有数据也不能进行排雷
printf("该坐标已经有数据\n");
continue;
}
}
//如果输入-1 -1进入标记模式
else if (x == -1 && y == -1)
{
//在标记模式时将flag就会置为1
flag = 1;
printf("进入标记模式!!!\n");
while (1)
{
//标记模式的标记提示和要求
printf("提示:再次输入被标记的坐标,能够解除标记\n");
printf("提示:标记是用 # 表示\n");
printf("输入你想要 做/解除 的标记:>");
//标记坐标(m, n)
int m = 0;
int n = 0;
scanf("%d %d", &m, &n);
// 合法性判断
if (m >= 1 && m <= ROW && n >= 1 && n <= COL)
{
//如果该位置为'*'————做标记
if (show_board[m][n] == '*')
{
show_board[m][n] = '#';
system("cls");
printf("进入/退出标记模式:>> -1 -1\n");
board_print(show_board, ROW, COL);
}
//如果该位置为'#'————取消标记
else if(show_board[m][n] == '#')
{
show_board[m][n] = '*';
system("cls");
printf("进入/退出标记模式:>> -1 -1\n");
board_print(show_board, ROW, COL);
}
else
{
printf("该坐标已经被排查,无需标记\n");
continue;
}
}
//在扫雷模式按 -1 -1
else if (m == -1 && n == -1)
{
//改变标记
flag = 0;
printf("回到扫雷模式\n");
break;
}
else
{
printf("该坐标非法,请重新输入\n");
}
}
}
else
{
printf("该坐标非法,请重新输入\n");
continue;
}
}
//离开count循环后,如果count为0,也就是排完了雷,进入if
if (!count)
{
board_print(mine_board, ROW, COL);
printf("恭喜你,扫雷成功!\n");
Sleep(3000);
}
//count不为0,那就被炸死了,进入else
else
{
printf("很遗憾,你被炸死了\n");
//并打印放雷棋盘
board_print(mine_board, ROW, COL);
}
}
//判断(x, y)坐标在mine_board上周围八个位置的情况,并根据情况,填到show_board
void check_clear_mine(char mine_board[ROWS][COLS], char show_board[ROWS][COLS], int x, int y, int sign[ROWS][COLS], int* count)
{
//用(x, y)这个坐标进入这个函数,那么这个坐标就一定被排雷了,那么就将sign[x][y]置为1
sign[x][y] = 1;
//需要排的雷的数量就减一
*count =*count - 1;
//记录周围八个位置的雷数量
int n = count_mine(mine_board, x, y);
//将雷的数量对应的char类型的值给填入到show_board中
show_board[x][y] = n + '0';
//如果该位置周围没有雷,那么就会考虑连消情况
if (n == 0)
{
//依次判断周围八个坐标
//首先是该坐标的sign为1,即没有被标记过/没有被判断过
//其次是合法性,周围的坐标需要在[1, ROW]×[1, COL]内
//然后才能进入递归,进入周围坐标的check_clear_mine判断
if (sign[x - 1][y - 1] == 0 && x - 1 >= 1 && x - 1 <= ROW && y - 1 >= 1 && y - 1 <= COL)
check_clear_mine(mine_board, show_board, x - 1, y - 1, sign, count);
if (sign[x - 1][y] == 0 && x - 1 >= 1 && x - 1 <= ROW && y >= 1 && y <= COL)
check_clear_mine(mine_board, show_board, x - 1, y, sign, count);
if (sign[x - 1][y + 1] == 0 && x - 1 >= 1 && x - 1 <= ROW && y + 1 >= 1 && y + 1 <= COL)
check_clear_mine(mine_board, show_board, x - 1, y + 1, sign, count);
if (sign[x][y - 1] == 0 && x >= 1 && x <= ROW && y - 1 >= 1 && y - 1 <= COL)
check_clear_mine(mine_board, show_board, x, y - 1, sign, count);
if (sign[x][y + 1] == 0 && x >= 1 && x <= ROW && y + 1 >= 1 && y + 1 <= COL)
check_clear_mine(mine_board, show_board, x, y + 1, sign, count);
if (sign[x + 1][y - 1] == 0 && x + 1 >= 1 && x + 1 <= ROW && y - 1 >= 1 && y - 1 <= COL)
check_clear_mine(mine_board, show_board, x + 1, y - 1, sign, count);
if (sign[x + 1][y] == 0 && x + 1 >= 1 && x + 1 <= ROW && y >= 1 && y <= COL)
check_clear_mine(mine_board, show_board, x + 1, y, sign, count);
if (sign[x + 1][y + 1] == 0 && x + 1 >= 1 && x + 1 <= ROW && y + 1 >= 1 && y + 1 <= COL)
check_clear_mine(mine_board, show_board, x + 1, y + 1, sign, count);
}
return;
}
Gitee代码下载
这是我的Gitee仓库中的代码地址,有需要的就去下载吧
https://gitee.com/grass-without-dream/learning_-c/tree/master/full_version_of_mine_sweeping