前言
??你的「掃雷」💣 還不能用「滑鼠」玩? 還在輸入「坐標」? 已經都 「2021」了 ?? 誰還會玩你的「掃雷」呢?
?? ??「看過來」??,本文將 手把手 💪帶你寫出「新的掃雷」😎
提示:該專案基于C語言撰寫,由 VisualStudio 2019 所實作
文章目錄
- 前言
- 一、游戲效果展示:
- 二、界面函式
- 2.0 句柄
- 2.0.1 GetStdHandle() 函式
- 2.1 控制臺
- 2.1.1 COORD結構體
- 2.1.2 SMALL_RECT 結構
- 2.1.3 SetConsoleTitle() 函式
- 2.1.4 SetConsoleWindowInfo() 函式
- 2.2 隱藏游標
- 2.2.1 CONSOLE_CURSOR_INFO 結構
- 2.2.2 GetConsoleCursorInfo() 函式
- 2.3 游標跳轉
- 2.3.1 SetConsoleCursorPosition() 函式
- 2.4 文本顏色函式
- 2.4.1 SetConsoleTextAttribute() 函式
- 2.4.2 字符屬性
- 2.4.3 顏色對照表
- 2.4.4 參考代碼
- 三、滑鼠事件
- 3.1 INPUT_RECORD結構
- 3.2 MOUSE_EVENT_RECORD 結構
- 3.3 ReadConsoleInput() 函式
- 3.4 參考示例
- 四、游戲代碼剖析
- 框架構建
- 游戲主體函式
- 界面初始化
- 布雷
- 排雷
- 滑鼠坐標資訊
- 標記雷
- 展開雷
- 排雷完整代碼
- 游戲判斷
- 界面更新
- 玩家選擇
- 五、附錄
注:游戲中所有操作均通過滑鼠實作
一、游戲效果展示:
下圖以三倍速展示
二、界面函式
??若想實作控制臺的基本操作,需先了解以下內容
注:下列函式,均須參考頭檔案 <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 | 控制臺螢屏緩沖區的句柄,標準輸出句柄即可 |
| 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);
結果如下:

點擊了解更多👉官方參考網址
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;
結構解釋:
| 引數 | 引數含義 |
|---|---|
| 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 | 滑鼠位置發生變化 |
注:上述僅提供了本專案可能用到的值,
3.3 ReadConsoleInput() 函式
函式結構:
BOOL WINAPI ReadConsoleInput(
_In_ HANDLE hConsoleInput,
_Out_ PINPUT_RECORD lpBuffer,
_In_ DWORD nLength,
_Out_ LPDWORD lpNumberOfEventsRead
);
結構說明:
| 功能 | 從緩沖區讀取資料并洗掉 |
|---|---|
| 引數 | 含義 |
| hConsoleInput | 標準輸入句柄 |
| lpBuffer | 指向INPUT_RECORD 結構的指標 |
| nLength | lpBuffer引數指向的陣列的大小,以陣列元素為單位, |
| 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);
}
五、附錄
完整代碼鏈接👉 「 掃雷 」
??以上就是本次文章所帶來的全部內容,若有幫助,點贊👍支持一波吧💛

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/296677.html
標籤:其他
