6

【C语言实现】全面的扫雷小游戏(包括空白展开、标记等)具体步骤加代码分析

 2 years ago
source link: https://blog.csdn.net/qq_52145272/article/details/120449099
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client



扫雷,是一个十分经典的小游戏,相信大家小时候都玩过,在实现过程中你将会有很大的成就感,现在就让我们一起来实现它吧,。


一、问题描述

实现除界面外扫雷游戏的所有功能。包括

实现一个简单的界面
实现排查雷的功能
实现标记雷的功能
实现显示周围雷的数量
实现第一次排查不会遇雷
实现如果周围没有雷则展开一片

在这里插入图片描述

二、基本框架构思

在开始编写代码之前,我们必须去玩一玩扫雷,熟悉一下它的各种规则和机制,有助于我们形成更加清晰的代码逻辑。同时在写完一个功能后,进行测试并与真实的扫雷功能做对比。为了编写代码,我玩了十余次。

在试玩过后,我们首先想到的便是界面如何展示,雷如何设置。对于二维平面,我们最常用的就是数组,在数组里面存放不同的数据,来表示有无雷。

  1. 假设我们用一个二维数组,用数据0表示没有雷,1表示有雷,但是当我们排查一个点之后需要显示周围雷的数量,假设也为1,那就会产生冲突,同时也不方便统计周围雷的数量。
  2. 还有一个问题数组容易出界,没次访问都需要判断,比较麻烦,容易出错。

解决办法:

  1. 我们使用两个数组,一个表示有无雷,一个展示给用户看,未排查用‘ *
    ’表示,已排查用周围雷的数量表示。为了统一,我们使用字符数组,遇到整数时将其转化为字符存放。

  2. 我们可以只使用设定数组的内圈部分,即最外圈不再使用,用于判断。

如下图:
在这里插入图片描述

接下来让我们来实现主函数,简单菜单

#define _CRT_SECURE_NO_WARNINGS 1

void Menu()
{
	printf("********************************\n");
	printf("*********   1. play     ********\n");
	printf("*********   0. exit     ********\n");
	printf("********************************\n");
}
int main()
{
	int input = 0;
	int count = 0;
	srand((unsigned int)time(NULL));
	do
	{
		Menu();

		//清除缓冲区,第一次不用
		if(count!=0)
		{
			char ch;
			while ((ch = getchar()) != EOF && ch != '\n')
			{
				;
			}			
		}
		count++;
		
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			//将标记正确的数量重置为0
			mark_count = 0;
			MineSweeper();
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("选择错误,重新选择!\n");
			break;
		}
	} while (input);

	return 0;
}

设置清除缓冲区代码的目的:
防止输入一个有效数字但带有空格,后面又输入一个数字存放在缓冲区,导致下一次直接取缓冲区的数字,不符合本意。
如图:
在这里插入图片描述
加上之后:
在这里插入图片描述

三、具体实现

1.扫雷接口实现

先定义相关宏定义,方面后面修改

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<stdio.h>
#include<time.h>
#include<stdlib.h>


#define ROW 9  //扫雷地图的数组的行
#define COL 9  //扫雷地图的数组的行

#define ROWS 11    //真实数组的行,为了方便查找雷的数量,这样的话不用判断访问数组是否越界
#define COLS 11    //真实数组的行,多加一圈

#define MINE_COUNT 9 //设置雷的数量

//设置全局变量
int find_count;//用于判断是否为第一次排雷,防止第一次被诈
int mark_count;//标记正确的数量

游戏接口,调用各函数实现扫雷。

void MineSweeper()
{
	//为了统一符号,使用字符数组

	char mine[ROWS][COLS] = { 0 };//用于存放雷的数组,0表示没有雷,1表示有雷
	char show[ROWS][COLS] = { 0 };//用于存放打印给用户看的该点周围雷的数量的数组,
	                             //默认为*,输入坐标后其内容为周围雷的数量
								 // !为标记点
	//数组初始化
	InitBoard(mine, ROWS, COLS, '0');
	InitBoard(show, ROWS, COLS, '*');


	//设置雷
	SetMine(mine, ROW, COL);

	system("cls");
	//调试用,可以查看雷的位置
	//DisplayBoard(mine, ROW, COL);
	
	//显示数组版面
	DisplayBoard(show, ROW, COL);

	//游戏入口
	PlayGame(mine, show, ROW, COL);
}

