你的「扫雷」 还不能用「鼠标」玩❓ 还在输入「坐标」❓ 已经都 「2021」了 ❗️ 谁还会玩你的「扫雷」呢❓
❤️「看过来」❤️,本文将 手把手 带你写出「新的扫雷」
提示:该项目基于C语言编写,由 VisualStudio 2019 所实现
注:游戏中所有操作均通过鼠标实现
下图以三倍速展示
若想实现控制台的基本操作,需先了解以下内容
注:下列函数,均须引用头文件
要想对界面进行一系列的「操作」,则离不开「句柄」这一重要概念。「句柄」是Windows最常用的概念。它通常用来标识Windows资源(如菜单、图标、窗口等)和设备等对象。虽然可以把句柄理解为是一个指针变量类型,但它不是对象所在的地址指针,而是作为Windows系统内部表的索引值来使用的。
其声明为:
typedef void *HANDLE;
从上面可以看出,句柄「 HANDLE 」是一个无类型指针。
参考代码:
HANDLE out_put = NULL;
点此深入了解深入了解Windows句柄到底是什么
函数结构:
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);
若要进行相关窗口操作,则必须先了解 「COORD」结构,结构如下:
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
说明:
X | 水平坐标或列值 |
---|---|
Y | 垂直坐标或行值 |
注:单位取决于函数调用
结构如下:
typedef struct _SMALL_RECT {
SHORT Left;
SHORT Top;
SHORT Right;
SHORT Bottom;
} SMALL_RECT;
结构说明:
功能 | 定义矩形的左上角和右下角的坐标 |
---|---|
Left | 矩形左上角的x坐标 |
Top | 矩形左上角的y坐标 |
Right | 矩形右下角的x坐标 |
Bottom | 矩形右下角的y坐标 |
函数结构:
BOOL WINAPI SetConsoleTitle(_In_ LPCTSTR lpConsoleTitle);
功能 | 设置当前控制台窗口标题 |
---|---|
参数:lpConsoleTitle | 要在控制台窗口的标题栏中显示的字符串 |
返回值 | 如果函数成功,则返回值为非零值。反之为0 |
参考代码:
SetConsoleTitle("扫雷");
注意:在实际使用过程中,出现了控制台乱码的情况
解决方法:右击 当前解决方案 属性 配置属性 高级 字符集 使用更多字符集
点击了解更多官方参考网址
函数结构:
BOOL WINAPI SetConsoleWindowInfo(
_In_ HANDLE hConsoleOutput,
_In_ BOOL bAbsolute,
_In_ const SMALL_RECT *lpConsoleWindow
);
功能 | 设置控制台屏幕缓冲区窗口的当前大小和位置 |
---|---|
参数 | 参数含义 |
hConsoleOutput | 可理解为标准输出句柄 |
bAbsolute | 如果此参数为TRUE,则坐标指定窗口的新左上角和右下角。如果为FALSE,则坐标相对于当前窗口角坐标 |
lpConsoleWindow | 指向SMALL_RECT结构的指针,该结构指定窗口的新左上角和右下角 |
返回值 | 如果函数成功,则返回值为非零值。反之为0 |
点击了解更多官方参考网址
扫雷过程中,为使界面更加美观,隐藏光标是必须的。在使用相关函数前,让我们先了解下列结构
结构说明:
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
参数 | 参数含义 |
---|---|
dwSize | 光标填充的字符单元格的百分比。通常该值介于1和100之间。光标外观会发生变化,从完全填充单元格到显示为单元格底部的水平线。 |
bVisible | 该参数为bool 类型,表示光标的可见性。如果光标可见,则此成员为TRUE,反之为 FALSE |
函数结构:
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);
点击了解更多官方参考网址
若想指定文本输出位置,可以通过函数 GetConsoleScreenBufferInfo() 与 FillConsoleOutputCharacter() 实现指定位置填充,但上述函数结构复杂,因此本文通过「 光标跳转」来实现。
函数结构:
BOOL WINAPI SetConsoleCursorPosition(
_In_ HANDLE hConsoleOutput,
_In_ COORD dwCursorPosition
);
函数解释:
功能 | 设置指定控制台屏幕缓冲区中的光标位置 |
---|---|
参数 | 参数解释 |
hConsoleOutput | 控制台屏幕缓冲区的句柄,标准输出句柄即可 |
dwCursorPosition | COORD 结构,用于指定新的光标位置(以字符为单位) |
返回值 | 如果函数成功,则返回值为非零值。 |
参考代码:
//定义句柄
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);
若要实现前文游戏界面,用不同颜色区分「 边界 」和「 雷区 」,则需使用 相关函数来实现。
函数结构:
BOOL WINAPI SetConsoleTextAttribute(HANDLE hConsoleOutput, WORD wAttributes);
函数解释:
功能 | 设置控制台文本属性(颜色),可以设置前景色FOREGROUND(文本颜色)和背景色BACKGROUND |
---|---|
参数 | 参数解释 |
hConsoleOutput | 控制台屏幕缓冲区的句柄。此处获取标准输出句柄即可 |
wAttributes | 字符属性 |
返回值 | 如果函数成功,则返回值为非零值。 |
点击了解更多官方参考网址
字符属性可以分为两类:颜色和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 | 下划线 |
除上述以外,还有以下颜色可供选择
//定义句柄
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() 函数的使用仅此而已。
仅仅掌握上述函数,只能美化你的界面,未能达到 「 鼠标操作 」 的目的。因此,还需学习以下内容
功能:描述控制台输入缓冲区中的输入事件
结构说明:
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结构有关控制台屏幕缓冲区的新大小信息 |
由于本文仅用鼠标操作,故仅介绍该结构
结构说明:
typedef struct _MOUSE_EVENT_RECORD {
COORD dwMousePosition;
DWORD dwButtonState;
DWORD dwControlKeyState;
DWORD dwEventFlags;
} MOUSE_EVENT_RECORD;
结构解释:
参数 | 参数含义 |
---|---|
dwMousePosition | COORD 结构,用来记录光标位置 |
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 | 鼠标位置发生变化 |
注:上述仅提供了本项目可能用到的值。
函数结构:
BOOL WINAPI ReadConsoleInput(
_In_ HANDLE hConsoleInput,
_Out_ PINPUT_RECORD lpBuffer,
_In_ DWORD nLength,
_Out_ LPDWORD lpNumberOfEventsRead
);
结构说明:
功能 | 从缓冲区读取数据并删除 |
---|---|
参数 | 含义 |
hConsoleInput | 标准输入句柄 |
lpBuffer | 指向INPUT_RECORD 结构的指针 |
nLength | lpBuffer参数指向的数组的大小,以数组元素为单位。 |
lpNumberOfEventsRead | 指向LPDWORD 结构的指针,该结构用来存储读取记录 |
点击了解更多官方参考网址
//定义句柄
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);
}
完整代码链接 「 扫雷 」