MineSweeper扫雷游戏

一、 前期分析

扫雷游戏就是用户选择一个坐标,如果位置是雷,就被炸死,游戏结束,如果位置不是雷,就显示该坐标周围有几个雷,直到将所有⾮雷都找出来,排雷成功。(这里我们就以简单版本的9*9的棋盘为例)。

按照这个思路,我们可以设置一个9*9的二维数组来表示棋盘,然后设置雷,用1表示雷,用0表示非雷,这样统计出某个坐标周围有几个雷,然后把该坐标上的数字改成几即可。但是这样做有三个问题。

第一,如果统计的坐标周围有1个雷,将该坐标改为1,那么下次统计其它坐标周围雷的个数时,这个排查出来的1和设置的雷的1就会产生混淆。(如统计图中(2,4)坐标处,它周围有一个雷)

想到如果把雷和非雷的信息不用数字表示,而是用字符表示就可以避免混淆。

第二,如果把排查出的雷的信息和设置的雷的信息放在一张棋盘上的话,那么展示给用户的时候,用户就可以直接看出哪里有雷,就做不到隐秘的效果了。所以我们可以用两张棋盘,一张用于存放设置雷的信息,另一张用于存放排查出的雷的信息;同时如果这么做,第一个会混淆的问题也可以被解决,而我们仍然可以用1表示雷,用0表示非雷。

展示给用户的棋盘要有排查出的雷的信息,同时为了保持神秘,可以将未排查的部分初始化为字符'*'。由于既有数字又有字符,而周围雷的个数又一定不会超过8,所以我们可以统一用字符来表示,这就确定了我们要用到二维字符数组。再想想设置雷的数组,用1和0表示雷和非雷,这也可以用字符’1’和字符’0’表示,如果两个棋盘都使用了字符数组,那么后续就可以用同一套字符数组的函数来操作了,这就简便了许多。

第三,统计最外围一圈的坐标周围雷的个数时,如统计图中的(3,9)坐标时,最右边的三个坐标就会越界。我们可以将棋盘扩大一圈(这里就变成11*11的棋盘),而仍将雷布置在中间的(9*9)区域,最外围一圈不用统计,这样就避免了越界的问题。

二、 代码实现

解决了上述最基本的问题,下面就来探讨游戏逻辑的实现。

为了完成这个游戏,需要用到许多函数,为了将程序模块化,我们可以将游戏的外围大框架写在一个源文件中,记为test.c,用于测试游戏的整体逻辑,将游戏细节部分的逻辑实现写在另一个源文件game.c中,再添加一个头文件game.h,在其中声明game.c中的函数。

在test.c中首先给出是否进入游戏的选项,可以定义一个menu函数,打印出菜单,让用户可以自由选择进入游戏或退出,可以在do-while循环中使用switch语句进行判断。

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

void test()
{
	int input = 0;
	do
	{
		menu();
		printf("请选择:>\n");
		scanf("%d", &input);

		switch (input)
		{

		case 1:
			game();
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("选择错误,请重新选择\n");

			break;
		}

	} while (input);
}

这里我们可以先测试一下,在game函数中打印“扫雷游戏”。

没有问题,继续往下写。

在game函数中需要定义两个11*11的二维数组,一个用于存放设置雷的信息,一个用于存放排查出的雷的信息,为了方便改变棋盘的规模,我们可以在头文件game.h在定义行和列两个常量。由于后续设置雷要在中间的9*9棋盘内,排查雷又需要整个11*11的棋盘,所以既需要中间的行列数,又需要扩大后的行列数。

//扩大前的行列数
#define ROW 9
#define COL 9

//扩大后的行列数
#define ROWS ROW + 2
#define COLS COL + 2

那么二维数组就容易定义了

char mine[ROWS][COLS] = { 0 };//存放设置雷的信息
char show[ROWS][COLS] = { 0 };//存放排查出的雷的信息

有了棋盘,先要对棋盘进行初始化,设置雷的棋盘未设置前要初始化为'0',而排查雷的棋盘未开始前要初始化为'*',初始化的内容不同,那么我们就可以将要初始化的内容作为参数传到函数中,这样就可以用同一个函数进行初始化了。

InitBoard(mine,ROWS,COLS,'0');
InitBoard(show,ROWS,COLS,'*');

具体怎么初始化呢?就是遍历二维数组,然后将每个元素都赋值为传过去的参数。

void InitBoard(char board[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++)
		{
			board[i][j] = set;
		}
	}
}

