【C语言项目】扫雷-鼠标版

前言

  你的「扫雷」💣 还不能用「鼠标」玩❓ 还在输入「坐标」❓ 已经都 「2021」了 ❗️ 谁还会玩你的「扫雷」呢❓
   ❤️「看过来」❤️,本文将 手把手 💪带你写出「新的扫雷」😎

提示:该项目基于C语言编写,由 VisualStudio 2019 所实现



注:游戏中所有操作均通过鼠标实现

一、游戏效果展示:

下图以三倍速展示

扫雷3倍速


二、界面函数

  若想实现控制台的基本操作,需先了解以下内容

注:下列函数,均须引用头文件 <Windows.h>

2.0 句柄

  要想对界面进行一系列的「操作」,则离不开「句柄」这一重要概念。「句柄」是Windows最常用的概念。它通常用来标识Windows资源(如菜单、图标、窗口等)和设备等对象。虽然可以把句柄理解为是一个指针变量类型,但它不是对象所在的地址指针,而是作为Windows系统内部表的索引值来使用的。

其声明为:

 typedef void *HANDLE;

从上面可以看出,句柄「 HANDLE 」是一个无类型指针。

参考代码:

 HANDLE out_put = NULL;

点此深入了解👉深入了解Windows句柄到底是什么

2.0.1 GetStdHandle() 函数

函数结构:

HANDLE WINAPI GetStdHandle(_In_ DWORD nStdHandle);
功能:获取指定标准设备的句柄(标准输入,标准输出或标准错误)
参数:nStdHandle标准设备。此参数可以是以下值之一。
STD_INPUT_HANDLE(DWORD)-10 标准输入设备。
STD_OUTPUT_HANDLE(DWORD)-11 标准输出设备。
STD_ERROR_HANDLE(DWORD)-12 标准错误设备。
返回值不同情况下,返回值有以下三种情况
指定设备的句柄函数成功
INVALID_HANDLE_VALUE函数失败
NULL无相关句柄

注意:
该函数仅有上述三种参数。默认情况下,标准输出句柄和标准错误句柄都是对应的屏幕(显示器)

参考代码:

//定义句柄
HANDLE out_put;
//获取标准输出句柄
out_put = GetStdHandle(STD_OUTPUT_HANDLE);

2.1 控制台

2.1.1 COORD结构体

  若要进行相关窗口操作,则必须先了解 「COORD」结构,结构如下:

typedef struct _COORD {
  SHORT X;
  SHORT Y;
} COORD, *PCOORD;

说明:

X水平坐标或列值
Y垂直坐标或行值

注:单位取决于函数调用

2.1.2 SMALL_RECT 结构

结构如下:

typedef struct _SMALL_RECT {
  SHORT Left;
  SHORT Top;
  SHORT Right;
  SHORT Bottom;
} SMALL_RECT;

结构说明:

功能定义矩形的左上角和右下角的坐标
Left矩形左上角的x坐标
Top矩形左上角的y坐标
Right矩形右下角的x坐标
Bottom矩形右下角的y坐标

2.1.3 SetConsoleTitle() 函数

函数结构:

BOOL WINAPI SetConsoleTitle(_In_ LPCTSTR lpConsoleTitle);
功能设置当前控制台窗口标题
参数:lpConsoleTitle要在控制台窗口的标题栏中显示的字符串
返回值如果函数成功,则返回值为非零值。反之为0

参考代码:

SetConsoleTitle("扫雷");

注意:在实际使用过程中,出现了控制台乱码的情况
解决方法:右击 当前解决方案👉 属性 👉 配置属性 👉 高级 👉 字符集 👉 使用更多字符集

点击了解更多👉官方参考网址

2.1.4 SetConsoleWindowInfo() 函数

函数结构:

BOOL WINAPI SetConsoleWindowInfo(
  _In_       HANDLE     hConsoleOutput,
  _In_       BOOL       bAbsolute,
  _In_ const SMALL_RECT *lpConsoleWindow
);
功能设置控制台屏幕缓冲区窗口的当前大小和位置
参数参数含义
hConsoleOutput可理解为标准输出句柄
bAbsolute如果此参数为TRUE,则坐标指定窗口的新左上角和右下角。如果为FALSE,则坐标相对于当前窗口角坐标
lpConsoleWindow指向SMALL_RECT结构的指针,该结构指定窗口的新左上角和右下角
返回值如果函数成功,则返回值为非零值。反之为0

