C语言扫雷小游戏(VS2022版)

        大家好,在这篇文章中我将分享在VS2022中自己做扫雷小游戏的经验,若有错误还请大家指出。

        首先,简单的介绍扫雷游戏j基础玩法

       1.扫雷要把所有非地雷的格子打开就算胜利,踩到地雷格子就失败。

        2.非雷方格被打开并显示出方格中的数字,方格中数字则表示其周围的8个方格隐藏了几颗雷,如果点开的格子周围有0颗雷,则周围格子自动打开

        接下来要在VS2022中准备好三个文件,一是 game.h 头文件(游戏函数的声明与引用),二是 game.c 源文件(游戏函数的实现与更改),三是 test.c 源文件(游戏界面与大致框架)。

        一、扫雷游戏的界面实现

        制作简单的界面,也需要将各个函数分配清楚。这一部分我们只要准备好四个函数在 test.c源文件 中就可以将框架完成:1.主函数 main函数2.游戏流程 test函数3.游戏菜单 menu函数 、    4.游戏执行 game函数

        当然,为了自定义函数和库函数的方便使用,应该将库函数头文件放入 game.h 头文件中,在其他两个源文件中引用 game.h 头文件

        1.主函数 main函数

        main函数是程序的入口,很重要,所以里面的内容要精简点:

                                

        2.游戏流程 test函数

        游戏流程函数的实现要从玩家视角去思考:首先玩家打开游戏会出现游戏界面,然后玩家选择玩游戏则进入游戏执行函数,玩家选择退出游戏则程序结束:                           

        玩家选择操作用一个变量来实现,而由于游戏流程至少要执行一次,恰好do-while循环有这个特点,所以选择do-while循环,紧接着用switch语句来实现分支操作,最后 test函数 不需要返回值则用 void 类型。 

        这里的 do-while循环 和 switch语句 能做到一个较好的联系:当 input变量 为1,执行到 while判断 时则为真,就进入下一次循环,当 input变量 为0,执行到 while判断 时则为假,就跳出循环

        3.游戏菜单 menu函数

        游戏菜单只需使用 printf库函数 便可实现,也不用返回值,但要注意与 test函数swich语句 实现的功能相对应:

           

        4.游戏执行 game函数

        游戏执行 game函数 包含两个基本函数:数据初始化函数(initBoard)打印画面函数(display)。还有实现扫雷游戏核心的 布置雷函数(setMine)找雷函数(findMine)展开函数(spread)。这里只是梳理一下 game函数 的大致内容,由于会联系到扫雷的核心代码,所以细致的讲解会在下一部分。

        总之,界面实现部分很简单,这个界面也适用于其他游戏,不需要费多少脑筋。

        二、扫雷游戏的基础内容实现

        这一部分会实现扫雷的核心代码,主要分为:1.扫雷的前期布置2.初始化函数(initBoard)3.打印函数(display)4.布置雷函数(setMine)5.找雷函数(findMine)6.展开函数(spread)

        1.扫雷的前期布置

        我们以简单模式(9×9)的扫雷来讲解:

        在9×9的扫雷模式里增加边框(红色图形,也就是增加数组大小),可方便边角计算雷的数量,防止越界,且利于玩家输入的坐标与数组下标对应。

        所以接下来:

                

        使用 #define 定义ROW、COL、ROWS、COLS这四个标识符,方便后面括大或缩小棋盘。

        而扫雷的棋盘准备两个:一个是 mine 放置雷,二个是 show 展示给玩家看。当然,两者数组长度应该保持一样为11×11:

                

        2.初始化函数(initBoard)

        初始化函数目标是将 mine 与 show 初始化为游戏开始的状态,所以这里将 mine 所有的元素(11×11)初始化成字符 ' 0 ' ,show 所有的元素(11×11)则初始化为字符 ' * '。前者初始化成字符 ' 0 ' ,是方便随机放入字符 ' 1 ' 的雷时利于计算,而后者初始化的字符可自己随意设计

         在 game.h头文件 中:

        在 game.c源文件 中:

        在 test.c源文件 中:

                

        这个函数要注意的是初始化 mine 和 show 时的字符,只需增加一个参数 char set 来接收要用于初始化的字符即可。

        3.打印函数(display)

        打印函数的目的是将棋盘呈现给玩家,传参的个数应该为9×9。

        在 game.h头文件 中:

        在 game.c源文件 中:

        在 test.c源文件 中:

     

        程序执行的效果:

        4.布置雷函数(setMine)

        布置雷函数目的是在 mine 初始化后随机地放置雷在 mine 中,需要设置srand与rand函数。

        在 game.h头文件 中:

        在 game.c源文件 中:

        在 test.c源文件 中:

        程序执行效果:

        5.找雷函数(findMine)

        找雷函数中内容包含:①.玩家操作②.计算雷的个数函数(mineCount)③.游戏输赢判定

        先布置完毕框架:

        在 game.h头文件 中:

        在 game.c源文件 中:

        在 test.c源文件 中:

               

        以下实现找雷函数的代码都在 game.c源文件 中:

        ①.玩家操作

        玩家查找时输入的坐标需要判定:一.是否超出扫雷方格的范围、二.是否被占用、三.是否是雷

        ②.计算雷的个数函数(mineCount)

        要计算坐标周围的雷的数量,我们需要知道其周围的坐标,如图:

                

        通过图片,我们就可以清楚地写出代码了:

