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

Posted 程序里的酒

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言实现全面的扫雷小游戏(包括空白展开标记等)具体步骤加代码分析相关的知识,希望对你有一定的参考价值。


前言

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


一、问题描述

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

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

二、基本框架构思

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

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

  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");以上是关于C语言实现全面的扫雷小游戏(包括空白展开标记等)具体步骤加代码分析的主要内容,如果未能解决你的问题,请参考以下文章

扫雷游戏(C语言实现)初级版和优化版(增加了自动展开标记地雷功能,同时排除了第一次排到地雷的情况)

扫雷游戏(C语言实现)初级版和优化版(增加了自动展开标记地雷功能,同时排除了第一次排到地雷的情况)

扫雷小游戏(可选择简单一般困难三种模式可展开,可标记)

C语言--扫雷小游戏(含递归展开)

扫雷C语言如何实现(含递归展开)

扫雷游戏(可展开,可标记)