点击了解更多👉官方参考网址

2.2 隐藏光标

  扫雷过程中,为使界面更加美观,隐藏光标是必须的。在使用相关函数前,让我们先了解下列结构

2.2.1 CONSOLE_CURSOR_INFO 结构

结构说明:

typedef struct _CONSOLE_CURSOR_INFO {
  DWORD dwSize;
  BOOL  bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
参数参数含义
dwSize光标填充的字符单元格的百分比。通常该值介于1和100之间。光标外观会发生变化,从完全填充单元格到显示为单元格底部的水平线。
bVisible该参数为bool 类型,表示光标的可见性。如果光标可见,则此成员为TRUE,反之为 FALSE

2.2.2 GetConsoleCursorInfo() 函数

函数结构:

BOOL WINAPI GetConsoleCursorInfo(
  _In_  HANDLE               hConsoleOutput,
  _Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

函数解释:

功能:获取光标相关信息
参数:参数解释
hConsoleOutput控制台屏幕缓冲区的句柄,且该句柄必须具有GENERIC_READ访问权限。
lpConsoleCursorInfo指向CONSOLE_CURSOR_INFO结构的指针,该结构接收有关控制台光标的信息。
返回值成功返回非0,反之为0

参考代码:

//定义句柄
HANDLE out_put;
//获取标准输出句柄
  //注意:获取的为标准输出句柄
out_put = GetStdHandle(STD_OUTPUT_HANDLE);
//创建光标信息
CONSOLE_CURSOR_INFO cursor;
//隐藏光标
cursor.bVisible = false;
cursor.dwSize = 1;
SetConsoleCursorInfo(out_put, &cursor);

点击了解更多👉官方参考网址

2.3 光标跳转

  若想指定文本输出位置,可以通过函数 GetConsoleScreenBufferInfo()FillConsoleOutputCharacter() 实现指定位置填充,但上述函数结构复杂,因此本文通过「 光标跳转」来实现。

2.3.1 SetConsoleCursorPosition() 函数

函数结构:

BOOL WINAPI SetConsoleCursorPosition(
  _In_ HANDLE hConsoleOutput,
  _In_ COORD  dwCursorPosition
);

函数解释:

功能设置指定控制台屏幕缓冲区中的光标位置
参数参数解释
hConsoleOutput控制台屏幕缓冲区的句柄,标准输出句柄即可
dwCursorPositionCOORD 结构,用于指定新的光标位置(以字符为单位)
返回值如果函数成功,则返回值为非零值。

参考代码:

//定义句柄
HANDLE out_put;
//获取标准输出句柄
out_put = GetStdHandle(STD_OUTPUT_HANDLE);
//用于存储鼠标当前坐标
COORD pos = { 5,5 };
SetConsoleCursorPosition(out_put, pos);
printf("当前鼠标位置X:%d Y:%d",pos.X,pos.Y);
pos.X = 10;
pos.Y = 10;
SetConsoleCursorPosition(out_put, pos);
printf("当前鼠标位置X:%d Y:%d",pos.X,pos.Y);

结果如下:
在这里插入图片描述
点击了解更多👉官方参考网址

2.4 文本颜色函数

  若要实现前文游戏界面,用不同颜色区分「 边界 」「 雷区 」,则需使用 相关函数来实现。

2.4.1 SetConsoleTextAttribute() 函数

函数结构:

BOOL WINAPI SetConsoleTextAttribute(HANDLE hConsoleOutput, WORD wAttributes);

函数解释:

功能设置控制台文本属性(颜色),可以设置前景色FOREGROUND(文本颜色)和背景色BACKGROUND
参数参数解释
hConsoleOutput控制台屏幕缓冲区的句柄。此处获取标准输出句柄即可
wAttributes字符属性
返回值如果函数成功,则返回值为非零值。

点击了解更多👉官方参考网址

2.4.2 字符属性

  字符属性可以分为两类:颜色和DBCS

字符属性含义
FOREGROUND_BLUE文字颜色包含蓝色
FOREGROUND_GREEN文字颜色包含绿色
FOREGROUND_RED文字颜色包含红色
FOREGROUND_INTENSITY文字颜色加强
BACKGROUND_BLUE背景颜色包含蓝色
BACKGROUND_GREEN背景颜色包含绿色
BACKGROUND_RED背景颜色包含红色
BACKGROUND_INTENSITY背景颜色加剧
COMMON_LVB_LEADING_BYTE前导字节
COMMON_LVB_TRAILING_BYTE尾随字节
COMMON_LVB_GRID_HORIZONTAL顶部水平
COMMON_LVB_GRID_LVERTICAL左垂直
COMMON_LVB_GRID_RVERTICAL正确的垂直
COMMON_LVB_REVERSE_VIDEO反转前景和背景属性
COMMON_LVB_UNDERSCORE下划线

2.4.3 颜色对照表

  除上述以外,还有以下颜色可供选择

十进制颜色对照表
十六进制颜色对照表

2.4.4 参考代码

	//定义句柄
	HANDLE out_put, in_put;
	//获取标准输出句柄
	out_put = GetStdHandle(STD_OUTPUT_HANDLE);

	for (int i = 0; i < 7; i++)
	{
		SetConsoleTextAttribute(out_put, 144 + 15 * i);
		printf("第%d次打印\n", i);
	}
	SetConsoleTextAttribute(out_put, FOREGROUND_INTENSITY);
	SetConsoleTextAttribute(out_put, FOREGROUND_BLUE);
	printf("第7次打印\n");
	SetConsoleTextAttribute(out_put, BACKGROUND_GREEN);
	printf("第8次打印\n");

打印结果如下:
打印结果

由此可见, SetConsoleTextAttribute() 函数的使用仅此而已。

三、鼠标事件

  仅仅掌握上述函数,只能美化你的界面,未能达到 「 鼠标操作 」 的目的。因此,还需学习以下内容

3.1 INPUT_RECORD结构

功能:描述控制台输入缓冲区中的输入事件

结构说明:

typedef struct _INPUT_RECORD {
  WORD  EventType;
  union {
    KEY_EVENT_RECORD          KeyEvent;
    MOUSE_EVENT_RECORD        MouseEvent;
    WINDOW_BUFFER_SIZE_RECORD WindowBufferSizeEvent;
    MENU_EVENT_RECORD         MenuEvent;
    FOCUS_EVENT_RECORD        FocusEvent;
  } Event;
} INPUT_RECORD;

结构解释:

EventType输入事件类型的句柄和存储在Event成员中的事件记录。
含义
FOCUS_EVENT该事件成员包含一个FOCUS_EVENT_RECORD结构
KEY_EVENT该事件成员包含一个KEY_EVENT_RECORD结构有关键盘事件的信息
MENU_EVENT该事件成员包含一个MENU_EVENT_RECORD结构
MOUSE_EVENT所述事件构件包含MOUSE_EVENT_RECORD结构用约鼠标移动或按键按压事件的信息
WINDOW_BUFFER_SIZE_EVENT该事件成员包含一个WINDOW_BUFFER_SIZE_RECORD结构有关控制台屏幕缓冲区的新大小信息

3.2 MOUSE_EVENT_RECORD 结构

由于本文仅用鼠标操作,故仅介绍该结构

结构说明:

typedef struct _MOUSE_EVENT_RECORD {
  COORD dwMousePosition;
  DWORD dwButtonState;
  DWORD dwControlKeyState;
  DWORD dwEventFlags;
} MOUSE_EVENT_RECORD;

结构解释:

参数参数含义
dwMousePositionCOORD 结构,用来记录光标位置
dwButtonState鼠标按键的状态
含义
FROM_LEFT_1ST_BUTTON_PRESSED鼠标左键
RIGHTMOST_BUTTON_PRESSED鼠标右键
FROM_LEFT_2ND_BUTTON_PRESSED鼠标滚轮
FROM_LEFT_3RD_BUTTON_PRESSED鼠标左起第三个按键(前进键)
FROM_LEFT_4TH_BUTTON_PRESSED鼠标左起第四个按键(后退键)
dwControlKeyState控制键状态(因本文用不到,故不在此展开)
wEventFlags鼠标事件类型
含义
DOUBLE_CLICK双击的第二次单击发生,第一次单击作为常规按钮事件返回
MOUSE_MOVED鼠标位置发生变化

注:上述仅提供了本项目可能用到的值。

3.3 ReadConsoleInput() 函数

函数结构:

BOOL WINAPI ReadConsoleInput(
  _In_  HANDLE        hConsoleInput,
  _Out_ PINPUT_RECORD lpBuffer,
  _In_  DWORD         nLength,
  _Out_ LPDWORD       lpNumberOfEventsRead
);

结构说明:

功能从缓冲区读取数据并删除
参数含义
hConsoleInput标准输入句柄
lpBuffer指向INPUT_RECORD 结构的指针
nLengthlpBuffer参数指向的数组的大小,以数组元素为单位。
lpNumberOfEventsRead指向LPDWORD 结构的指针,该结构用来存储读取记录

点击了解更多👉官方参考网址

3.4 参考示例

	//定义句柄
	HANDLE in_put;
	//获取标准输入句柄
	in_put = GetStdHandle(STD_INPUT_HANDLE);
	//用于存储鼠标当前坐标
	COORD pos = { 0,0 };
	//定义输入事件结构体
	INPUT_RECORD mouse_record;
	//用于存储读取记录
	DWORD res;
	//Game();

	while (1)
	{
		//读取输入事件
		ReadConsoleInput(in_put, &mouse_record, 1, &res);
		
		if (mouse_record.EventType == MOUSE_EVENT)
		{
			//单击鼠标右键
			if (mouse_record.Event.MouseEvent.dwButtonState == RIGHTMOST_BUTTON_PRESSED )
				printf("单击右键\n");
			//双击
			if (mouse_record.Event.MouseEvent.dwEventFlags == DOUBLE_CLICK)
			{
				printf("双击\n");
				break;
			}
			//单击鼠标左键
			else if (mouse_record.Event.MouseEvent.dwButtonState == FROM_LEFT_1ST_BUTTON_PRESSED)
				printf("单击左键\n");
		}	
	}
	
	CloseHandle(in_put);

注:事实上,运行起来并不能直接实现鼠标操作,而需要 「 二次运行 」方可。即,出现图一后,需将光标置于文档中,再次按下 Ctrl + F5,出现图二「 cmd.exe 控制台 」之后进行如下设置,重新打开「 cmd.exe 控制台 」即可

四、游戏代码剖析

框架构建

  首先我们需预设「 窗口大小 」「 雷场大小、坐标」「 雷的个数 」,为方便后期重新设置、增加代码可读性,在此我们进行宏定义

//设置雷区行数
#define ROW 10
//设置雷区列数
#define COL 10
//设置雷的个数
#define NUM 10
//设置窗口大小
#define WIDTH 50
#define HEIGHT 25
//雷场起始坐标定义
COORD pos_field;

游戏主体函数

首先确定雷区起始坐标,为使界面美观,在此定义 Y 为窗口高的三分之一
创建一个二维数组,用来存储雷场信息,事实上,该二维数组大小创建为 [ROW + 2] [COL + 2]更有利于后期展开,在此为设置成如此,是为了后期鼠标坐标和对应数组坐标的一致性

void Game()
{
	//用于存储雷区起始坐标
	COORD pos_field = { (WIDTH - (2 + ROW) * 2) / 2 ,(HEIGHT - COL - 2) / 3 - 1 };
	pos_field.X = (pos_field.X % 2) == 0 ? pos_field.X : pos_field.X - 1;
	//定义雷区
	int arr[ROW][COL];
	int i;
	do
	{
		//system("CLS");
		//界面初始化
		InitiaInterface(pos_field);
		//布置雷
		PlaceMines(arr);
		//排雷
		MineClearance(arr,pos_field);
		//游戏结束,玩家选择
		i = ChoiceGet(pos_field) - 2;
	} while (i);

	//定义句柄
	HANDLE out_put, in_put;
	//获取标准输出、输入句柄
	out_put = GetStdHandle(STD_OUTPUT_HANDLE);
	in_put = GetStdHandle(STD_INPUT_HANDLE);
	//关闭句柄
	CloseHandle(out_put);
	CloseHandle(in_put);
}

界面初始化

  界面初始化,需要完成「 边界 」「 雷场 」「 提示区 」的打印,以及「 光标隐藏 」「 设置窗口大小 」等操作

值得注意的是:

1.在 cmd 窗口中一个「 方块或中文 」占两个单位的横坐标,一个单位的纵坐标
2.打印雷场时,光标应一次跳 2个单位
为使界面更加美观,应合理安排雷区起始位置,故提前设置好雷区起始坐标(非边界起始坐标)

void InitiaInterface(COORD pos_field)
{
	//定义句柄
	HANDLE out_put, in_put;
	//获取标准输出句柄
	out_put = GetStdHandle(STD_OUTPUT_HANDLE);
	//获取标准输入句柄
	in_put = GetStdHandle(STD_INPUT_HANDLE);
	//设置窗口标题
	SetConsoleTitle("扫雷");
	//用于存储鼠标当前坐标
	COORD pos = { 0,0 };
	//创建光标信息
	CONSOLE_CURSOR_INFO cursor;
	//隐藏光标
	cursor.bVisible = false;
	cursor.dwSize = 1;
	SetConsoleCursorInfo(out_put, &cursor);
	//设置控制台屏幕缓冲区窗口的当前大小和位置
	SMALL_RECT rect = { 0, 0, WIDTH , HEIGHT };
	SetConsoleWindowInfo(out_put, 1, &rect);

	//打印边界、雷场
	for (int j = 0; j < COL + 2; j++)
	{
		for (int i = 0; i < ROW + 2; i++)
		{
			//设置光标位置
			pos.X = pos_field.X - 2 + 2 * i;
			pos.Y = pos_field.Y + j - 1;
			SetConsoleCursorPosition(out_put, pos);
			//边界设置为绿色,雷区黄色,以便区分
			if (i == 0 || i == ROW + 1 || j == 0 || j == COL + 1)
				color(10);
			else
				color(14);
			printf("■");
		}
	}

	//设置分割线
	color(FOREGROUND_BLUE);
	pos.X = 0;
	pos.Y = pos_field.Y + COL + 2;
	SetConsoleCursorPosition(out_put, pos);
	for (int i = 0; i < WIDTH - 1; i++)
		printf("-");

	//设置左提示区
	color(BACKGROUND_GREEN);
	pos.X = (WIDTH / 2 - 5) / 2 ;
	pos.Y += 3;
	SetConsoleCursorPosition(out_put, pos);
	printf("提示");
	//提示信息
	color(FOREGROUND_GREEN);
	pos.X = (WIDTH / 2 - 13) / 2;
	pos.Y++;
	SetConsoleCursorPosition(out_put, pos);
	printf("左键单击选择");
	pos.Y++;
	SetConsoleCursorPosition(out_put, pos);
	printf("右击添加标记");
	pos.Y++;
	SetConsoleCursorPosition(out_put, pos);
	printf("双击取消标记");


	//设置右提示区
	//打印边界
	pos.X = WIDTH / 2;
	pos.Y = pos_field.Y + COL + 3;
	color(FOREGROUND_GREEN);
	SetConsoleCursorPosition(out_put, pos);
	for (int i = WIDTH / 2; i < WIDTH - 1; i++)
	{
		printf("-");
	}
	pos.Y++;
	for (; pos.Y < HEIGHT - 1; pos.Y++)
	{
		SetConsoleCursorPosition(out_put, pos);
		printf("|");
	}
	pos.X = WIDTH - 2;
	pos.Y = pos_field.Y + COL + 4;
	for (; pos.Y < HEIGHT - 2; pos.Y++)
	{
		SetConsoleCursorPosition(out_put, pos);
		printf("|");
	}
	pos.X = WIDTH / 2;
	SetConsoleCursorPosition(out_put, pos);
	for (int i = WIDTH / 2; i < WIDTH - 1; i++)
	{
		printf("-");
	}
}

为方便颜色设置,设置颜色函数

void color(int x)
{
	HANDLE out_put = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleTextAttribute(out_put, x);
}

布雷

  应避免因人为因素,雷的个数超出雷区的实际范围

void PlaceMines(int arr[ROW][COL])
{
	assert(NUM < ROW * COL);
	srand((unsigned int)time(NULL));
	int n = NUM;
	//非雷均为0
	for (int i = 0; i < ROW; i++)
	{
		for (int j = 0; j < COL; j++)
		{
			arr[i][j] = 0;
		}
	}
	//设置雷为1
	while (n)
	{
		int i = rand() % ROW;
		int j = rand() % COL;
		//为雷,跳过
		if (arr[i][j])
			continue;
		arr[i][j] = 1;
		n--;
	}
}

排雷

  实际上,排雷包括「 标记雷 」「 展开雷 」「 判断游戏情况 」「 提示 」等操作,因此先逐一介绍

鼠标坐标信息

  在不同的雷区设置下,雷场的坐标都会有相应的变化,因此将雷区坐标作为全局变量(参数)是很方便的选择。另外,由于一个方块在横坐标上占两个单位,且与数组坐标一一对应,故有以下写法。

		pos = mouse_record.Event.MouseEvent.dwMousePosition;
		pos.X = (pos.X % 2) == 0 ? pos.X : pos.X - 1;
		int x = (int)(pos.X - pos_field.X) / 2;
		int y = (int)pos.Y - pos_field.Y;

标记雷

  标记雷的操作很简单,只需要在原来坐标上,换颜色进行覆盖即可

void MarkMines(int arr[ROW][COL], COORD pos)
{
	HANDLE out_put = GetStdHandle(STD_OUTPUT_HANDLE);
	color(FOREGROUND_RED);
	SetConsoleCursorPosition(out_put, pos);
	printf("■");
}

注:所传坐标,为 方块 首坐标

展开雷

雷的展开有以下条件

1.展开位置不为雷
2.若周围有雷,显示雷的数目
3.周围8个均不为雷方可递归展开
4.展开位置另行标记

注:自身位置不可递归,否则死循环

void MinesSpread(int arr[ROW][COL], int x, int y, COORD pos)
{
	坐标不合法,直接返回
	if ((x < 0) || (y < 0) || (x >= ROW) || (y >= COL))
		return;

	HANDLE out_put = GetStdHandle(STD_OUTPUT_HANDLE);
	//统计个数
	int count = 0;
	for (int j = y - 1; j <= y + 1; j++)
	{
		if (j < 0 || j >= COL)
			continue;
		for (int i = x - 1; i <= x + 1; i++)
		{
			if (i < 0 || i >= ROW || (arr[i][j] == -1))  
				continue;
			count += arr[i][j];
		}
	}
	//有雷则打印 
	if (count)
	{
		color(FOREGROUND_RED);
		SetConsoleCursorPosition(out_put, pos);
		printf("%2d", count);
	}
	//无雷,递归
	else
	{
		//置为空方块
		color(FOREGROUND_RED);
		SetConsoleCursorPosition(out_put, pos);
		printf("□");
		//递归
		for (int j = y - 1; j <= y + 1; j++)
		{
			if (j < 0 || j >= COL)
				continue;
			for (int i = x - 1; i <= x + 1; i++)
			{
				if (i < 0 || i >= ROW)
					continue;
					//注意(i != x || j != y)中 ||,方才表示非 原坐标
				if ((i != x || j != y) && (arr[i][j] != 1) && (arr[i][j] != -1)) 
				{
					COORD ps = { pos.X + 2 * (i - x),pos.Y + j - y };
					//置为-1,表示该处已被点击,且不为雷,为后期游戏进度判断所用
					arr[i][j] = -1;
					MinesSpread(arr, i, j, ps);
				}
			}
		}
	}
	return;
}

排雷完整代码

  上述代码,仅为部分情况。实际排雷,还会出现「踩雷 」「 扫完雷 」「 取消标记 」等情况。理当有相应的提示信息

void MineClearance(int arr[ROW][COL], COORD pos_field)
{
	//定义句柄
	HANDLE out_put, in_put;
	//获取标准输出句柄
	out_put = GetStdHandle(STD_OUTPUT_HANDLE);
	//获取标准输入句柄
	in_put = GetStdHandle(STD_INPUT_HANDLE);
	//用于存储鼠标当前坐标
	COORD pos = { 0,0 };
	//定义输入事件结构体
	INPUT_RECORD mouse_record;
	//用于存储读取记录
	DWORD res;
	//设置光标位置
	pos.X = pos_field.X + ROW / 2;
	pos.Y = pos_field.Y + COL + 3;
	SetConsoleCursorPosition(out_put, pos);

	//开始排雷
	while (1)
	{
		//读取输入事件
		ReadConsoleInput(in_put, &mouse_record, 1, &res);
		//获取鼠标当前位置
		pos = mouse_record.Event.MouseEvent.dwMousePosition;
		pos.X = (pos.X % 2) == 0 ? pos.X : pos.X - 1;
		int x = (int)(pos.X - pos_field.X) / 2;
		int y = (int)pos.Y - pos_field.Y;
		//确定选择范围在雷场内
		if ((x >= 0) && (y >= 0) && (x < ROW) && (y < COL))
		{
			if (mouse_record.EventType == MOUSE_EVENT)
			{
				//单击鼠标右键
				if (mouse_record.Event.MouseEvent.dwButtonState == RIGHTMOST_BUTTON_PRESSED && arr[x][y] != -1)
					MarkMines(arr, pos);
				//双击
				if (mouse_record.Event.MouseEvent.dwEventFlags == DOUBLE_CLICK)
				{
					//恢复成原来颜色
					color(14);
					SetConsoleCursorPosition(out_put, pos);
					printf("■");
				}
				//单击鼠标左键
				else if (mouse_record.Event.MouseEvent.dwButtonState == FROM_LEFT_1ST_BUTTON_PRESSED)
				{
					//为雷
					if (arr[x][y] == 1)
					{
						pos.X = WIDTH / 2 + (WIDTH / 2 - 18) / 2;
						pos.Y = pos_field.Y + COL + 4;
						MinesShow(arr, pos_field);
						SetConsoleCursorPosition(out_put, pos);
						color(192);
						printf("很遗憾,您踩到了雷");
						//暂停一秒,让玩家知道失败了
						Sleep(1000);
						//界面更新
						InterfaceUpdate(pos_field);
						pos.X = pos.Y = 0;
						SetConsoleCursorPosition(out_put, pos);
						break;
					}
					else
					{
						//先置为-1
						arr[x][y] = -1;
						MinesSpread(arr, x, y, pos);
						//光标跳转到提示信息所处位置
						pos.X = WIDTH / 2 + (WIDTH / 2 - 18) / 2;
						pos.Y = pos_field.Y + COL + 4;
						SetConsoleCursorPosition(out_put, pos);
						//注意:先展开后进行游戏判断
						if (GameJudgment(arr))
						{
							color(32);
							//空格,保证字符串长度与原先一致,完全覆盖
							printf(" 恭喜你,挑战成功 ");
							Sleep(1000);
							//界面更新
							InterfaceUpdate(pos_field);
							break;
						}
						else
						{
							color(14);
							printf(" 位置x:%2d y:%2d ", x + 1, y + 1);
						}
					}
				}
			}
		}
	}
}

游戏判断

  由于雷有 NUM 个,已点击的均为 -1 若全部相加结果为 2 * NUM - ROW * COL ,则雷全部找出

bool GameJudgment(int arr[ROW][COL])
{
	int num = 0;
	for (int i = 0; i < ROW; i++)
	{
		for (int j = 0; j < COL; j++)
		{
			num += arr[i][j];
		}
	}
	if (num == 2 * NUM - ROW * COL)
		return true;
	return false;
}

界面更新

  无论游戏成功与否,均需要界面更新,打印出相应提示信息,供玩家选择。因此,需要将原先提示信息进行覆盖。

注意:一个中文占两个字符

void InterfaceUpdate(COORD pos_field)
{
	HANDLE out_put = GetStdHandle(STD_OUTPUT_HANDLE);;
	COORD ps;

	//左提示区
	ps.X = (WIDTH / 2 - 13) / 2;
	ps.Y = pos_field.Y + COL + 6;
	color(FOREGROUND_GREEN);
	SetConsoleCursorPosition(out_put, ps);
	printf("------------");
	ps.Y++;
	SetConsoleCursorPosition(out_put, ps);
	printf("| ");

	//color(FOREGROUND_RED);
	color(224);
	printf(" 请选择 ");
	color(FOREGROUND_GREEN);
	printf(" |");
	ps.Y++;
	SetConsoleCursorPosition(out_put, ps);
	printf("------------");


	//右提示区
	//消除首行提示信息
	ps.X = WIDTH / 2 + (WIDTH / 2 - 18) / 2;
	ps.Y = pos_field.Y + COL + 4;
	//反转背景色,以达到清除目的
	color(COMMON_LVB_REVERSE_VIDEO);
	SetConsoleCursorPosition(out_put, ps);
	for (int i = ps.X; i < WIDTH - 3; i++)
	{
		printf(" ");
	}
	ps.X = WIDTH / 2 + (WIDTH / 2 - 12) / 2;
	ps.Y++;
	SetConsoleCursorPosition(out_put, ps);
	color(176);
	printf("1.继续游戏");
	ps.Y += 2;
	SetConsoleCursorPosition(out_put, ps);
	printf("2.退出游戏");
}

玩家选择

  前文游戏主体函数中,我的 i 是减去了 2的,这是为和提示信息所对应一致,具体实现依据个人爱好。

int ChoiceGet(COORD pos_field)
{
	HANDLE out_put = GetStdHandle(STD_OUTPUT_HANDLE);
	HANDLE 	in_put = GetStdHandle(STD_INPUT_HANDLE);
	//用于存储鼠标当前坐标
	COORD ps = { 0,0 };
	//定义输入事件结构体
	INPUT_RECORD mouse_record;
	//用于存储读取记录
	DWORD res;

	while (1)
	{
		ReadConsoleInput(in_put, &mouse_record, 1, &res);
		ps = mouse_record.Event.MouseEvent.dwMousePosition;

		int x = (int)ps.X - (WIDTH / 2 + (WIDTH / 2 - 12) / 2);
		int y = (int)ps.Y - (pos_field.Y + COL + 5);

		if (mouse_record.EventType == MOUSE_EVENT)
		{
			ps.X = WIDTH / 2 + (WIDTH / 2 - 12) / 2;
			if ((mouse_record.Event.MouseEvent.dwButtonState == FROM_LEFT_1ST_BUTTON_PRESSED))
			{
				//提示信息所占字符大小为10个字符
				if (x >= 0 && x < 10)
				{
					ps.X = WIDTH / 3 + 4 + (WIDTH / 3 * 2 - 16) / 2;
					//确保对应行有提示信息
					if (y == 0)
					{
						//消除提示区
						ps.Y = (HEIGHT - COL - 2) / 3 + COL + 4;
						//反转前景和背景属性,以达到覆盖目的
						color(COMMON_LVB_REVERSE_VIDEO);
						SetConsoleCursorPosition(out_put, ps);
						printf("                     ");
						ps.Y += 2;
						SetConsoleCursorPosition(out_put, ps);
						printf("                     ");
						return 1;
					}
					else if (y == 2)
					{
						ps.X = WIDTH / 2 + 2;
						ps.Y += 2;
						color(245);					
						SetConsoleCursorPosition(out_put, ps);
						//退出提示
						printf("退出成功");
						return 2;
					}
				}
			}
		}
	}
}

注:关于光标跳转,各位亦可以写成函数,方便使用
参考代码:

void gotoxy(int x,int y)
{
	COORD ps;
	ps.X = x;
	ps.Y = y;
	SetConsoleCursorPosition(GetStdHandle(STD_INPUT_HANDLE), ps);
}

五、附录

完整代码链接👉 「 扫雷 」


  以上就是本次文章所带来的全部内容,若有帮助,点赞👍支持一波吧💛
在这里插入图片描述

  • 122
    点赞
  • 186
    收藏
    觉得还不错? 一键收藏
  • 46
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值