作為 MFC 入門練手項目,本專案完整復刻了 Windows 經典掃雷遊戲的核心邏輯與界面交互。通過這個項目,重點掌握了 MFC 文檔/視圖架構、GDI 繪圖技術、遞歸算法在遊戲邏輯中的應用,以及資源管理與事件處理機制。
每個格子的狀態封裝在 MINEWND 結構中:
struct MINEWND {
UINT row, col; // 行列座標
UINT uAttrib; // 屬性:普通/雷/標記
UINT uState; // 當前狀態:未翻開/已翻開/標旗/問號
UINT uOldState; // 上一狀態(用於復原/繪圖優化)
};
主視圖類 CMineWnd 維護二維數組 m_pMines[24][30] 支持最大 24×30 難度。
uState 動態選擇位圖繪製BitBlt 到屏幕為保證公平性,第一次點擊不觸雷,佈雷在首次點擊時動態生成:
void CMineWnd::LayMines(UINT row, UINT col) {
srand((unsigned)time(NULL)); // 種子確保每局不同
for (UINT index = 0; index < m_uMineNum; ) {
UINT i = rand() % m_uYNum;
UINT j = rand() % m_uXNum;
if (i == row && j == col) continue; // 首點絕不佈雷
if (m_pMines[i][j].uAttrib != ATTRIB_MINE) {
m_pMines[i][j].uAttrib = ATTRIB_MINE;
index++;
}
}
}
點擊空白格(周圍雷數為 0)時自動展開鄰域:
void CMineWnd::ExpandMines(UINT row, UINT col) {
UINT around = GetAroundNum(row, col);
m_pMines[row][col].uState = 15 - around; // 狀態映射到對應數字圖片
m_pMines[row][col].uOldState = 15 - around;
DrawSpecialMine(row, col); // 局部重繪
if (around == 0) { // 只有空白格才遞歸展開
UINT minRow = (row == 0) ? 0 : row - 1;
UINT maxRow = min(row + 2, m_uYNum);
UINT minCol = (col == 0) ? 0 : col - 1;
UINT maxCol = min(col + 2, m_uXNum);
for (UINT i = minRow; i < maxRow; i++) {
for (UINT j = minCol; j < maxCol; j++) {
if (!(i == row && j == col)
&& m_pMines[i][j].uState == STATE_NORMAL
&& m_pMines[i][j].uAttrib != ATTRIB_MINE) {
ExpandMines(i, j); // 遞歸調用
}
}
}
}
}
關鍵優化:邊界檢查使用 min/max 避免越界,狀態判斷防止重複展開導致棧溢出。
uAttrib == ATTRIB_MINE)或標記錯誤後揭示STATE_NORMAL 剩餘數等於雷數)| 操作 | 左鍵 | 右鍵 | 雙鍵/中鍵 |
|---|---|---|---|
| 未翻開格 | 展開 | 標旗/問號循環 | 周圍旗數匹配時自動展開 |
| 已翻開數字 | 無效 | 無效 | 周圍旗數匹配時自動展開 |
| 已標旗 | 無效 | 改問號/取消 | 無效 |
srand(time(NULL)) 僅在佈雷時調用一次,避免同秒多次調用導致序列相同DrawSpecialMine),配合 InvalidateRect 區域失效CBitmap、CDC,防止 GDI 句柄泄漏參考了 lpq9907 的掃雷源碼,感謝開源社區的知識分享。代碼註釋採用英文練習,歡迎指正。