有没有初始化成功呢?可以把棋盘打印出来看一下。定义一个打印的函数,将数组和要打印的行列传过去(注意这里传过去的是11*11的数组,只是要用到中间的9*9的格子)。

DisplayBoard(mine, ROW, COL);
DisplayBoard(show, ROW, COL);

为了将每次的棋盘间隔开,我们可以在每次打印前加上“-----扫雷游戏-----”。

void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	printf("-----扫雷游戏-----\n");
	
	int i = 0;
	for (i = 1; i <= row; i++)
	{

		int j = 0;
		for (j = 1; j <= col; j++)
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");
	}
}

效果如下

初始化成功了,但是选择要排查的坐标时,一排一排的数很麻烦,为了快速锁定坐标,可以在每一行、每一列前加上序号。

void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	printf("-----扫雷游戏-----\n");
	//为方便快速锁定坐标,打印时可以加上行号和列号
	int i = 0;

	for (i = 0; i <= col; i++)//打印列号
	{
		printf("%d ", i);
	}
	printf("\n");
	for (i = 1; i <= row; i++)
	{
		printf("%d ", i);//打印行号

		int j = 0;
		for (j = 1; j <= col; j++)
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");
	}
}

效果如下

这里补充一点,由于两个源文件都需要包含<stdio.h>和"game.h",我们就可以在自己写的"game.h"中包含<stdio.h>,这样两个源文件就只需要包含"game.h"一个头文件。(后续设置随机数要用到的头文件也可以定义在"game.h"中)

下一步是设置雷,这就需要生成随机数。在main函数中设置srand((unsigned int)time(NULL))作为rand函数的种子(一个工程中只要设置一次),然后就可以调用rand函数生成我们想要的随机数。由于是9*9的棋盘,就要生成1~9的数字,rand() % ROW + 1和rand() % COL + 1就可以满足这个要求。假设要设置简单的10个雷,为方便修改,也可以将雷的个数在头文件中定义。(#define EASY_COUNT 10)

void SetMine(char board[ROWS][COLS], int row, int col)
{
	
	int count = EASY_COUNT;
	while (count)     //共有 EASY_COUNT 个雷,设置完了就停止
	{
		int x = rand() % ROW + 1;      //每次都要重新生成随机数
		int y = rand() % COL + 1;
		if (board[x][y] == '0')      //判断该位置是否被设置过雷
		{
			board[x][y] = '1';
			count--;
		}
	}
}

设置完了雷,可以打印出来看看是否成功。

下一步就到了排查雷的环节,需要根据用户输入的坐标,在mine数组中数出周围雷的个数,然后展示到show数组中。输入坐标后要判断坐标是否在合法范围内(是否越界),如果合法,还需要判断是否已经被排查过。满足了上述两个条件,就要将该坐标周围雷的个数加起来,由于数组中放的是字符,减去8个字符'0'才得到总的个数。

int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
	return mine[x - 1][y - 1] + mine[x - 1][y] + mine[x - 1][y + 1] +
		mine[x][y - 1] + mine[x][y + 1] + mine[x + 1][y - 1] +
		mine[x + 1][y] + mine[x + 1][y + 1] - '0' * 8;
}

还有另一种方法,因为选择的坐标不是雷时才要统计雷的数量,所以可以将周围(包括自己)9个坐标分别减去字符'0'后相加。

int GetMineCount1(char mine[ROWS][COLS], int x, int y)
{
	int i = 0;
	int sum = 0;
	for (i = x - 1; i <= x + 1; i++)
	{
		int j = 0;
		for (j = y - 1; j <= y + 1; j++)
		{
			sum += (mine[i][j] - '0');
		}
	}
	return sum;
}

最后如果排查完了还需要判断输赢,也就是将非雷的71个位置都找到,可以定义一个变量int win = row * col - EASY_COUNT,每排查一个位置win就减去1,直到win等于0就跳出循环。

void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;

	int win = row * col - EASY_COUNT;
	while (win)
	{
		printf("请输入要排查的坐标: ");
		scanf("%d %d", &x, &y);
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			if (show[x][y] != '*')
			{
				printf("该坐标已经被排查过,请重新输入\n");
			}
			else
			{
				if (mine[x][y] == '1')
				{
					printf("很遗憾,你被炸死了\n");//失败了,展示棋盘
					DisplayBoard(mine, ROW, COL);
					break;
				}
				else
				{

					//计数并赋值(一定不是雷,才进入到这里)
					int count = GetMineCount1(mine, x, y);
					show[x][y] = count + '0';   
					DisplayBoard(show, ROW, COL);
					win--;
				}
			}
		}
		else
		{
			printf("输入坐标非法,请重新输入\n");
		}
	}

	if (win == 0)
	{
		printf("恭喜你,排雷成功\n");
		DisplayBoard(mine, ROW, COL);
	}

}