程序展示:

                                        

        注意,计算雷的个数时要加上字符 ' 0 ' ,因为 count 得到的是整型数字的雷的个数,要把整型数字转为字符数字要加上' 0 ' (字符' 0 '的ASCII为48,换成加48也一样)

                

        ③.游戏输赢判定

        游戏进行时有三种状态:1.查找的坐标不是雷,坐标也没查找完、2.查找到雷,游戏失败、3.查找了所有的非雷坐标,游戏胜利。

这里设置了 win变量 计算查找到非雷的坐标数量row col - EASY_COUNT = 71,则需要找到非雷的71个坐标。                                                (    9  ×  9   -            10          )= 71

程序展示(这里将EASY_COUNT改成80,只需要找1个非雷坐标):

                                

                                

        6.展开函数(spread)

        展开函数目的是缩短找雷时间,这里用递归可以很方便的完成,但要明确条件:

     1.当查找的坐标不是雷且周围没有雷则进入递归

     2.递归时遇到周围有雷用数字标记的坐标则停下。(注意坐标仅限于9×9,不要把边框计算在内)

     3.递归一次后要改变坐标为非字符' * '防止反复递归同一坐标进入死循环

        在 game.h头文件 中:

        在 game.c源文件 中:

程序展示:

注意要把计算非雷数量的 win变量 的地址传进去,在递归一次后解引用 spread函数 中的 win指针变量 让其+1,不然即便找到所有的非雷坐标也不会赢

        7.扫雷游戏基础内容源代码

        game.h头文件:

#pragma once

#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <time.h> // time 函数需要的头文件
#include <stdlib.h> // rand 与 srand 函数需要的头文件 

#define ROW 9
#define COL 9

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

#define EASY_COUNT 10 // 雷的数量

void initBoard(char arr[ROWS][COLS], int rows, int cols, char set);

void display(char arr[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);

void spread(char mine[ROWS][COLS], char show[ROWS][COLS], int y, int x, int* win);

        game.c源文件:

#include "game.h"

void initBoard(char arr[ROWS][COLS], int rows, int cols, char set)
{
	for (int i = 0; i < rows; i++)
		for (int j = 0; j < cols; j++)
			arr[i][j] = set;
}

void display(char arr[ROWS][COLS], int row, int col)
{
	int i, j;
	printf("上-----扫雷游戏-----上\n");

	for (i = 0; i <= col; i++) // 打印列的数量
		printf("%d ", i);
	printf("\n"); // 换行

	for (i = 1; i <= row; i++)
	{
		printf("%d ", i); // 打印当前的行

		for (j = 1; j <= col; j++) // 打印扫雷内容
			printf("%c ", arr[i][j]);
		printf("\n"); // 换行
	}

	printf("下-----扫雷游戏-----下\n");
}

void setMine(char mine[ROWS][COLS], int row, int col)
{
	int count = 0;
	while (count < EASY_COUNT) // 布置雷的数量,数量到达时则跳出循环
	{
		int x = rand() % col + 1;
		int y = rand() % row + 1;
		if (mine[y][x] == '0') // 若为 '0',则放置雷
		{
			mine[y][x] = '1';
			count++;
		}
	}
}

int mineCount(char mine[ROWS][COLS], int y, int x)
{
	int count = 0;
	for (int i = -1; i <= 1; i++)
		for (int j = -1; j <= 1; j++)
			if (mine[y + i][x + j] == '1')
				count++;

	return count;
}

void findMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int y = 0;
	int x = 0;
	int win = 0;
	while (win < row * col - EASY_COUNT)
	{
		printf("请输入坐标:>");
		scanf("%d %d", &y, &x);
		if (y >= 1 && y <= row && x >= 1 && x <= col) // 判断输入的坐标是否在9行9列的范围内
		{
			if (show[y][x] == '*')                    // 判断输入的坐标是否被占用
			{
				if (mine[y][x] == '1')                // 判断输入的坐标是否是雷
				{
					printf("很遗憾,你被炸死了\n");
					display(mine, ROW, COL);          // 打印布置雷的棋盘
					break;                            // 跳出循环
				}
				else
				{
					int count = mineCount(mine, y, x);
					show[y][x] = count + '0';
					win++;
					spread(mine, show, y, x, &win);
					display(show, row, col);
				}
			}
			else
			{
				printf("坐标被占用,请重新输入\n");
			}
		}
		else
		{
			printf("坐标非法,请重新输入\n");
		}
	}
	if (win == row * col - EASY_COUNT)
		printf("你赢了\n");
}

void spread(char mine[ROWS][COLS], char show[ROWS][COLS], int y, int x, int* win)
{
	if (show[y][x] == '0')       // 若周围没有雷才进入
	{
		for (int i = -1; i <= 1; i++) // 行
		{
			for (int j = -1; j <= 1; j++) // 列
			{
				if (y + i >= 1 && y + i <= ROW && x + j >= 1 && x + j <= COL) // 防止超出9×9的范围
				{
					if (show[y + i][x + j] == '*')        // 防止反复递归同一个坐标
					{
						int count = mineCount(mine, y + i, x + j);
						show[y + i][x + j] = count + '0'; // 将已经递归过的坐标显示它周围雷的数量,防止反复递归同一个坐标
						(*win)++;     // 增加查找非雷坐标的数量
						spread(mine, show, y + i, x + j, win); // 进入下一次递归
					}
				}
			}
		}
	}
}

        test.c源文件:

#include "game.h"

void menu()
{
	printf("************************\n");
	printf("**** 1.play  0.exit ****\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);
	display(show, ROW, COL);
	//display(mine, ROW, COL);
	findMine(mine, show, ROW, COL);
}

void test()
{
	int input = 0;
	srand((unsigned int)time(NULL)); // 使rand函数产生伪随机数
	do
	{
		menu();
		printf("请输入操作:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			game();
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("输入错误,请重新输入\n");
			break;
		}
	} while (input);
}

int main()
{
	test();
	return 0;
}

        以上的内容已经可以实现扫雷游戏。由于我是以函数的视角来讲述扫雷,接下来一些细致的内容会修改之前讲的部分函数,若放在一起讲又不能将整体的逻辑阐述明白,所以我会放在下一部分。

        三、扫雷游戏的补充内容实现

        这一部分会补充扫雷游戏内容,分别为:1.避雷实现2.插旗操作3.数字展开函数(numberSpread)4.整体界面操作与代码优化

        1.避雷实现

        避雷实现指的是第一次查找坐标不会直接找到雷而失败,这里可以将布置雷函数(setMine)放在第一次查找后再布置雷,布置时并避开其查找的3×3的坐标(所以要加查找的坐标参数)。要注意的是,若将 setMine 修改为 布置雷坐标与第一次查找坐标相等就重新选择行与列这种功能,这样的效率其实可以改进。

        我们可以这样分两种情况:

        1.当第一次查找的行和其旁边两行不同于布置雷的行(蓝色为相同,橙色为不同),则列就没有避雷限制:

        以第一次查找的坐标6行5列举例子,若在1、2、3、4、8、9行布置雷就不需要限制列了。

        2.当第一次查找的行或其旁边两行有等于布置雷的行,则列需要避雷的限制:

        以第一次查找的坐标6行5列举例子,若在5、6、7行布置雷就需要限制列不能在4、5、6列。

        知道原理,就可以用代码实现了。

        在 game.h头文件 中:

        在 game.c源文件 中:

        在 test.c源文件 中(删除setMine):

·

        2.插旗操作

        想要插旗操作,则应该在坐标输入后进行选择。

        3.数字展开函数(numberSpread)

        插旗操作主要是服务于 数字展开函数(numberSpread),而数字展开意思为大于字符 ' 0 ' 的数字坐标会展开周围没插旗的未知坐标。我们要明确这个函数的使用条件:

        1.当数字字符(即周围雷的数量)周围未知坐标数量等于自身则不会展开。

        2.当数字字符周围旗的数量小于自身则不会展开。

        3.当数字字符周围未知且未插旗的坐标为零则不会展开。

        在 game.h头文件 中:

        在 game.c源文件 中:

        对于 numberSpread:

        对于 findMine:

        红色1、2、4数字处改变了游戏输赢逻辑,红色数字3中的第五个参数 show[y][x] - ' 0 ' 是因为传过去比较的 num变量 是整型,其减字符' 0 ',刚好转换成对应的数字,与计算雷的个数函数(mineCount)同理 。

        4.整体界面操作与代码优化

        现在的界面操作过于简陋,而且代码中含有一些不必要的 if-else语句嵌套,可更改一下。

        ①.在行列旁增加 " | "

        其方便分清行列与坐标:

        程序效果:

                        

        ②.插入清屏函数。

        其所需要的头文件 stdlib.h 已包含,只需在 game.c源文件 的display函数加上一句:

        这里要注意,它会把之前的图像与文字清除,若发现一些文字或图像没有出现,看看是不是将display函数放在了其后面,如数字展开函数中讲过的:

        程序效果:

                                

        ③.改变数字颜色

        使用 控制台\033方式 改变数字字符的数字颜色:

        程序效果:

                                

        ④.代码优化

        这里我实在是想不出其他地方的改动,只将 wantDo变量 周围的代码改动了一下,如果大神能出手提醒还请直接说出来。

        

        5.静态扫雷的源代码

        在 game.h头文件 中:

#pragma once

#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <time.h> // time 函数需要的头文件
#include <stdlib.h> // rand 与 srand 函数需要的头文件 

#define ROW 9
#define COL 9

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

#define EASY_COUNT 10 // 雷的数量

void initBoard(char arr[ROWS][COLS], int rows, int cols, char set);

void display(char arr[ROWS][COLS], int row, int col);

void setMine(char mine[ROWS][COLS], int row, int col, int y, int x);

void findMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);

void spread(char mine[ROWS][COLS], char show[ROWS][COLS], int y, int x, int* win);

void numberSpread(char mine[ROWS][COLS], char show[ROWS][COLS], int y, int x, int num, int* win);

        在 game.c源文件 中:

#include "game.h"

void initBoard(char arr[ROWS][COLS], int rows, int cols, char set)
{
	for (int i = 0; i < rows; i++)
		for (int j = 0; j < cols; j++)
			arr[i][j] = set;
}