2.地图初始化

因为两个数组最初都是只存放一种字符,’ 0 ‘和’ * ',所有可以直接把字符当作参数传入,这样就可以通用一个函数了。

//数组初始化
void InitBoard(char board[][COLS], int row, int col, char ch)
{
	for (int i = 0; i < row; i++)
	{
		for (int j = 0; j < col; j++)
		{
			board[i][j] = ch;
		}
	}
}

3.设置雷

使用随机函数设置雷,即将mine数组的部分随机设置成’ 1 ',需要注意的是,我们只针对数组内部真正有效的部分,最外一圈不管,所以遍历是从1开始。

//设置雷
void SetMine(char board[][COLS], int row, int col)
{
	int count = MINE_COUNT;
	while (count)
	{
		//随机获得雷的坐标
		int x = rand() % row + 1;//从1到row-1
		int y = rand() % col + 1;
		//判断是否已经是雷
		if (board[x][y] != '1')
		{
			//不是雷,就放
			board[x][y] = '1';
			count--;
		}
	}
}

4.显示界面

打印传入的数组,并打印行号,注意只打印数组内部,遍历从1开始。
如果做了标记用’ ! '表示。

//显示数组版面
void DisplayBoard(char board[][COLS], int row, int col)
{
	printf("------------------------\n");
	printf("  ");
	for (int i = 1; i <= col; i++)
	{
		printf("%2d", i);
	}
	printf("\n");
	for (int i = 1; i <= row; i++)
	{
		printf("%2d", i);
		for (int j = 1; j <= col; j++)
		{
			printf(" %c", board[i][j]);
		}
		printf("\n");
	}
	printf("------------------------\n");
}

5.开始扫雷

排查过的数量等于不是雷的数量 或者 标记正确雷的数量等于设置雷的数量结束,排雷成功。
每次选择执行完后打印显示(show)数组。

//游戏入口,选择排查或者标记
void PlayGame(char mine[][COLS], char show[][COLS], int row, int col)
{
	int win_count = 0;//排查过的数量

	//排查过的数量等于‘0’的数量的时候 或者 标记正确雷的数量等于设置的雷数量结束,排雷成功
	while (win_count < (row * col - MINE_COUNT) && mark_count < MINE_COUNT)
	{

		printf("################################\n");
		printf("#########   1. 排查雷   ########\n");
		printf("#########   2. 标记雷   ########\n");
		printf("#########   3. 取消标记 ########\n");
		printf("################################\n");
		int choice;
		printf("请选择:>");
		
		//清除缓冲区
		char ch;
		while ((ch = getchar()) != EOF && ch != '\n')
		{
			;
		}
		scanf("%d", &choice);
		if (choice != 1 && choice != 2 && choice != 3)
		{
			printf("输入错误,请重新输入\n");
			//跳过本次循环
			continue;
		}
		if (choice == 1)
		{
			//排查雷
			int judge = FindMine(mine, show, row, col, &win_count);
			if (!judge)
			{
				//被雷炸死,打印藏雷的数组,并结束
				DisplayBoard(mine, row, col);
				return;
			}
		}
		else
		{			
			if (choice == 2)
			{
				//标记雷
				MarkMine(mine, show, row, col);
				system("cls");
				DisplayBoard(show, row, col);
			}
			else
			{
				//取消标记雷				
				CancelMark(mine, show, row, col);
				system("cls");
				DisplayBoard(show, row, col);
			}
		}

	}
	if (win_count == row * col - MINE_COUNT||mark_count==MINE_COUNT)
	{
		system("cls");
		printf("恭喜你,排雷成功\n");
		DisplayBoard(mine, ROW, COL);
		return;
	}
}

6.计算周围雷的数量

