【C语言】扫雷的模拟实现详解

前言
扫雷是我们同年的小游戏,今天我们就来实现一下这个童年小游戏。

在这里插入图片描述

与上次三子棋一样,我们用三个文件完成这个程序。

一. 设计思路

1.选项

每一个游戏都有属于自己的游戏选择界面

(1 / 0)  玩游戏/退出游戏    &&   输入其他数字则输入错误重新输入
void menu()
{
    
    
	printf("**************************\n");
	printf("******    1.play    ******\n");
	printf("******    0.exit    ******\n");
	printf("**************************\n");
}
#include "game.h"

int main()
{
    
    
	int input = 0;
	menu();
	do
	{
    
    
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
    
    
		case 1:
			game();
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("输入错误\n");
			break;
		}
		
	} while (input);
	return 0;
}
游戏模块
void game()
{
    
    
	char mine[ROWS][COLS] = {
    
     0 };
	char show[ROWS][COLS] = {
    
     0 };
	//初始化棋盘
	InitBoard(mine, ROWS, COLS , '0');
	InitBoard(show, ROWS, COLS , '*');
	//打印棋盘
	DisplayBoard(show, ROW, COL);
	//布置雷
	SetMine(mine, ROW, COL);
	DisplayBoard(mine, ROW, COL);
	//排雷
	FindMine(mine, show, ROW, COL);
}

主函数中的#include 并没有使用库函数的头文件,而是使用自己写出来的game.h文件
在这里插入图片描述

2. 扫雷游戏的设计

我们用两个数组[show 数组 (用户排雷观看用)与 mine数组(布置雷用)], show数组用来展示该坐标周围八个坐标有多少个雷。当我学习了一位大佬的设计,mine数组用来设置地雷,用字符'1'来设置为,而字符'0'非雷,当然我们也可以用其他字符来设置雷,但是当我们看了后续代码就可以看出大佬这样设计的方便之处了。

show数组(棋盘)

在这里插入图片描述

mine数组(棋盘)

在这里插入图片描述
当我们设置这么一个 9 x 9 的二维数组,就会发现当选择的坐标不是边界的坐标时,只需要统计周围八个坐标是否是雷,而如果是选择边界的坐标时,若访问周围八个坐标时,则有可能会越界访问,并且如果我们将这些特殊情况单独列出来,对于我们目前来说还是有些许困难,所以我选择在这个9 x 9 的二维数组加大一圈
在这里插入图片描述
如果我们在我们需要的大小下再加大一圈的话,就不需要担心这个问题了。若我们需要一个 9 x 9 的扫雷界面 ,我们则设计一个11 x 11 的数组。像下图,我们需要的是蓝色部分,但是如果我们选择的是边界,周围八个坐标都在11 x 11 的数组中,也不会出现数组越界的情况
在这里插入图片描述

3. 棋盘的初始化

我们要将棋盘的内容进行初始化。当我们进入一个扫雷游戏的时候,刚开始我们什么都看不见,所以我们在给用户显示的棋盘上都初始化为*,对于布置雷的棋盘先全部初始化为'0',之后再布置雷。

当我们看下面这段初始化代码,这里的board[i][j]我们并不知道放什么值,假设初始化mine棋盘'0',那么我们还要设计一个初始化show棋盘的函数,相反也是,无论我们传什么进去都要设计两个函数,下面对该函数进行优化。

void InitBoard(char board[ROWS][COLS], int rows, int cols)
{
    
    
	int i = 0;
	int j = 0;
	for (i = 0; i < rows; i++)
	{
    
    
		for (j = 0; j < cols; j++)
		{
    
    
			board[i][j] = ; // 0 ? *
		}
	}
}
当我们将函数多一个参数,我们想初始化什么就将该字符传进去。
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)
{
    
    
	int i = 0;
	int j = 0;
	for (i = 0; i < rows; i++)
	{
    
    
		for (j = 0; j < cols; j++)
		{
    
    
			board[i][j] = set;
		}
	}
}

4. 布置雷的函数

注意事项:
(1)布置雷的时候,记得控制坐标的范围,防止越界。
(2)布置雷的时候,要判断该坐标是否为非雷的坐标。
(3)记得rand()函数使用的必要条件。

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