void display(char arr[ROWS][COLS], int row, int col)
{
	system("cls");
	int i, j;
	printf("上-----扫雷游戏-----上\n|");

	for (i = 0; i <= col; i++) // 打印列的数量
		printf("%d|", i);
	printf("\n"); // 换行

	for (i = 1; i <= row; i++)
	{
		printf("|%d|", i); // 打印当前的行

		for (j = 1; j <= col; j++) // 打印扫雷内容
		{
			if (arr[i][j] == '*')
				printf("%c ", arr[i][j]);
			if (arr[i][j] == '0')
				printf("\033[30m%c\033[0m ", arr[i][j]); // 黑色
			if (arr[i][j] == '1')
				printf("\033[36m%c\033[0m ", arr[i][j]); // 浅蓝色
			if (arr[i][j] == '2')
				printf("\033[34m%c\033[0m ", arr[i][j]); // 蓝色
			if (arr[i][j] == '3')
				printf("\033[33m%c\033[0m ", arr[i][j]); // 黄色
			if (arr[i][j] == '4')
				printf("\033[31m%c\033[0m ", arr[i][j]); // 红色
			if (arr[i][j] == '5')
				printf("\033[35m%c\033[0m ", arr[i][j]); // 紫色
			if (arr[i][j] == '6')
				printf("%c ", arr[i][j]);
			if (arr[i][j] == '7')
				printf("%c ", arr[i][j]);
			if (arr[i][j] == 'F')
				printf("\033[32m%c\033[0m ", arr[i][j]); // 绿色
		}
		printf("\n"); // 换行
	}
	printf("下-----扫雷游戏-----下\n");
}

void setMine(char mine[ROWS][COLS], int row, int col, int y, int x)
{
	int count = 0;
	int i, j;
	// 记录输入行和旁边的两行
	int judgeRow[3] = { 0 }; 
	for (int k = -1, a = 0; k <= 1; k++, a++)
		judgeRow[a] = k + y;  

	// 记录输入列和旁边的两列
	int judgeCol[3] = { 0 }; 
	for (int k = -1, a = 0; k <= 1; k++, a++)
		judgeCol[a] = k + x;

	while (count < EASY_COUNT) // 布置雷的数量,数量到达时则跳出循环
	{
		i = rand() % col + 1; // 行

		// 当输入行且旁边两行与布置行不同时
		if (i != judgeRow[0] && i != judgeRow[1] && i != judgeRow[2]) 
			j = rand() % row + 1; // 列

		// 当输入行或旁边两行与布置行相同时
		else
		{
			do
			{
				j = rand() % col + 1;
				// 若输入列或旁边两列与布置列相同则进入循环
			} while (j == judgeCol[0] || j == judgeCol[1] || j == judgeCol[2]);
		}
		if (mine[i][j] == '0') // 若为 '0',则放置雷
		{
			mine[i][j] = '1';
			count++;
		}
	}
}

int mineCount(char mine[ROWS][COLS], int y, int x)
{
	int count = 0;
	for (int i = -1; i <= 1; i++)
		for (int j = -1; j <= 1; j++)
			if (mine[y + i][x + j] == '1')
				count++;

	return count;
}

void select()
{
	printf("************************************************\n");
	printf("**** 0.返回 1.查找 2.插旗 3.取旗 4.数字展开 ****\n");
	printf("************************************************\n");
}

void operation(char mine[ROWS][COLS], char show[ROWS][COLS], int y, int x, int* first, int* win)
{
	int wantDo = 0;
	do
	{
		printf("请输入操作:>");
		scanf("%d", &wantDo);
		switch (wantDo)
		{
		case 1:
			if (show[y][x] == '*')                    // 判断输入的坐标是否被占用
			{
				if (*first)
				{
					setMine(mine, ROW, COL, y, x);
					*first = 0;
				}
				if (mine[y][x] == '1')                // 判断输入的坐标是否是雷
				{
					*win = -9;
				}
				else
				{
					int count = mineCount(mine, y, x);
					show[y][x] = count + '0';
					(*win)++;
					spread(mine, show, y, x, win);
					display(show, ROW, COL);
				}
				wantDo = 0;
			}
			else
				printf("坐标被占用,请重新输入\n");
			break;

		case 2:
			if (show[y][x] == '*')
			{
				show[y][x] = 'F';
				display(show, ROW, COL);
				wantDo = 0;
			}
			else
				printf("非未知坐标,请重新输入\n");
			break;

		case 3:
			if (show[y][x] == 'F')
			{
				show[y][x] = '*';
				display(show, ROW, COL);
				wantDo = 0;
			}
			else
				printf("非插旗坐标,请重新输入\n");
			break;

		case 4:
			if (show[y][x] >= '1' && show[y][x] <= '7')
			{
				numberSpread(mine, show, y, x, show[y][x] - '0', win);
				wantDo = 0;
			}
			else
				printf("非有效坐标,请重新选择\n");
			break;

		case 0:
			break;

		default:
			printf("输入错误,请重新输入\n");
			break;
		}
	} while (wantDo);
}

void findMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int y = 0;
	int x = 0;
	int win = 0;
	int first = 1;
	while (win < row * col - EASY_COUNT && win >= 0) // 当win为负数意思为被雷炸死
	{
		printf("请输入坐标:>");
		scanf("%d %d", &y, &x);
		if (y >= 1 && y <= row && x >= 1 && x <= col) // 判断输入的坐标是否在9行9列的范围内
		{
			select();
			operation(mine, show, y, x, &first, &win);
		}
		else
			printf("坐标非法,请重新输入\n");
	}
	if (win == row * col - EASY_COUNT)
		printf("你赢了\n");
	else
	{
		display(mine, ROW, COL);
		printf("很遗憾,你被炸死了\n");
	}
}