统计八个方向,上、下、左、右、左上,左下,右上,右下为雷的数量,因为存放的是字符,所有相加后得减去’ 0 ',即可得到整数。

//查找周围雷的数量
int GetMineCount(char mine[][COLS], int x, int y)
{
	return (mine[x - 1][y] +
		mine[x + 1][y] +
		mine[x][y - 1] +
		mine[x][y + 1] +
		mine[x - 1][y - 1] +
		mine[x + 1][y - 1] +
		mine[x - 1][y + 1] +
		mine[x + 1][y + 1] - 8 * '0');
}

7.排查雷

排查雷需要设置第一次不被炸死,同时如果被标记则不能再排查。当被炸死或者输入坐标错误则返回。

//排查雷
//返回0代表结束,返回1代表继续
int FindMine(char mine[][COLS], char show[][COLS], int row, int col, int* pwin)
{
	int x, y;
	
	printf("请输入想要排查的坐标:>");
	scanf("%d %d", &x, &y);
	//如果是第一次,重新设置后如果还是雷则继续循环
	while (find_count == 0)
	{
		if (mine[x][y] == '1')//是雷
		{
			//现将mine数组置空,即初始化
			InitBoard(mine, row, col, '0');
			//重新布雷
			SetMine(mine, row, col);
		}
		else
		{
			break;
		}
	}
	
	if (x >= 1 && x <= row && y >= 1 && y <= col)
	{
		if (mine[x][y] == '0')//不是雷
		{
			//如果已经标记,则不能排查
			if (show[x][y] == '!')
			{
				printf("该点已经被标记,请重新输入\n");
				return 1;
			}
			system("cls");
			SpreadBlank(mine, show, x, y, pwin);
			DisplayBoard(show, row, col);
		}
		else
		{			
			printf("很遗憾,你被炸死了!\n");			
			return 0;
		}
	}
	else
	{
		printf("输入坐标错误,请重新输入\n");
		return 1;
	}
	
}

标记后不能再被排查:
在这里插入图片描述
第一次不会被炸死:
在这里插入图片描述
选择之后:
在这里插入图片描述

8.空白展开

目的:如果周围没有雷则继续展开(递归),遇到雷停止
这是较难实现的一个函数,需要使用递归实现,而使用递归就需要确定递归的终止条件,这里有三个

  1. 对最外层一圈不做计算,直接返回,这就是多设置一圈的好处
  2. 如果show数组里面不是*,即已经被探查过的,直接返回,防止死递归,导致栈溢出。
  3. 如果周围有雷就停止。
//如果周围没有雷,则全部展开
//展开空白区域
void SpreadBlank(char mine[][COLS], char show[][COLS], int x, int y, int* pwin)
{
	//对最外层一圈不做计算,直接返回,这就是多设置一圈的好处
	if (x==0||y==0||x==ROWS-1||y==COLS-1)
		return;

	//如果show数组里面不是*,即已经被探查过的,直接返回,防止死递归,导致栈溢出
	if (show[x][y] != '*')
		return;
		
	int count = GetMineCount(mine, x, y);
	if (count > 0)
	{
		show[x][y] = count + '0';
		//增加排查数量
		(*pwin)++;
		return ;
	}
	else
	{
		//八个方向,上、下、左、右、左上,左下,右上,右下
		
		show[x][y] = '0';
		//增加排查数量
		(*pwin)++;
		SpreadBlank(mine, show, x - 1, y, pwin);
		SpreadBlank(mine, show, x + 1, y, pwin);
		SpreadBlank(mine, show, x, y - 1, pwin);
		SpreadBlank(mine, show, x, y + 1, pwin);
		SpreadBlank(mine, show, x - 1, y - 1, pwin);
		SpreadBlank(mine, show, x + 1, y - 1, pwin);
		SpreadBlank(mine, show, x - 1, y + 1, pwin);
		SpreadBlank(mine, show, x + 1, y + 1, pwin);
	}
}

9.标记雷

用’ ! '为标记符号。如果正确标记雷点,则标记正确数+1