5. 棋盘的打印

这里做一个用户方便看的棋盘可能要试验好几次。
记得在每一行,每一列记录一下行号和列号,方便用户输入坐标。

void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
    
    
	int i = 1;
	int j = 1;
	printf(" ***************  扫雷  ***************\n");
	for (j = 0; j <= col; j++)
	{
    
    
		printf(" %d  ", j);     //记录行号
	}
	printf("\n\n");
	for (i = 1; i <= row; i++)  
	{
    
    
		printf(" %d  ", i);    记录列号
		for (j = 1; j <= col; j++)
		{
    
    
			printf(" %c ", board[i][j]);
			if (j != col)
			{
    
    
				printf("|");
			}
		}
		printf("\n");
		if (i != row)
		{
    
    
			printf("    ");
			for (j = 1; j <= col; j++)
			{
    
    
				printf("---");
				if (j != col)
				{
    
    
					printf("|");
				}
			}
			
		}
		printf("\n");
	}
}

6. 排雷函数

6.1 排雷

void FindMine(char mine[ROWS][COLS], 
	char show[ROWS][COLS], int row, int col)
{
    
    
	int x = 0;
	int y = 0;
	int win = 0;
	char ch = 0;
	while (win < row * col - EASY_COUNT)   
	{
    
    
		printf("请输入需要排除的坐标:>");
		scanf("%d%d", &x, &y);
		if (x >= 1 && x <= row && y <= col && y >= 1)
		{
    
    
			if (show[x][y] == '*')          //判断该坐标是否已经被排除
			{
    
    
				if (mine[x][y] == '1')      //判断是否为雷
				{
    
    
					printf("你输了,游戏结束\n");
					DisplayBoard(mine, ROW, COL);
					return;
				}
				else
				{
    
    
					//记录排除坐标的个数
					win += Explode_spread(show , mine , row , col , x ,y);
					DisplayBoard(show, ROW, COL);
				}
			}
			else
			{
    
    
				printf("该坐标已被占用\n");
			}
		}
		else
		{
    
    
			printf("输入的坐标非法\n");
		}

		printf("是否需要标记地址,若需要则输入'Y',否则则输入’N'\n");
		while ((ch = getchar()) != '\n'); //清理缓存区
		scanf("%c", &ch);
		switch (ch)
		{
    
    
		case 'Y':
			SignMine(show, row, col);
			break;
		case 'N':
			break;
		}
	}

	if (win == row * col - EASY_COUNT)
	{
    
    
		printf("恭喜你,游戏胜利\n");
	}
}

6.2 记录周围雷个数

这里统计雷个数的方法很巧妙。
若用其他的定义雷,把周围的八个坐标全部判断会有点麻烦,但是这里用'1'定义雷只需要将八个坐标的值相加再与8 * '0' 相减则能直接得到雷的个数。

int MineCount(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]) - 8 * '0';
}

6.3 非雷的连片展开

在这里插入图片描述
在这里插入图片描述

扫雷游戏中,当排查的坐标周围的八个坐标没有一个雷show棋盘上该坐标显示为空格,然后排查的的范围将会扩大,将周围的八个坐标挨个向前面一样判断,直到遇到周围有雷的时候停止,并且将周围的雷的个数记录在show棋盘上。

注意事项:
(1)我们设计的这个函数为递归函数,必须有条件来限制,否则会造成死递归。这里我们看到两个重合的位置,若不加限制条件,这几个坐标相互向外判断,造成死循环。
(2)注意控制坐标的合法性,不能让数组越界访问
在这里插入图片描述

int Explode_spread(char show[ROWS][COLS],
	char mine[ROWS][COLS],int row,int col,int x,int y)
{
    
    
	int i = 0, j = 0;
	int win = 0;
	if (x >= 1 && x <= row && y <= col && y >= 1)
	{
    
    
		//防止数组越界
		if ((MineCount(mine, x, y) + '0') == '0')
		{
    
    
			//若该坐标周围八个坐标都没有雷,向外扩散
			show[x][y] =' ';
			for (i = x - 1; i <= x + 1; i++)
			{
    
    
				for (j = y - 1; j <= y + 1; j++)
				{
    
    
					if (show[i][j] == '*')
					{
    
    
						//判断是否该坐标已经被判断过
						Explode_spread(show, mine, row, col, i, j);
						win++;
					}

				}
			}
		}
		else
		{
    
    
			show[x][y] = MineCount(mine, x, y) + '0';
			win++;
		}
	}
	return win;
}