void spread(char mine[ROWS][COLS], char show[ROWS][COLS], int y, int x, int* win)
{
	if (show[y][x] == '0')       // 若周围没有雷才进入
	{
		for (int i = -1; i <= 1; i++) // 行
		{
			for (int j = -1; j <= 1; j++) // 列
			{
				if (y + i >= 1 && y + i <= ROW && x + j >= 1 && x + j <= COL) // 防止超出9×9的范围
				{
					if (show[y + i][x + j] == '*')        // 防止反复递归同一个坐标
					{
						int count = mineCount(mine, y + i, x + j);
						show[y + i][x + j] = count + '0'; // 将已经递归过的坐标显示它周围雷的数量,防止反复递归同一个坐标
						(*win)++;     // 增加查找非雷坐标的数量
						spread(mine, show, y + i, x + j, win); // 进入下一次递归
					}
				}
			}
		}
	}
}

void numberSpread(char mine[ROWS][COLS], char show[ROWS][COLS], int y, int x, int num, int* win)
{
	int unknown = 0;     // 未知坐标的数量
	int unknownBlank = 0;// 未知坐标且未插旗的数量
	int FCount = 0;		 // 插旗坐标的数量
	// 记录周围三者的数量
	for (int i = -1; i <= 1; i++)
	{
		for (int j = -1; j <= 1; j++)
		{
			if (show[y + i][x + j] == '*' || show[y + i][x + j] == 'F')
				unknown++;
			if (show[y + i][x + j] == 'F')
				FCount++;
			if (show[y + i][x + j] == '*')
				unknownBlank++;
		}
	}

	if (unknown > num) // 周围未知的坐标的数量要大于周围雷的数量
	{
		if (FCount >= num) // 周围旗子的数量要大于等与周围雷的数量
		{
			if (unknownBlank != 0) // 周围未知坐标且未插旗的数量不能为零
			{
				for (int i = -1; i <= 1; i++) // 行
				{
					for (int j = -1; j <= 1; j++) // 列
					{
						// 防止进入边框坐标
						if (y + i >= 1 && y + i <= ROW && x + j >= 1 && x + j <= COL)
						{
							// 数字展开时扫到雷
							if (mine[y + i][x + j] == '1' && show[y + i][x + j] == '*')
							{
								(*win) = -9;
							}
							// 数字展开扫到非雷
							else if (show[y + i][x + j] == '*' && mine[y + i][x + j] == '0')
							{
								int count = mineCount(mine, y + i, x + j);
								show[y + i][x + j] = count + '0';
								(*win)++;
								spread(mine, show, y + i, x + j, win);
							}
						}
					}
				}
				if (*win > 0)
					display(show, ROW, COL);
			}
			else
				printf("周围没有未知且未插旗的坐标,不能展开\n");
		}
		else
			printf("周围旗子数量小于周围雷的数量\n");
	}
	else
		printf("未知坐标数量不大于周围雷的数量\n");
}

        在 test.c源文件 中:

#include "game.h"

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

void game()
{
	char mine[ROWS][COLS] = { 0 };
	char show[ROWS][COLS] = { 0 };
	initBoard(mine, ROWS, COLS, '0');
	initBoard(show, ROWS, COLS, '*');
	display(show, ROW, COL);
	//display(mine, ROW, COL);
	findMine(mine, show, ROW, COL);
}

void test()
{
	int input = 0;
	srand((unsigned int)time(NULL)); // 使rand函数产生伪随机数
	do
	{
		menu();
		printf("请输入操作:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			game();
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("输入错误,请重新输入\n");
			break;
		}
	} while (input);
}

int main()
{
	test();
	return 0;
}

        

         (Bug提醒):

        setMine函数 中有一处错误:

        对于行与列相同时此问题没有影响,但是接下来实现的代码行与列会变动,不改会出现Bug.

        

        四.动态扫雷的内容实现

        这一部分也会改动前面的代码,需要分开讲述,并且以其主要功能的角度讲解,内容为:1.空间的申请与释放2.布雷准备3.自定义行与列的实现、4.整体的代码改动

        1.空间的申请与释放

        实现行与列的变动需要 malloc函数 ,头文件为 stdlib.h :

        在 game.h头文件 中:

        

        在 game.c源文件 中:

        2.布雷准备

        这里只需布雷函数:

        在 game.h头文件 中:

        

        在 game.c源文件 中:

                ​​​​​​​                

        3.自定义行与列的实现

        这里要实现行与列由玩家自定义,则需要两个函数并提前声明输入界限:

        在 game.h头文件 中:

        ​​​​​​​        

        在 game.c源文件 中:

        4.整体的代码改动

        整体的代码大部分都要改动:

        在 game.h头文件 中:

        在 test.c源文件 中:

        在 game.c源文件 中:

        上半:

下半:

        5.动态扫雷源代码

        在 game.h头文件 中:

#pragma once

#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <time.h> // time 函数需要的头文件
#include <stdlib.h> // rand、 srand、malloc 函数需要的头文件 

void initBoard(char** arr, int rows, int cols, char set);

void display(char** arr, int row, int col);

void setMine(char** mine, int row, int col, int y, int x);

void findMine(char** mine, char** show, int row, int col);

void spread(char** mine, char** show, int y, int x, int* win, int row, int col);