//标记雷点
void MarkMine(char mine[][COLS], char show[][COLS], int row, int col)
{
	int x;
	int y;
	printf("请输入想要标记的坐标:>");
	scanf("%d%d", &x, &y);
	//该点需未被探查,即在show数组为‘*’的点
	if (x >= 1 && x <= row && y >= 1 && y <= col && show[x][y]=='*')
	{
		if (mine[x][y] == '1')
		{
			//正确标记雷点
			mark_count++;
		}
		show[x][y] = '!';
	}
	else
	{
		printf("输入坐标错误,请重新输入\n");
	}

}

10.取消标记

与标记类似,只需把标记点改为’ * '即可。如果该点本是正确标记雷点,则标记正确数-1

//取消标记雷点
void CancelMark(char mine[][COLS], char show[][COLS], int row, int col)
{
	int x;
	int y;
	printf("请输入想要取消标记的坐标:>");
	scanf("%d%d", &x, &y);
	//该点需已被标记
	if (x >= 1 && x <= row && y >= 1 && y <= col && show[x][y]=='!')
	{
		if (mine[x][y] == '1')
		{
			//如果是正确标记雷点,数量减一
			mark_count--;
		}
		show[x][y] = '*';
	}
	else
	{
		printf("输入坐标错误或者不是被标记点,请重新输入\n");
	}

}

四、结果演示

我们将雷的数量设置为1,同时展示mine数组,让我们知道雷的位置,方便测试。

#define MINE_COUNT 1 //设置雷的数量
DisplayBoard(mine, ROW, COL);

在这里插入图片描述
在这里插入图片描述
展开演示:
在这里插入图片描述
全部展开,排雷成功。
在这里插入图片描述
当然也有下面种情况。

在这里插入图片描述

五、完整代码

MineSweeper.h

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<stdio.h>
#include<time.h>
#include<stdlib.h>


#define ROW 9  //扫雷地图的数组的行
#define COL 9  //扫雷地图的数组的行

#define ROWS 11    //真实数组的行,为了方便查找类的数量,这样的话不用判断访问数组是否越界
#define COLS 11    //真实数组的行,多加一圈

#define MINE_COUNT 1 //设置雷的数量


int find_count;//用于判断是否为第一次排雷,防止第一次被诈
int mark_count;//标记正确的数量



//数组初始化
void InitBoard(char board[][COLS], int row, int col, char ch);

//显示数组版面
void DisplayBoard(char board[][COLS], int row, int col);

//设置雷
void SetMine(char board[][COLS], int row, int col);

//游戏入口,选择排查或者标记
void PlayGame(char mine[][COLS], char show[][COLS], int row, int col);

//排查雷
//返回0代表结束,返回1代表继续
int FindMine(char mine[][COLS], char show[][COLS], int row, int col, int* pwin);

//查找周围雷的数量
int GetMineCount(char mine[][COLS], int x, int y);

//如果周围没有雷,则全部展开
//展开空白区域
void SpreadBlank(char mine[][COLS], char show[][COLS], int x, int y, int* pwin);

//标记雷点
void MarkMine(char mine[][COLS], char show[][COLS], int row, int col);
//取消标记雷点
void CancelMark(char mine[][COLS], char show[][COLS], int row, int col);

MineSweeper.c

#pragma once

#include"MineSweeper.h"


//数组初始化
void InitBoard(char board[][COLS], int row, int col, char ch)
{
	for (int i = 0; i < row; i++)
	{
		for (int j = 0; j < col; j++)
		{
			board[i][j] = ch;
		}
	}
}

//显示数组版面
void DisplayBoard(char board[][COLS], int row, int col)
{
	printf("------------------------\n");
	printf("  ");
	for (int i = 1; i <= col; i++)
	{
		printf("%2d", i);
	}
	printf("\n");
	for (int i = 1; i <= row; i++)
	{
		printf("%2d", i);
		for (int j = 1; j <= col; j++)
		{
			printf(" %c", board[i][j]);
		}
		printf("\n");
	}
	printf("------------------------\n");
}