6.4 标雷

void SignMine(char show[ROWS][COLS],int row, int col)
{
    
    
	static sign = EASY_COUNT;  //标记的次数
	char option = 'Y';
	int x = 0;
	int y = 0;
	char ch = 0;
	if (sign)    //现在标记次数
	{
    
    
		while (option == 'Y')
		{
    
    
			printf("还能标记%d个坐标\n", sign);
			printf("请输入需要标记的坐标:>");
			scanf("%d%d", &x, &y);
			if (x >= 1 && x <= row && y <= col && y >= 1)
			{
    
    
				if (show[x][y] == '*')
				{
    
    
					show[x][y] = '?';
					sign--;
					DisplayBoard(show, ROW, COL);
				}
				else
				{
    
    
					printf("该坐标已被占用\n");
				}
			}
			else
			{
    
    
				printf("输入的坐标非法\n");
			}
			printf("是否需要继续标记地址,若需要则输入'Y',否则则输入’N'\n");
			while ((ch = getchar()) != '\n'); //清理缓存区
			scanf("%c", &option);
		}
	}
	else
	{
    
    
		printf("标记次数过多\n");
		return;
	}
}

二. 整体程序的完成

1. game.h文件 :声明

#pragma once

#define ROW 9               //需要的行
#define COL 9				//需要的列
#define ROWS  ROW + 2		//加大一圈的行
#define COLS  COL + 2		//加大一圈的列
#define EASY_COUNT 10       //简单版本的地雷个数

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

//初始化棋盘
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 mine[ROWS][COLS], int row, int col);
//排雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);

2. game.c文件 :扫雷的实现模块

#include "game.h"
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)
{
    
    
	int i = 0;
	int j = 0;
	for (i = 0; i < rows; i++)
	{
    
    
		for (j = 0; j < cols; j++)
		{
    
    
			board[i][j] = set;
		}
	}
}

//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
    
    
	int i = 1;
	int j = 1;
	printf(" ***************  扫雷  ***************\n");
	for (j = 0; j <= col; j++)
	{
    
    
		printf(" %d  ", j);    //记录行号
	}
	printf("\n\n");
	for (i = 1; i <= row; i++)
	{
    
    
		printf(" %d  ", i);    //记录列号
		for (j = 1; j <= col; j++)
		{
    
    
			printf(" %c ", board[i][j]);
			if (j != col)
			{
    
    
				printf("|");
			}
		}
		printf("\n");
		if (i != row)
		{
    
    
			printf("    ");
			for (j = 1; j <= col; j++)
			{
    
    
				printf("---");
				if (j != col)
				{
    
    
					printf("|");
				}
			}
			
		}
		printf("\n");
	}
}

//布置地雷
void SetMine(char mine[ROWS][COLS], int row, int col)
{
    
    
	int count = EASY_COUNT;
	while (count)
	{
    
    
		int x = rand() % row + 1;
		int y = rand() % col + 1;
		if (x >= 1 && x <= row && y <= col && y >= 1)
		{
    
    
			if (mine[x][y] == '0')
			{
    
    
				mine[x][y] = '1';
				count--;
			}
		}
	}
}

//记录一个坐标周围有多少个地雷
int MineCount(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]) - 8 * '0';
}

//若一个坐标周围八个坐标都没有雷,连片展开
int Explode_spread(char show[ROWS][COLS],
	char mine[ROWS][COLS],int row,int col,int x,int y)
{
    
    
	int i = 0, j = 0;
	int win = 0;
	if (x >= 1 && x <= row && y <= col && y >= 1)
	{
    
    
		//防止数组越界
		if ((MineCount(mine, x, y) + '0') == '0')
		{
    
    
			//若该坐标周围八个坐标都没有雷,向外扩散
			show[x][y] =' ';
			for (i = x - 1; i <= x + 1; i++)
			{
    
    
				for (j = y - 1; j <= y + 1; j++)
				{
    
    
					if (show[i][j] == '*')
					{
    
    
						//判断是否该坐标已经被判断过
						Explode_spread(show, mine, row, col, i, j);
						win++;
					}

				}
			}
		}
		else
		{
    
    
			show[x][y] = MineCount(mine, x, y) + '0';
			win++;
		}
	}
	return win;
}