void numberSpread(char** mine, char** show, int y, int x, int num, int* win, int row, int col);

// 动态扫雷实现
char** apply(int* rows, int* cols);

void release(char** arr, int rows, int cols);

int getMine();

void myApply(int* rows, int* cols);

int myGetMine();

        在 game.c源文件 中:

#include "game.h"

extern int getMineCount;

void initBoard(char** arr, int rows, int cols, char set)
{
	for (int i = 0; i < rows; i++)
		for (int j = 0; j < cols; j++)
			arr[i][j] = set;
}

void display(char** arr, int row, int col)
{
	int count = 0;
	system("cls");
	int i, j;
	if (row <= 9 && col <= 9)
	{
		for (i = 0; i <= col / 2 - 1; i++)
			printf("--");
		printf("扫雷");
		for (i = 0; i <= col / 2 - 1; i++)
			printf("--");
		printf("\n|");

		for (i = 0; i <= col; i++) // 打印列的数量
			printf("%d|", i);
		printf("\n"); // 换行

		for (i = 1; i <= row; i++)
		{
			printf("|%d|", i); // 打印当前的行

			for (j = 1; j <= col; j++) // 打印扫雷内容
			{
				if (arr[i][j] == '*')
					printf("%c ", arr[i][j]);
				if (arr[i][j] == '0')
					printf("\033[30m%c\033[0m ", arr[i][j]); // 黑色
				if (arr[i][j] == '1')
					printf("\033[36m%c\033[0m ", arr[i][j]); // 浅蓝色
				if (arr[i][j] == '2')
					printf("\033[34m%c\033[0m ", arr[i][j]); // 蓝色
				if (arr[i][j] == '3')
					printf("\033[33m%c\033[0m ", arr[i][j]); // 黄色
				if (arr[i][j] == '4')
					printf("\033[31m%c\033[0m ", arr[i][j]); // 红色
				if (arr[i][j] == '5')
					printf("\033[35m%c\033[0m ", arr[i][j]); // 紫色
				if (arr[i][j] == '6')
					printf("%c ", arr[i][j]);
				if (arr[i][j] == '7')
					printf("%c ", arr[i][j]);
				if (arr[i][j] == 'F')
				{
					printf("\033[32m%c\033[0m ", arr[i][j]); // 绿色
					count++;
				}
			}
			printf("\n"); // 换行
		}

		for (i = 0; i <= col / 2 - 1; i++)
			printf("--");
		printf("雷:\033[31m%d\033[0m", getMineCount - count);
		for (i = 0; i <= col / 2 - 1; i++)
			printf("--");
		printf("\n");

	}
	else
	{
		for (i = 0; i <= col / 2 - 1; i++)
			printf("---");
		printf("扫雷");
		for (i = 0; i <= col / 2 - 1; i++)
			printf("---");
		printf("\n|");

		for (i = 0; i <= col; i++) // 打印列的数量
			printf("%2d|", i);
		printf("\n"); // 换行

		for (i = 1; i <= row; i++)
		{
			printf("|%2d|", i); // 打印当前的行

			for (j = 1; j <= col; j++) // 打印扫雷内容
			{
				if (arr[i][j] == '*')
					printf("%2c ", arr[i][j]);
				if (arr[i][j] == '0')
					printf("\033[30m%2c\033[0m ", arr[i][j]); // 黑色
				if (arr[i][j] == '1')
					printf("\033[36m%2c\033[0m ", arr[i][j]); // 浅蓝色
				if (arr[i][j] == '2')
					printf("\033[34m%2c\033[0m ", arr[i][j]); // 蓝色
				if (arr[i][j] == '3')
					printf("\033[33m%2c\033[0m ", arr[i][j]); // 黄色
				if (arr[i][j] == '4')
					printf("\033[31m%2c\033[0m ", arr[i][j]); // 红色
				if (arr[i][j] == '5')
					printf("\033[35m%2c\033[0m ", arr[i][j]); // 紫色
				if (arr[i][j] == '6')
					printf("%2c ", arr[i][j]);
				if (arr[i][j] == '7')
					printf("%2c ", arr[i][j]);
				if (arr[i][j] == 'F')
				{
					printf("\033[32m%2c\033[0m ", arr[i][j]); // 绿色
					count++;
				}
			}
			printf("\n"); // 换行
		}

		for (i = 0; i <= col / 2 - 1; i++)
			printf("---");
		printf("雷:\033[31m%d\033[0m", getMineCount - count);
		for (i = 0; i <= col / 2 - 1; i++)
			printf("---");
		printf("\n");
	}
}

void setMine(char** mine, int row, int col, int y, int x)
{
	int count = 0;
	int i, j;
	// 记录输入行和旁边的两行
	int judgeRow[3] = { 0 };
	for (int k = -1, a = 0; k <= 1; k++, a++)
		judgeRow[a] = k + y;

	// 记录输入列和旁边的两列
	int judgeCol[3] = { 0 };
	for (int k = -1, a = 0; k <= 1; k++, a++)
		judgeCol[a] = k + x;

	while (count < getMineCount) // 布置雷的数量,数量到达时则跳出循环
	{
		i = rand() % row + 1; // 行

		// 当输入行且旁边两行与布置行不同时
		if (i != judgeRow[0] && i != judgeRow[1] && i != judgeRow[2])
			j = rand() % col + 1; // 列

		// 当输入行或旁边两行与布置行相同时
		else
		{
			do
			{
				j = rand() % col + 1;
				// 若输入列或旁边两列与布置列相同则进入循环
			} while (j == judgeCol[0] || j == judgeCol[1] || j == judgeCol[2]);
		}
		if (mine[i][j] == '0') // 若为 '0',则放置雷
		{
			mine[i][j] = '1';
			count++;
		}
	}
}

int mineCount(char** mine, int y, int x)
{
	int count = 0;
	for (int i = -1; i <= 1; i++)
		for (int j = -1; j <= 1; j++)
			if (mine[y + i][x + j] == '1')
				count++;

	return count;
}

void select(int n)
{
	if (n == 1)
	{
		printf("************************************************\n");
		printf("**** 0.返回 1.查找 2.插旗 3.取旗 4.数字展开 ****\n");
		printf("************************************************\n");
	}
	else if (n == 2)
	{
		printf("**********************\n");
		printf("******  你赢了  ******\n");
		printf("**********************\n");
	}
	else if (n == 3)
	{
		printf("**********************\n");
		printf("**很遗憾,你被炸死了**\n");
		printf("**********************\n");
	}
}

void operation(char** mine, char** show, int y, int x, int* first, int* win, int row, int col)
{
	int wantDo = 0;
	do
	{
		printf("请输入操作:>");
		scanf("%d", &wantDo);
		switch (wantDo)
		{
		case 1:
			if (show[y][x] == '*')                    // 判断输入的坐标是否被占用
			{
				if (*first)
				{
					setMine(mine, row, col, y, x);
					*first = 0;
				}
				if (mine[y][x] == '1')                // 判断输入的坐标是否是雷
				{
					*win = -9;
				}
				else
				{
					int count = mineCount(mine, y, x);
					show[y][x] = count + '0';
					(*win)++;
					spread(mine, show, y, x, win, row, col);
					display(show, row, col);
				}
				wantDo = 0;
			}
			else
				printf("坐标被占用,请重新输入\n");
			break;

		case 2:
			if (show[y][x] == '*')
			{
				show[y][x] = 'F';
				display(show, row, col);
				wantDo = 0;
			}
			else
				printf("非未知坐标,请重新输入\n");
			break;

		case 3:
			if (show[y][x] == 'F')
			{
				show[y][x] = '*';
				display(show, row, col);
				wantDo = 0;
			}
			else
				printf("非插旗坐标,请重新输入\n");
			break;

		case 4:
			if (show[y][x] >= '1' && show[y][x] <= '7')
			{
				numberSpread(mine, show, y, x, show[y][x] - '0', win, row, col);
				wantDo = 0;
			}
			else
				printf("非有效坐标,请重新选择\n");
			break;

		case 0:
			break;

		default:
			printf("输入错误,请重新输入\n");
			break;
		}
	} while (wantDo);
}

void findMine(char** mine, char** show, int row, int col)
{
	int y = 0;
	int x = 0;
	int win = 0;
	int first = 1;
	while (win < row * col - getMineCount && win >= 0) // 当win为负数意思为被雷炸死
	{
		printf("请输入坐标:>");
		scanf("%d %d", &y, &x);
		if (y >= 1 && y <= row && x >= 1 && x <= col) // 判断输入的坐标是否在9行9列的范围内
		{
			select(1);
			operation(mine, show, y, x, &first, &win, row, col);
		}
		else
			printf("坐标非法,请重新输入\n");
	}
	if (win == row * col - getMineCount)
		select(2);
	else
	{
		display(mine, row, col);
		select(3);
	}
}

void spread(char** mine, char** show, int y, int x, int* win, int row, int col)
{
	if (show[y][x] == '0')       // 若周围没有雷才进入
	{
		for (int i = -1; i <= 1; i++) // 行
		{
			for (int j = -1; j <= 1; j++) // 列
			{
				if (y + i >= 1 && y + i <= row && x + j >= 1 && x + j <= col) // 防止超出9×9的范围
				{
					if (show[y + i][x + j] == '*')        // 防止反复递归同一个坐标
					{
						int count = mineCount(mine, y + i, x + j);
						show[y + i][x + j] = count + '0'; // 将已经递归过的坐标显示它周围雷的数量,防止反复递归同一个坐标
						(*win)++;     // 增加查找非雷坐标的数量
						spread(mine, show, y + i, x + j, win, row, col); // 进入下一次递归
					}
				}
			}
		}
	}
}