完成后如果想要测试成功排完雷的逻辑,需要执行71次,太麻烦,我们可以将雷设置为80个,然后打印出设置雷的棋盘,再进行测试,这样只需排一次就可以成功了。

三、完整代码

下附每个部分的完整代码:

//game.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define ROW 9
#define COL 9

#define ROWS ROW + 2
#define COLS COL + 2

//设置雷的数量
#define EASY_COUNT 10

//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);

//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col);

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

//排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
//game.c

#include "game.h"
void InitBoard(char board[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++)
		{
			board[i][j] = set;
		}
	}
}

void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	printf("-----扫雷游戏-----\n");
	int i = 0;

	for (i = 0; i <= col; i++)
	{
		printf("%d ", i);
	}
	printf("\n");
	for (i = 1; i <= row; i++)
	{
		printf("%d ", i);

		int j = 0;
		for (j = 1; j <= col; j++)
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");
	}
}

void SetMine(char board[ROWS][COLS], int row, int col)
{
		
	int count = EASY_COUNT;
	while (count)
	{
		int x = rand() % ROW + 1;
		int y = rand() % COL + 1;
		if (board[x][y] == '0')
		{
			board[x][y] = '1';
			count--;
		}
	}
}

int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
	return mine[x - 1][y - 1] + mine[x - 1][y] + mine[x - 1][y + 1] +
		mine[x][y - 1] + mine[x][y + 1] + mine[x + 1][y - 1] +
		mine[x + 1][y] + mine[x + 1][y + 1] - '0' * 8;
}


void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;

	int win = row * col - EASY_COUNT;
	while (win)
	{
		printf("请输入要排查的坐标: ");
		scanf("%d %d", &x, &y);
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			if (show[x][y] != '*')
			{
				printf("该坐标已经被排查过,请重新输入\n");
			}
			else
			{
				if (mine[x][y] == '1')
				{
					printf("很遗憾,你被炸死了\n");//失败了,展示棋盘
					DisplayBoard(mine, ROW, COL);
					break;
				}
				else
				{

					//计数并赋值(一定不是雷,才进入到这里)
					int count = GetMineCount(mine, x, y);
					show[x][y] = count + '0';  
					DisplayBoard(show, ROW, COL);
					win--;
				}
			}
		}
		else
		{
			printf("输入坐标非法,请重新输入\n");
		}
	}

	if (win == 0)
	{
		printf("恭喜你,排雷成功\n");
		DisplayBoard(mine, ROW, COL);
	}
}
//test.c

#include "game.h"
void menu()
{
	printf("*****************************\n");
	printf("********   0.exit  **********\n");
	printf("********   1.play  **********\n");
	printf("*****************************\n");
}
void game()
{
	char mine[ROWS][COLS] = { 0 };
	char show[ROWS][COLS] = { 0 };

	
	InitBoard(mine,ROWS,COLS,'0');
	InitBoard(show,ROWS,COLS,'*');


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

	//打印棋盘
	//DisplayBoard(mine, ROW, COL);
	DisplayBoard(show, ROW, COL);

	//排查雷	
	FindMine(mine, show, ROW, COL);
}
void test()
{
	int input = 0;
	do
	{
		menu();
		printf("请选择:>\n");
		scanf("%d", &input);

		switch (input)
		{

		case 1:
			game();
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("选择错误,请重新选择\n");

			break;
		}

	} while (input);
}
int main()
{
	srand((unsigned int)time(NULL));
	test();
	
	return 0;
}

可以发现,这个游戏虽然看起来很复杂,但把它拆分成一个个小的模块来写,就会容易一些。当我们掌握了基本的语法和框架,就有能力去写出一个个看似遥不可及的较大的程序。也许写的过程中会遇到很多次无法执行,但这是无法避免的磨练的过程,我们需要消除恐惧,耐下心来,找到错误点,改正错误,这样才能在一次次错误中加深印象,写出更加规范合理的代码。

此外,我们还应养成写一段测一段的习惯,写好外面的框架,就测试一下逻辑是否可行,写好某一个细节部分,就测试一下是否正确。如果写完了几百行甚至上千行代码才发现里面有错误,那找出错误点将会变得非常困难。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值