//标记地雷
void SignMine(char show[ROWS][COLS],int row, int col)
{
    
    
	static sign = EASY_COUNT;
	char option = 'Y';
	int x = 0;
	int y = 0;
	char ch = 0;
	if (sign)
	{
    
    
		while (option == 'Y')
		{
    
    
			printf("还能标记%d个坐标\n", sign);
			printf("请输入需要标记的坐标:>");
			scanf("%d%d", &x, &y);
			if (x >= 1 && x <= row && y <= col && y >= 1)
			{
    
    
				if (show[x][y] == '*')
				{
    
    
					show[x][y] = '?';
					sign--;
					DisplayBoard(show, ROW, COL);
				}
				else
				{
    
    
					printf("该坐标已被占用\n");
				}
			}
			else
			{
    
    
				printf("输入的坐标非法\n");
			}
			printf("是否需要继续标记地址,若需要则输入'Y',否则则输入’N'\n");
			while ((ch = getchar()) != '\n'); //清理缓存区
			scanf("%c", &option);
		}
	}
	else
	{
    
    
		printf("标记次数过多\n");
		return;
	}
}

//排雷
void FindMine(char mine[ROWS][COLS], 
	char show[ROWS][COLS], int row, int col)
{
    
    
	int x = 0;
	int y = 0;
	int win = 0;
	char ch = 0;
	while (win < row * col - EASY_COUNT)   
	{
    
    
		printf("请输入需要排除的坐标:>");
		scanf("%d%d", &x, &y);
		if (x >= 1 && x <= row && y <= col && y >= 1)
		{
    
    
			if (show[x][y] == '*')          //判断该坐标是否已经被排除
			{
    
    
				if (mine[x][y] == '1')      //判断是否为雷
				{
    
    
					printf("你输了,游戏结束\n");
					DisplayBoard(mine, ROW, COL);
					return;
				}
				else
				{
    
    
					//记录排除坐标的个数
					win += Explode_spread(show , mine , row , col , x ,y);
					DisplayBoard(show, ROW, COL);
				}
			}
			else
			{
    
    
				printf("该坐标已被占用\n");
			}
		}
		else
		{
    
    
			printf("输入的坐标非法\n");
		}

		printf("是否需要标记地址,若需要则输入'Y',否则则输入’N'\n");
		while ((ch = getchar()) != '\n'); //清理缓存区
		scanf("%c", &ch);
		switch (ch)
		{
    
    
		case 'Y':
			SignMine(show, row, col);
			break;
		case 'N':
			break;
		}
	}

	if (win == row * col - EASY_COUNT)
	{
    
    
		printf("恭喜你,游戏胜利\n");
	}
}

3. test.c文件:测试扫雷的基本功能

#include "game.h"

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

void game()
{
    
    
	//定义两个11 x 11 的数组(棋盘)
	char mine[ROWS][COLS] = {
    
     0 };
	char show[ROWS][COLS] = {
    
     0 };
	//初始化棋盘
	InitBoard(mine, ROWS, COLS , '0');
	InitBoard(show, ROWS, COLS , '*');
	//打印棋盘
	DisplayBoard(show, ROW, COL);
	//布置雷
	SetMine(mine, ROW, COL);
	DisplayBoard(mine, ROW, COL);
	//排雷
	FindMine(mine, show, ROW, COL);
}


int main()
{
    
    
	int input = 0;
	srand((unsigned)time(NULL));
	menu();
	do
	{
    
    
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
    
    
		case 1:
			game();
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("输入错误\n");
			break;
		}
		
	} while (input);
	return 0;
}

结尾

如果有什么建议和疑问,或是有什么错误,希望大家能够提一下。
希望大家以后也能和我一起进步!!
如果这篇文章对你有用的话,希望能给我一个小小的赞!

猜你喜欢

转载自blog.csdn.net/qq_55401402/article/details/129604967
今日推荐