void numberSpread(char** mine, char** show, int y, int x, int num, int* win, int row, int col)
{
	int unknown = 0;     // 未知坐标的数量
	int unknownBlank = 0;// 未知坐标且未插旗的数量
	int FCount = 0;		 // 插旗坐标的数量
	// 记录周围三者的数量
	for (int i = -1; i <= 1; i++)
	{
		for (int j = -1; j <= 1; j++)
		{
			if (show[y + i][x + j] == '*' || show[y + i][x + j] == 'F')
				unknown++;
			if (show[y + i][x + j] == 'F')
				FCount++;
			if (show[y + i][x + j] == '*')
				unknownBlank++;
		}
	}

	if (unknown > num) // 周围未知的坐标的数量要大于周围雷的数量
	{
		if (FCount >= num) // 周围旗子的数量要大于等与周围雷的数量
		{
			if (unknownBlank != 0) // 周围未知坐标且未插旗的数量不能为零
			{
				for (int i = -1; i <= 1; i++) // 行
				{
					for (int j = -1; j <= 1; j++) // 列
					{
						// 防止进入边框坐标
						if (y + i >= 1 && y + i <= row && x + j >= 1 && x + j <= col)
						{
							// 数字展开时扫到雷
							if (show[y + i][x + j] == '*' && mine[y + i][x + j] == '1')
							{
								(*win) = -9;
							}
							// 数字展开扫到非雷
							else if (show[y + i][x + j] == '*' && mine[y + i][x + j] == '0')
							{
								int count = mineCount(mine, y + i, x + j);
								show[y + i][x + j] = count + '0';
								(*win)++;
								spread(mine, show, y + i, x + j, win, row, col);
							}
						}
					}
				}
				if (*win > 0)
					display(show, row, col);
			}
			else
				printf("周围没有未知且未插旗的坐标,不能展开\n");
		}
		else
			printf("周围旗子数量小于周围雷的数量\n");
	}
	else
		printf("未知坐标数量不大于周围雷的数量\n");
}

char** apply(int* rows, int* cols)
{
	// 申请二级指针(二维数组)行的数量
	char** arr = (char**)malloc(sizeof(char*) * (*rows));
	if (NULL == arr)
	{
		printf("游戏异常,已退出\n");
		exit(-1);
	}

	// 申请每行之中列的数量
	for (int i = 0; i < *rows; i++)
	{
		arr[i] = (char*)malloc(sizeof(char) * (*cols));
		if (NULL == arr[i])
		{
			printf("游戏异常,已退出\n");
			exit(-1);
		}
	}

	// 返回首元素地址
	return arr;
}

void release(char** arr, int rows, int cols)
{
	// 先释放一级指针空间
	for (int i = 0; i < rows; i++)
		free(arr[i]);
	// 后释放二级指针的空间
	free(arr);
}

int getMine(int num)
{
	switch (num)
	{
	case 1:
		return 10;
	case 2:
		return 40;
	case 3:
		return 99;
	case 4:
		return myGetMine();
	}
	return 0;
}

void myApply(int* rows, int* cols)
{
	printf("***********************************************\n");
	printf("****************** 提前声明:******************\n");
	printf("*自定义行和列建议至少全部都大于3,否者后果自负*\n");
	printf("***********************************************\n");
	printf("请输入行数:>");
	scanf("%d", rows);
	if (*rows < 1)
	{
		*rows = 4;
		printf("行数小于1,自动改成4行\n");
	}
	printf("请输入列数:>");
	scanf("%d", cols);
	if (*cols < 1)
	{
		*cols = 4;
		printf("列数小于1,自动改成4列\n");
	}
	(*rows) = (*rows) + 2;
	(*cols) = (*cols) + 2;
}

int myGetMine()
{
	int n = 0;
	printf("***********************************************\n");
	printf("****************** 提前声明:******************\n");
	printf("******自定义雷的数量建议最多:行 × 列 - 9 *****\n");
	printf("*****************否者后果自负******************\n");
	printf("请布置雷的数量:>");
	scanf("%d", &n);
	if (n < 0)
	{
		printf("数量小于1,改为0");
		n = 0;
	}
	return n;
}

        在 test.c源文件 中:

#include "game.h"

int getMineCount = 0;

void menu(int n)
{
	if (n == 1)
	{
		printf("************************\n");
		printf("**** 1.play  0.exit ****\n");
		printf("************************\n");
	}
	else if (n == 2)
	{
		printf("*************************\n");
		printf("****   1.初级(9×9)   ****\n");
		printf("****   2.中级(16×16) ****\n");
		printf("****   3.高级(16×30) ****\n");
		printf("****   4.自定义      ****\n");
		printf("*************************\n");
	}
}

int option(int* rows, int* cols)
{
	menu(2);
	int choose = 0;
	do
	{
		printf("请选择难度:>");
		scanf("%d", &choose);
		switch (choose)
		{
		case 1:
			*rows = 11;
			*cols = 11;
			return 1;
		case 2:
			*rows = 18;
			*cols = 18;
			return 2;
		case 3:
			*rows = 18;
			*cols = 32;
			return 3;
		case 4:
			myApply(rows, cols);
			return 4;
		default:
			printf("输入错误,请重新输入\n");
			break;
		}
	} while (choose);
	return 1;
}

void game()
{
	int rows = 0;
	int cols = 0;
	int num = option(&rows, &cols);
	char** mine = apply(&rows, &cols);
	char** show = apply(&rows, &cols);
	getMineCount = getMine(num);
	int row = rows - 2;
	int col = cols - 2;
	initBoard(mine, rows, cols, '0');
	initBoard(show, rows, cols, '*');
	display(show, row, col);
	findMine(mine, show, row, col);
	release(mine, rows, cols);
	release(show, rows, cols);
}

void test()
{
	int input = 0;
	srand((unsigned int)time(NULL)); // 使rand函数产生伪随机数
	do
	{
		menu(1);
		printf("请输入操作:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			game();
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("输入错误,请重新输入\n");
			break;
		}
	} while (input);
}

int main()
{
	test();
	return 0;
}

        结语:

        以上是在VS2022中做扫雷小游戏的经验,若有错误等其他问题请大家在评论区指出或私信。

        谢谢阅读

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值