//设置雷
void SetMine(char board[][COLS], int row, int col)
{
	int count = MINE_COUNT;
	while (count)
	{
		//随机获得雷的坐标
		int x = rand() % row + 1;
		int y = rand() % col + 1;
		//判断是否已经是雷
		if (board[x][y] != '1')
		{
			//不是雷,就放
			board[x][y] = '1';
			count--;
		}
	}
}

//游戏入口,选择排查或者标记
void PlayGame(char mine[][COLS], char show[][COLS], int row, int col)
{
	int win_count = 0;//排查过的数量

	//排查过的数量等于‘0’的数量的时候 或者 标记正确雷的数量等于设置的雷数量结束,排雷成功
	while (win_count < (row * col - MINE_COUNT) && mark_count < MINE_COUNT)
	{

		printf("################################\n");
		printf("#########   1. 排查雷   ########\n");
		printf("#########   2. 标记雷   ########\n");
		printf("#########   3. 取消标记 ########\n");
		printf("################################\n");
		int choice;
		printf("请选择:>");
		//清除缓冲区
		char ch;
		while ((ch = getchar()) != EOF && ch != '\n')
		{
			;
		}
		scanf("%d", &choice);
		if (choice != 1 && choice != 2 && choice != 3)
		{
			printf("输入错误,请重新输入\n");
			//跳过本次循环
			continue;
		}
		if (choice == 1)
		{
			//排查雷
			int judge = FindMine(mine, show, row, col, &win_count);
			if (!judge)
			{
				//被雷炸死,打印藏雷的数组,并结束
				DisplayBoard(mine, row, col);
				return;
			}
		}
		else
		{			
			if (choice == 2)
			{
				//标记雷
				MarkMine(mine, show, row, col);
				system("cls");
				DisplayBoard(show, row, col);
			}
			else
			{
				//取消标记雷				
				CancelMark(mine, show, row, col);
				system("cls");
				DisplayBoard(show, row, col);
			}
		}

	}
	if (win_count == row * col - MINE_COUNT||mark_count==MINE_COUNT)
	{
		system("cls");
		printf("恭喜你,排雷成功\n");
		DisplayBoard(mine, ROW, COL);
		return;
	}
}


//排查雷
//返回0代表结束,返回1代表继续
int FindMine(char mine[][COLS], char show[][COLS], int row, int col, int* pwin)
{
	int x, y;
	
	printf("请输入想要排查的坐标:>");
	scanf("%d %d", &x, &y);
	//如果是第一次
	while (find_count == 0)
	{
		if (mine[x][y] == '1')//是雷
		{
			//现将mine数组置空,即初始化
			InitBoard(mine, row, col, '0');
			//重新布雷
			SetMine(mine, row, col);
		}
		else
		{
			break;
		}
	}
	if (x >= 1 && x <= row && y >= 1 && y <= col)
	{
		if (mine[x][y] == '0')//不是雷
		{
			//如果已经标记,则不能排查
			if (show[x][y] == '!')
			{
				printf("该点已经被标记,请重新输入\n");
				return 1;
			}

			//int count = GetMineCount(mine, x, y);
			//show[x][y] = count + '0';

			system("cls");
			SpreadBlank(mine, show, x, y, pwin);
			DisplayBoard(show, row, col);
			//win++;
		}
		else
		{			
			printf("很遗憾,你被炸死了!\n");			
			return 0;
		}
	}
	else
	{
		printf("输入坐标错误,请重新输入\n");
		return 1;
	}
	
}

//查找周围雷的数量
int GetMineCount(char mine[][COLS], int x, int y)
{
	return (mine[x - 1][y] +
		mine[x + 1][y] +
		mine[x][y - 1] +
		mine[x][y + 1] +
		mine[x - 1][y - 1] +
		mine[x + 1][y - 1] +
		mine[x - 1][y + 1] +
		mine[x + 1][y + 1] - 8 * '0');
}


