C语言 扫雷(代码及详细注释可直接前往我的Gitee仓库下载代码)

在这里插入图片描述

前言

所有的逻辑思考我都会放在代码注释中,大家只需要按照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

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

失去梦想的小草

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值