详细解析C语言实现扫雷游戏(VS2019)

扫雷游戏相信大家都玩过,今天我们自己用C语言代码来实现一个扫雷游戏吧!

目录

游戏框架

初始化函数

打印函数

优化打印棋盘代码

设置雷的函数

排查雷的函数    

优化程序

排查坐标周围雷的函数

标记雷的函数

取消标记的函数

判断胜利的函数

更新排查雷函数

整个程序的代码


由于代码比较多,我们把整个程序拆分为两个源文件test.c和game.c以及一个头文件game.h,这样会更有条理,并且方便后期维护。

游戏框架

和之前的三子棋游戏一样,我们先制作一个简易的菜单,然后在do...while语句中嵌套一个switch...case语句,根据玩家的选择来执行相应的命令,选择1玩游戏,选择0退出程序,选择其他的则提示玩家选择错误,重新选择:

void menu()
{
	printf("*****************************\n");
	printf("********   1.play   *********\n");
	printf("********   0.exit   *********\n");
	printf("*****************************\n");
}
int main()
{
	int input = 0;
	do
	{
		menu();
		printf("请选择:>\n");
		scanf("%d", &input);
		switch(input)
		{
		case 1:
			printf("欢迎来到扫雷游戏\n");
            //game();
			break;
		case 0:
			printf("退出程序\n");
			break;
		default:
			printf("选择错误,请重新选择:>\n");
			break;
		}
	} while (input);
	return 0;
}

程序执行结果:

说明我们的游戏框架没有问题,接下来开始实现game()函数:

首先,我们来理一下思路,要玩扫雷一定得有雷可扫,所以我们第一步应该先布置雷,第二步再排查雷。所以我们需要创建两个二维数组:

一个数组用来存放布置好的雷的信息;

一个数组用来存放排查出的雷的信息;

但是这两个数组的类型是什么?大小是多少?

上图中,我们可以看到,游戏一开始,所有未被排查的坐标都是空白格子,那么在代码中,我们就可以用一个字符‘*’来表示这些空白格子,而在存放布置好的雷的数组中,我们可以用字符‘1’表示雷字符‘0’表示没有雷,这样也方便后面计算一个坐标附件有多少颗雷,所以数组的类型我们可以设为char类型

在排查雷的过程中,我们每输入一个坐标就需要排查这个坐标附近的另外8个坐标是否有雷。但是,在排查最外边一圈的坐标时,就会出现越界访问的情况:

所以我们需要在这个9*9的棋盘的四周再加一圈:

把它变成一个11*11的棋盘,在多出来的位置都放上字符‘0’表示没有雷,这样既可以正常玩,也不会出现越界访问的情况,所以数组的大小应该为11行11列(两个数组的大小保持一致,这样可以使它们对应的坐标相同)。

由于9*9和11*11后面会多次用到,所以我们在头文件中定义几个符号:

#define ROW 9
#define COL 9

#define ROWS ROW+2
#define COLS COL+2
void game()
{
	char bomb[ROWS][COLS] = { 0 };//存放布置好的雷的信息
	char show[ROWS][COLS] = { 0 };//存放排查出的雷的信息
}

初始化函数

数组创建完毕之后,我们需要把它们初始化一下,写一个初始化函数吧:

布置雷的数组初始化为‘0’,排查雷的数组初始化为‘*’ :

void InitBoard(char board[ROWS][COLS], int rows, int cols, char ch)
{
	int i = 0;
	int j = 0;
	for (i = 0;i < rows;i++)
	{
		for (j = 0;j < cols;j++)
		{
			board[i][j] = ch;
		}
	}
}

打印函数

数组初始化完毕,我们把棋盘打印出来看一下,打印的时候,我们只需要打印中间9*9的位置,多加的那一圈不需要打印:

void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	int j = 0;
	for (i = 0;i < row;i++)
	{
		for (j = 0;j < col;j++)
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");//打印完一行之后换行
	}
}

现在我们运行一下代码,看看有没有问题:

说明我们的初始化函数和打印函数都没有问题。

优化打印棋盘代码

我们的打印代码可以优化一下,给它加上行号和列号,这样更方便玩家输入坐标:

void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	int j = 0;
	for (i = 0;i <= row;i++)//打印列号
	{
		printf("%d ", i);
	}
	printf("\n");
	for (i = 1;i <= row;i++)
	{
		printf("%d ", i);//打印行号
		for (j = 1;j <= col;j++)
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");//打印完一行之后换行
	}
}

优化后的结果:

设置雷的函数

设置雷的时候,我们可以调用两次rand()函数生成两个随机数作为雷的横纵坐标 ,但是坐标的范围需要在1~9之间,所以我们用生成的随机数%9,得到0~8的数字,然后+1就可以得到1~9的数字(调用rand()函数之前,我们先在main()函数内部调用srand()函数,并且用time()函数的返回值作为srand()函数的参数,使生成的随机数更加随机):

void SetBomb(char board[ROWS][COLS], int row, int col)
{
	//设置10个雷
	int count = COUNT;

	while (count)
	{
		//生成雷的随机下标
		int x = (rand() % row) + 1;//范围在1—9之间
		int y = (rand() % col) + 1;//范围在1—9之间

		if (board[x][y] == '0')//字符'0'说明没有雷
		{
			board[x][y] = '1';
			count--;
		}
	}
}

这里我们在头文件中定义了一个符号COUNT表示雷的数量,以便后期修改雷数:

#define COUNT 10

排查雷的函数    

设置好了雷之后,我们就要排查雷了,排查雷时,当玩家输入要排查位置的坐标后,我们要判断坐标是否合法(横纵坐标的范围在1~9之间),坐标是否已经被排查过,坐标处是否有雷,并执行相应的命令:

转化为代码:

void FindBomb(char bomb[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int tmp = 0;
	while (tmp < row * col - COUNT)//10个雷,最多排查71次后跳出循环
	{
		printf("请输入要排查的雷的坐标:>\n");
		scanf("%d %d", &x, &y);
		if (x >= 1 && x <= row && y >= 1 && y <= col)//判断坐标合法性
		{
			if (show[x][y] == '*')//判断坐标是否被排查过
			{
				if (bomb[x][y] == '1')//判断坐标处是不是雷
				{
					printf("很遗憾,你被炸死了\n");
					DisplayBoard(bomb, ROW, COL);//打印出棋盘,让玩家“死”的瞑目
					break;
				}
				else
				{
					int ret = Get_Bomb_Count(bomb, x, y);
					show[x][y] = ret + '0';//把ret转化为字符ret
					DisplayBoard(show, ROW, COL);//更新棋盘信息
					tmp++;//玩家胜利的条件
				}
			}
			else
			{
				printf("该位置已经排查过,请重新输入:>\n");
			}
		}
		else
		{
			printf("请输入合法的坐标:>\n");
		}
	}
	if (tmp == row * col - COUNT)
	{
		printf("恭喜你胜利了\n");
	}
}

解析:

while()循环的判断条件是根据tmp的值,tmp初始值为0,每当玩家排查出一个没有雷的坐标时,tmp++,而9*9的棋盘一共有10个坐标是有雷的,9*9-10个坐标是没有雷的,即row*col-COUNT,所以当tmp<row*col-COUNT时,说明还有不是雷的坐标没有被排查完,一旦tmp=row*col-COUNT时,说明所有不是雷的坐标都被排查完了,玩家胜利了,而在这个循环过程中,玩家一旦踩到雷,我们就break跳出循环,结束游戏。

当玩家输入的坐标处没有雷的话,我们就要写一个函数来统计该坐标周围的8个坐标的雷的数量,即上面代码中的Get_Bomb_Count()函数,我们用ret来接收这个函数的返回值,因为这个函数的返回值为int类型,而show数组是一个char类型的数组,所以要给ret+'0',得到相应数字的字符,根据ASCII码表,0~9的数字和它们对应的字符‘0’~字符‘9’的差值都为48,即差了个‘0’的值:

 Get_Bomb_Count()函数的实现:

static int Get_Bomb_Count(char bomb[ROWS][COLS], int x, int y)
{
	return bomb[x][y - 1] +
		bomb[x + 1][y - 1] +
		bomb[x + 1][y] +
		bomb[x + 1][y + 1] +
		bomb[x][y + 1] +
		bomb[x - 1][y + 1] +
		bomb[x - 1][y] +
		bomb[x - 1][y - 1] - 8 * '0';

}

坐标(x,y)与它周围的八个坐标:

Get_Bomb_Count()函数实现之后,我们的扫雷游戏就可以玩了~

不过我们的代码只是实现了扫雷游戏的基本逻辑,还有很多需要优化的地方:

1、玩家可以标记或取消标记某一个疑似为雷的坐标;

2、玩家输入一个坐标后,游戏可以自动排查该坐标周围区域是否有雷。

优化程序

在玩家排雷之前,我们可以先给出一个菜单,选择1是排雷,选择2是标记雷,选择3是取消标记:

接下来,我们就需要再次实现4个函数:

1、排查坐标周围雷的函数

2、标记雷的函数

3、取消标记的函数

4、判断胜利的函数

由于现在我们的代码可以排查一个坐标周围雷的情况,所以判断胜利的条件也要发生改变。

排查坐标周围雷的函数

void Around_Bomb_count(char bomb[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{
	if (x == 0 || y == 0 || x == ROWS - 1 || y == COLS - 1)//判断坐标是否越界
	{
		return;
	}
	if (show[x][y] != '*')//判断坐标是否被排查过
	{
		return;
	}
	int count = Get_Bomb_Count(bomb, x, y);
	if (count > 0)
	{
		show[x][y] = count + '0';
		return;
	}
	else if (count == 0)
	{
		show[x][y] = ' ';
		Around_Bomb_count(bomb, show, x - 1, y);
		Around_Bomb_count(bomb, show, x - 1, y - 1);
		Around_Bomb_count(bomb, show, x, y - 1);
		Around_Bomb_count(bomb, show, x + 1, y - 1);
		Around_Bomb_count(bomb, show, x + 1, y);
		Around_Bomb_count(bomb, show, x + 1, y + 1);
		Around_Bomb_count(bomb, show, x, y + 1);
		Around_Bomb_count(bomb, show, x - 1, y + 1);
	}
}

解析:每次进入这个函数,我们首先要判断坐标是否越界,以及这个坐标是否已经被排查过(会出现一个坐标被排查很多次的情况),然后再调用之前的Get_Bomb_Count()函数计算这个坐标处雷的个数,如果这个坐标处有雷,就返回,不执行后面的语句;如果这个坐标处没雷,我们就把这个坐标置为空格(感觉比置为0更好看),并且把这个坐标周围的8个坐标作为新的参数传给这个函数,以递归的方式实现拓展式排雷。

没雷的情况下,置为‘0’和置为空格的效果对比:

 

标记雷的函数

void MarkBomb(char show[ROWS][COLS], int x, int y)//标记雷
{
	if (show[x][y] == '*')
	show[x][y] = '#';
}

坐标为’*‘的时候,即未被排查过的坐标才可以被标记。

取消标记的函数

void CancelMark(char show[ROWS][COLS], int x, int y)//取消标记
{
	if (show[x][y] == '#')
	show[x][y] = '*';
}

坐标为‘#’的时候,即被标记过的坐标才可以被取消标记。

判断胜利的函数

int IsWin(char show[ROWS][COLS], int row,int col)//判断是否胜利
{
	int i = 0;
	int j = 0;
	int count = 0;
	for (i = 0;i < row;i++)
	{
		for (j = 0;j < col;j++)//遍历整个棋盘
		{
			if (show[i][j] == '*' || show[i][j] == '#')
			count++;//未被排查的坐标和被标记的坐标数量之和
		}
	}
	//未被排查的坐标和被标记的坐标数量之和如果等于雷的数量
	//说明玩家已经排完了所有的非雷坐标,玩家胜利了
	//玩家胜利返回1,没有胜利返回0
	if (count == COUNT)
	{
		return 1;
	}
	else
		return 0;
}

更新排查雷函数

void FindBomb(char bomb[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int z = 0;
	int ret = 0;
	while (1)
	{
		menu2();
		printf("请选择:>\n");
		scanf("%d", &z);
		if (z == 1)
		{
			printf("请输入要排查的雷的坐标:>\n");
again:
			scanf("%d %d", &x, &y);
			if (x >= 1 && x <= row && y >= 1 && y <= col)//判断坐标合法性
			{
				if (show[x][y] == '*')//判断坐标是否被排查过
				{
					if (bomb[x][y] == '1')//判断坐标处是不是雷
					{
						printf("很遗憾,你被炸死了\n");
						DisplayBoard(bomb, ROW, COL);//打印出棋盘,让玩家“死”的瞑目
						break;
					}
					else
					{
						Around_Bomb_count(bomb, show, x, y);//排查该坐标周围8个坐标雷的个数
						DisplayBoard(show, ROW, COL);//更新棋盘信息
					}
				}
				else
				{
					printf("该位置已经被排查过,请重新输入:>\n");
					goto again;
				}
			}
			else
			{
				printf("请输入合法的坐标:>\n");
				goto again;
			}
		}
		else if (z == 2)
		{
			printf("请输入要标记的坐标:>\n");
			scanf("%d %d", &x, &y);
			MarkBomb(show, x, y);//标记某一坐标
			DisplayBoard(show, ROW, COL);
		}
		else if(z == 3)
		{
			printf("请输入要取消标记的坐标:>\n");
			scanf("%d %d", &x, &y);
			CancelMark(show, x, y);//取消标记
			DisplayBoard(show, ROW, COL);
		}
		else
		{
			printf("输入错误,请重新输入:>\n");
		}
		ret = IsWin(show, row, col);//判断胜利
		if (ret == 1)
		{
			break;
		}
	}
	if (ret == 1)
	{
		printf("恭喜你胜利了\n");
	}
}

整个程序的代码:

tese.c

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

void game()
{
	char bomb[ROWS][COLS] = { 0 };//存放布置好的雷的信息
	char show[ROWS][COLS] = { 0 };//存放排查出的雷的信息
	//初始化棋盘
	InitBoard(bomb, ROWS, COLS, '0');
	InitBoard(show, ROWS, COLS, '*');
	//打印棋盘
	//DisplayBoard(bomb, ROW, COL);
	DisplayBoard(show, ROW, COL);

	//设置雷
	SetBomb(bomb, ROW, COL);
	//DisplayBoard(bomb, ROW, COL);

	//排查雷
	FindBomb(bomb, show, ROW, COL);
}
int main()
{
	srand((unsigned int)time(NULL));
	int input = 0;
	do
	{
		menu();
		printf("请选择:>\n");
		scanf("%d", &input);
		switch(input)
		{
		case 1:
			printf("欢迎来到扫雷游戏\n");
			game();
			break;
		case 0:
			printf("退出程序\n");
			break;
		default:
			printf("选择错误,请重新选择:>\n");
			break;
		}
	} while (input);
	return 0;
}

game.h

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

//符号定义
#define ROW 9
#define COL 9

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

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

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

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

//排查雷
void FindBomb(char bomb[ROWS][COLS], char show[ROWS][COLS], int row, int col);

game.c

#include "game1.h"

//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char ch)
{
	int i = 0;
	int j = 0;
	for (i = 0;i < rows;i++)
	{
		for (j = 0;j < cols;j++)
		{
			board[i][j] = ch;
		}
	}
}

//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	int j = 0;
	for (i = 0;i <= row;i++)//打印列号
	{
		printf("%d ", i);
	}
	printf("\n");
	for (i = 1;i <= row;i++)
	{
		printf("%d ", i);//打印行号
		for (j = 1;j <= col;j++)
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");//打印完一行之后换行
	}
}

//设置雷
void SetBomb(char board[ROWS][COLS], int row, int col)
{
	//设置10个雷
	int count = COUNT;

	while (count)
	{
		//生成雷的随机下标
		int x = (rand() % row) + 1;//范围在1—9之间
		int y = (rand() % col) + 1;//范围在1—9之间

		if (board[x][y] == '0')//字符'0'说明没有雷
		{
			board[x][y] = '1';
			count--;
		}
	}
}

//排查一个坐标是不是雷
static int Get_Bomb_Count(char bomb[ROWS][COLS], int x, int y)
{
	return bomb[x][y - 1] +
		bomb[x + 1][y - 1] +
		bomb[x + 1][y] +
		bomb[x + 1][y + 1] +
		bomb[x][y + 1] +
		bomb[x - 1][y + 1] +
		bomb[x - 1][y] +
		bomb[x - 1][y - 1] - 8 * '0';

}

//排查坐标周围的雷
void Around_Bomb_count(char bomb[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{
	if (x == 0 || y == 0 || x == ROWS - 1 || y == COLS - 1)//判断坐标是否越界
	{
		return;
	}
	if (show[x][y] != '*')//判断坐标是否被排查过
	{
		return;
	}
	int count = Get_Bomb_Count(bomb, x, y);
	if (count > 0)
	{
		show[x][y] = count + '0';
		return;
	}
	else if (count == 0)
	{
		show[x][y] = ' ';
		Around_Bomb_count(bomb, show, x - 1, y);
		Around_Bomb_count(bomb, show, x - 1, y - 1);
		Around_Bomb_count(bomb, show, x, y - 1);
		Around_Bomb_count(bomb, show, x + 1, y - 1);
		Around_Bomb_count(bomb, show, x + 1, y);
		Around_Bomb_count(bomb, show, x + 1, y + 1);
		Around_Bomb_count(bomb, show, x, y + 1);
		Around_Bomb_count(bomb, show, x - 1, y + 1);
	}
}

void menu2()
{
	printf("****************************\n");
	printf("**********1.排查雷**********\n");
	printf("**********2.标记雷**********\n");
	printf("**********3.取消标记********\n");
	printf("****************************\n");
}

void MarkBomb(char show[ROWS][COLS], int x, int y)//标记雷
{
	if (show[x][y] == '*')
	show[x][y] = '#';
}

void CancelMark(char show[ROWS][COLS], int x, int y)//取消标记
{
	if (show[x][y] == '#')
	show[x][y] = '*';
}

int IsWin(char show[ROWS][COLS], int row,int col)//判断是否胜利
{
	int i = 0;
	int j = 0;
	int count = 0;
	for (i = 0;i < row;i++)
	{
		for (j = 0;j < col;j++)//遍历整个棋盘
		{
			if (show[i][j] == '*' || show[i][j] == '#')
			count++;//未被排查的坐标和被标记的坐标数量之和
		}
	}
	//未被排查的坐标和被标记的坐标数量之和如果等于雷的数量
	//说明玩家已经排完了所有的非雷坐标,玩家胜利了
	//玩家胜利返回1,没有胜利返回0
	if (count == COUNT)
	{
		return 1;
	}
	else
		return 0;
}
void FindBomb(char bomb[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int z = 0;
	int ret = 0;
	while (1)
	{
		menu2();
		printf("请选择:>\n");
		scanf("%d", &z);
		if (z == 1)
		{
			printf("请输入要排查的雷的坐标:>\n");
again:
			scanf("%d %d", &x, &y);
			if (x >= 1 && x <= row && y >= 1 && y <= col)//判断坐标合法性
			{
				if (show[x][y] == '*')//判断坐标是否被排查过
				{
					if (bomb[x][y] == '1')//判断坐标处是不是雷
					{
						printf("很遗憾,你被炸死了\n");
						DisplayBoard(bomb, ROW, COL);//打印出棋盘,让玩家“死”的瞑目
						break;
					}
					else
					{
						Around_Bomb_count(bomb, show, x, y);//排查该坐标周围8个坐标雷的个数
						DisplayBoard(show, ROW, COL);//更新棋盘信息
					}
				}
				else
				{
					printf("该位置已经被排查过,请重新输入:>\n");
					goto again;
				}
			}
			else
			{
				printf("请输入合法的坐标:>\n");
				goto again;
			}
		}
		else if (z == 2)
		{
			printf("请输入要标记的坐标:>\n");
			scanf("%d %d", &x, &y);
			MarkBomb(show, x, y);//标记某一坐标
			DisplayBoard(show, ROW, COL);
		}
		else if(z == 3)
		{
			printf("请输入要取消标记的坐标:>\n");
			scanf("%d %d", &x, &y);
			CancelMark(show, x, y);//取消标记
			DisplayBoard(show, ROW, COL);
		}
		else
		{
			printf("输入错误,请重新输入:>\n");
		}
		ret = IsWin(show, row, col);//判断胜利
		if (ret == 1)
		{
			break;
		}
	}
	if (ret == 1)
	{
		printf("恭喜你胜利了\n");
	}
}

感谢大佬的浏览~

  • 8
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

吃点橘子

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

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

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

打赏作者

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

抵扣说明:

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

余额充值