//如果周围没有雷,则全部展开
//展开空白区域
void SpreadBlank(char mine[][COLS], char show[][COLS], int x, int y, int* pwin)
{
	//对最外层一圈不做计算
	if (x==0||y==0||x==ROWS-1||y==COLS-1)
		return;

	//如果show数组里面不是*,即已经被探查过的,直接返回,防止死递归,导致栈溢出
	if (show[x][y] != '*')
		return;
	int count = GetMineCount(mine, x, y);
	if (count > 0)
	{
		show[x][y] = count + '0';
		//增加排查数量
		(*pwin)++;
		return ;
	}
	else
	{
		//mine[x][y] = '2';
		//八个方向,上、下、左、右、左上,左下,右上,右下
		
		show[x][y] = '0';
		//增加排查数量
		(*pwin)++;
		SpreadBlank(mine, show, x - 1, y, pwin);
		SpreadBlank(mine, show, x + 1, y, pwin);
		SpreadBlank(mine, show, x, y - 1, pwin);
		SpreadBlank(mine, show, x, y + 1, pwin);
		SpreadBlank(mine, show, x - 1, y - 1, pwin);
		SpreadBlank(mine, show, x + 1, y - 1, pwin);
		SpreadBlank(mine, show, x - 1, y + 1, pwin);
		SpreadBlank(mine, show, x + 1, y + 1, pwin);
	}
}

//标记雷点
void MarkMine(char mine[][COLS], char show[][COLS], int row, int col)
{
	int x;
	int y;
	printf("请输入想要标记的坐标:>");
	scanf("%d%d", &x, &y);
	//该点需未被探查,即在show数组为‘*’的点
	if (x >= 1 && x <= row && y >= 1 && y <= col && show[x][y]=='*')
	{
		if (mine[x][y] == '1')
		{
			//正确标记雷点
			mark_count++;
		}
		show[x][y] = '!';
	}
	else
	{
		printf("输入坐标错误,请重新输入\n");
	}

}

//取消标记雷点
void CancelMark(char mine[][COLS], char show[][COLS], int row, int col)
{
	int x;
	int y;
	printf("请输入想要取消标记的坐标:>");
	scanf("%d%d", &x, &y);
	//该点需已被标记
	if (x >= 1 && x <= row && y >= 1 && y <= col && show[x][y]=='!')
	{
		if (mine[x][y] == '1')
		{
			//如果是正确标记雷点,数量减一
			mark_count--;
		}
		show[x][y] = '*';
	}
	else
	{
		printf("输入坐标错误或者不是被标记点,请重新输入\n");
	}

}

MineSweeperTest.c

#define _CRT_SECURE_NO_WARNINGS 1

#include"MineSweeper.h"

void Menu()
{
	printf("********************************\n");
	printf("*********   1. play     ********\n");
	printf("*********   0. exit     ********\n");
	printf("********************************\n");
}

void MineSweeper()
{
	//为了统一符号,使用字符数组

	char mine[ROWS][COLS] = { 0 };//用于存放雷的数组,0表示没有雷,1表示有雷
	char show[ROWS][COLS] = { 0 };//用于存放打印给用户看的该点周围雷的数量的数组,
	                             //默认为*,输入坐标后其内容为周围雷的数量
								 // !为标记点
	//数组初始化
	InitBoard(mine, ROWS, COLS, '0');
	InitBoard(show, ROWS, COLS, '*');


	//设置雷
	SetMine(mine, ROW, COL);

	system("cls");
	//调试用
	DisplayBoard(mine, ROW, COL);

	
	//显示数组版面
	DisplayBoard(show, ROW, COL);

	//游戏入口
	PlayGame(mine, show, ROW, COL);
}

int main()
{
	int input = 0;
	int count = 0;
	srand((unsigned int)time(NULL));
	do
	{
		Menu();

		//清除缓冲区,第一次不用
		if(count!=0)
		{
			char ch;
			while ((ch = getchar()) != EOF && ch != '\n')
			{
				;
			}			
		}
		count++;
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			//将标记正确的数量重置为0
			mark_count = 0;
			MineSweeper();
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("选择错误,重新选择!\n");
			break;
		}
	} while (input);

	return 0;
}

对扫雷的完善,将其除界面外全部功能实现,其中最难的一点是展开空白区域,因为涉及递归,并且在数组受限条件较多,需要多次调试。如有错误,欢迎大佬指正。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK