C++ 新手项目之扫雷
扫雷很简单,直接一个大数组,记录该方格状态就行了。唯一麻烦的地方在于图形化界面,每次点击都要更新画面。我使用的是Easyx实现游戏界面,这个库很简单,适合新手。
游戏展示
扫雷
前言
这次突发奇想,如果把所有的代码都写到一个类里会怎么样。结果很糟糕,找函数得往上找半天😭。过度设计要不得,偷懒设计也要不得。程序其实还可以添加几个按钮,让用户自行选择难度,逻辑大致为:根据点击区域,选择是否更新相应参数并重置游戏。
ps:素材是网上一个个扣下来的。给大家介绍个抠图神器:Snipaste,免费软件,非常好用。
设计思路
逻辑方面其实很简单。假如游戏区域为r * c (r行,c列),用数组mineMap[r * c]保存每个方格的状态(-1表示这个格子是雷,0-8表示周围雷的数量)。为防止第一次就踩雷,采用先点击再初始化地图的方法。以点击方格为中心的 3 * 3 范围设为安全区,插入地雷时避开该区域。揭露方块时,如果该方格mineMap[i] = 0(表示周围没有雷),则连同四周的方格一块揭露,DFS/BFS都能实现(建议使用BFS,当地图非常大,遇到一大批无雷区时会导致递归深度很深)。胜利检测,用了两个值 leftcell(剩余格子数)和 correctflagcount(正确插旗的方格数量),两者和为地雷总数时,获得胜利。
接着就是游戏画面的更新。检测鼠标点击信息,分左键和右键,左键揭露方格,右键给方格设置状态(插旗,不确定,正常状态)。点击后,根据该方格的状态,修改mineMap和revealed并更新小方格的图标(只改修改过方格即可)。
代码展示
#pragma once #include <windows.h> #include <random> #include <vector> #include <iostream> #include <graphics.h> #include <cstdio> #include <ctime> const int CELL = 30; // 每个小方格30*30像素 class mineSweeper { public: enum CellState : unsigned char { HIDDEN = 0, // 隐藏 REVEALED = 1, // 翻开 FLAGGED = 2, // 标记 QUESTION = 3 // 不确定 }; public: // 加载图片素材 IMAGE img_t[10]; // 时间 IMAGE img_d[9]; // 数字 IMAGE img_c[7]; // 方块的状态,未翻开,插旗,雷,问号 IMAGE img_f[5]; // 笑脸 void loadMyImg(); private: int row, col; // 行,列 int game_x, game_y; // 游戏区域尺寸为game_x*game_y int offsetX, offsetY; // cell区域相对(0,0)的偏移量 int facex, facey; // 笑脸图标的坐标 int imaget_x; // Time图片缩放后x的尺寸 int mines; // 雷的数量 bool gameover; // 游戏结束 bool gamewin; // 游戏胜利 bool firstgame; // 初次游戏 int leftcell; // 剩余的安全格子数 int correctflagcount; // 正确插旗的雷数 int flagcount; // 插旗数量 int time; // 时间 std::vector<int> mineMap; // mineMap[y*col+x]=j,j>=0表示周围8格雷的数量,j=-1表示该格为雷 std::vector<CellState> revealed; // mineMap中的格子的状态 public: mineSweeper() {} void setParameter(int r, int c, int num); // 设置参数 void initMapAfterClick(int safe_r, int saft_c); // 点击后再设置雷 int countAroundMine(int r, int c); // 计算该格周围的雷数 void calculateAllNumber(); // 设置mineMap每格的数字 void revealEmptyCell(int r, int c); // 揭露空的格子 void leftClick(int r, int c); // 左击方格 void rightClick(int r, int c); // 右击方格 void checkWin(); // 检查胜利 void resetGame(); // 重置游戏 void updateTime(); // 更新时间 public: void initUI(); // 初始化UI界面 void updateTimeUI(); // 更新右上角的时间 void updateCellImage(int r, int c); // 更新方格图标 void handleMouseClick(int x, int y, bool isLeft); // 处理点击 void updateAllCellImage(); // 更新所有cell图标 void updateFaceImage(); // 更新笑脸图标 void updateMinenumDisplay(); // 更新左上角的剩余地雷数 void FailureShow(); // 游戏失败时显示地雷 void quickReveal(int r, int c); // 点击已揭露的数字方格,闪烁提示四周范围并揭露方块 void run(); // 游戏的主循环 };#include "mine.h" // 加载图片 void mineSweeper::loadMyImg() { char filename[20]; int t_x = static_cast<int>((CELL + 5) / 2); int t_y = CELL + 5; // 时间 for (int i = 0; i < 10; ++i) { sprintf_s(filename, "./pictures/t%d.png", i); loadimage(&img_t[i], filename, t_x, t_y); } // 数字 for (int i = 0; i < 9; ++i) { sprintf_s(filename, "./pictures/%d.png", i); loadimage(&img_d[i], filename, CELL, CELL); } // 方块状态 for (int i = 0; i < 7; ++i) { sprintf_s(filename, "./pictures/c%d.png", i); loadimage(&img_c[i], filename, CELL, CELL); } // 笑脸 for (int i = 0; i < 5; ++i) { sprintf_s(filename, "./pictures/f%d.png", i); loadimage(&img_f[i], filename, CELL + 7, CELL + 7); } } // 设置参数 void mineSweeper::setParameter(int r, int c, int mine_num) { loadMyImg(); row = r; col = c; mines = mine_num; game_x = col * CELL + 30; game_y = row * CELL + 90; facex = game_x / 2 - 17; facey = 20; imaget_x = static_cast<int>(CELL / 2 + 5); offsetX = 15; offsetY = 75; gameover = false; gamewin = false; firstgame = true; time = 0; leftcell = r * c; flagcount = 0; correctflagcount = 0; mineMap.assign(static_cast<size_t>(r * c), 0); revealed.assign(static_cast<size_t>(r * c), CellState::HIDDEN); SetProcessDPIAware(); initgraph(game_x, game_y); } // 点击后设置安全区再插入地雷,初始化地图 void mineSweeper::initMapAfterClick(int r, int c) { // 以安全点为中心,3*3范围内的格子设置为安全 mineMap[static_cast<size_t>(r * col + c)] = -2; for (int dr = -1; dr <= 1; ++dr) { for (int dc = -1; dc <= 1; ++dc) { int newr = r + dr; int newc = c + dc; if (newr >= 0 && newr < row && newc >= 0 && newc < col) mineMap[static_cast<size_t>(newr * col + newc)] = -2; } } // 安全区之外插入地雷 int count = mines; std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution distr(0, row - 1); std::uniform_int_distribution distc(0, col - 1); while (count > 0) { int rr = distr(gen); int cc = distc(gen); size_t n = static_cast<size_t>(rr * col + cc); // -2为安全区,-1为雷区,0为可插入区 if (mineMap[n] == 0) { mineMap[n] = -1; --count; } } // 安全区设置为0 mineMap[static_cast<size_t>(r * col + c)] = 0; for (int dr = -1; dr <= 1; ++dr) { for (int dc = -1; dc <= 1; ++dc) { int newr = r + dr; int newc = c + dc; if (newr >= 0 && newr < row && newc >= 0 && newc < col) mineMap[static_cast<size_t>(newr * col + newc)] = 0; } } // 计算地图中每个格子的数字 calculateAllNumber(); firstgame = false; } // 计算格子周围的地雷数量 int mineSweeper::countAroundMine(int r, int c) { if (mineMap[static_cast<size_t>(r * col + c)] == -1) return -1; int count = 0; for (int dr = -1; dr <= 1; ++dr) { for (int dc = -1; dc <= 1; ++dc) { int newr = r + dr; int newc = c + dc; if (newr >= 0 && newr < row && newc >= 0 && newc < col) if (mineMap[static_cast<size_t>(newr * col + newc)] == -1) ++count; } } return count; } void mineSweeper::calculateAllNumber() { for (int r = 0; r < row; ++r) { for (int c = 0; c < col; ++c) { { size_t n = static_cast<size_t>(r * col + c); if (mineMap[n] != -1) mineMap[n] = countAroundMine(r, c); } } } } // 揭露该格 void mineSweeper::revealEmptyCell(int r, int c) { if (r < 0 || r >= row || c < 0 || c >= col) // 边界检查 return; size_t n = static_cast<size_t>(r * col + c); if (revealed[n] != CellState::HIDDEN) // 只处理隐藏状态的格子 return; if (mineMap[n] == -1) // 不处理雷 return; revealed[n] = CellState::REVEALED; // 该格被揭露 --leftcell; // test std::cout << "Leftcell: " << leftcell << "\tCorrectflagcount: " << correctflagcount << std::endl; if (mineMap[n] == 0) // 如果该格为0,则周围格子递归调用showEmpty { for (int dr = -1; dr <= 1; ++dr) { for (int dc = -1; dc <= 1; ++dc) { if (dr == 0 && dc == 0) continue; revealEmptyCell(r + dr, c + dc); } } } } void mineSweeper::leftClick(int r, int c) { // r,c 在handleMouseClick中检测过了,不用再检测 // if (gameover || gamewin) // return; // if (r < 0 || r >= row || c < 0 || c >= col) // return; size_t n = static_cast<size_t>(r * col + c); // 点击翻开的且数字(>0)的方格,闪烁周围未被揭露且未插旗的方格,并快速揭露肯定安全的方格 if (revealed[n] == CellState::REVEALED && mineMap[n] > 0) { quickReveal(r, c); return; } // 问号和插旗方格点击无效 if (revealed[n] == CellState::FLAGGED || revealed[n] == CellState::REVEALED || revealed[n] == CellState::QUESTION) return; // 是否初次游戏 if (firstgame) initMapAfterClick(r, c); // 是否中雷了 if (mineMap[n] == -1) { gameover = true; revealed[n] = CellState::REVEALED; FailureShow(); updateFaceImage(); return; } // 揭露方格 revealEmptyCell(r, c); // 检查胜利 checkWin(); } void mineSweeper::rightClick(int r, int c) { // if (gameover || gamewin) // return; // if (r < 0 || r >= row || c < 0 || c >= col) // return; size_t n = static_cast<size_t>(r * col + c); if (revealed[n] == CellState::REVEALED) return; // 切换状态 FLAGGED -> QUESTION -> HIDDEN ->FLAGGED switch (revealed[n]) { case CellState::FLAGGED: revealed[n] = CellState::QUESTION; if (mineMap[n] == -1) { --correctflagcount; // 取消正确标记 ++leftcell; // 剩余的非雷格子数+1 } --flagcount; break; case CellState::QUESTION: revealed[n] = CellState::HIDDEN; break; case CellState::HIDDEN: if (flagcount == mines) // 当插旗数等于地雷数时无法插旗 break; revealed[n] = CellState::FLAGGED; if (mineMap[n] == -1) { ++correctflagcount; // 正确标记了雷 --leftcell; // 剩余的非雷格子数-1 } ++flagcount; break; default: break; } // test std::cout << "Leftcell: " << leftcell << "\tCorrectflagcount: " << correctflagcount << std::endl; } void mineSweeper::checkWin() { // 剩余格子数+正确插旗数 == 雷数时判赢 if (leftcell + correctflagcount == mines) gamewin = true; // 胜利时显示所有未插旗的雷 if (gamewin == true) { for (int r = 0; r < row; ++r) { for (int c = 0; c < col; ++c) { size_t n = static_cast<size_t>(r * col + c); if (mineMap[n] == -1 && revealed[n] != CellState::FLAGGED) revealed[n] = CellState::FLAGGED; } } std::cout << "获得胜利了!" << std::endl; } } void mineSweeper::initUI() { setbkcolor(RGB(204, 204, 204)); cleardevice(); // 雷区的起始位置为(15,75) for (int i = 0; i < col; ++i) { for (int j = 0; j < row; ++j) { putimage(offsetX + i * CELL, offsetY + j * CELL, &img_c[0]); } } // 边界线,模拟阴影 setlinecolor(RGB(128, 128, 128)); // 灰 for (int i = 0; i < 2; ++i) { line(13, 15 + i, game_x - 14, 15 + i); line(13, 73 + i, game_x - 14, 73 + i); line(13 + i, 75, 13 + i, game_y - 14); line(13 + i, 15, 13 + i, 60); } setlinecolor(RGB(255, 255, 255)); // 白 for (int i = 0; i < 2; ++i) { line(15, game_y - 15 + i, game_x - 14, game_y - 15 + i); line(game_x - 15 + i, 75, game_x - 15 + i, game_y - 14); line(15, 60 + i, game_x - 14, 60 + i); line(game_x - 15 + i, 15, game_x - 15 + i, 60); } // 绘制笑脸 putimage(game_x / 2 - 17, 20, &img_f[0]); // 时间和雷数 int temp = mines; int index; for (int i = 0; i < 3; ++i) { index = temp % 10; temp = static_cast<int>(temp / 10); putimage(20 + (2 - i) * imaget_x, 21, &img_t[index]); // 左侧的雷数 putimage(game_x - 20 - (i + 1) * imaget_x, 21, &img_t[0]); // 右侧的时间 } } // time每秒+1,时间每秒更新一次 void mineSweeper::updateTimeUI() { int temp = time; int index; for (int i = 0; i < 3; ++i) { index = temp % 10; temp = static_cast<int>(temp / 10); putimage(game_x - 20 - (i + 1) * imaget_x, 21, &img_t[index]); // 右侧的时间 } } // 更新cell图标 void mineSweeper::updateCellImage(int r, int c) { if (r < 0 || r >= row || c < 0 || c >= col) return; // cell区域的起始像素坐标 int x = offsetX + c * CELL; int y = offsetY + r * CELL; size_t n = static_cast<size_t>(r * col + c); if (revealed[n] == REVEALED) { if (mineMap[n] == -1) putimage(x, y, &img_c[3]); // 雷 else { putimage(x, y, &img_d[0]); // 先显示个空白 putimage(x, y, &img_d[mineMap[n]]); // 再显示数字 } } else if (revealed[n] == FLAGGED) putimage(x, y, &img_c[1]); // 插旗 else if (revealed[n] == QUESTION) putimage(x, y, &img_c[5]); // 问号 else putimage(x, y, &img_c[0]); // 隐藏 } void mineSweeper::resetGame() { gameover = false; gamewin = false; firstgame = true; leftcell = row * col; flagcount = 0; correctflagcount = 0; time = 0; // 重置地图 std::fill(mineMap.begin(), mineMap.end(), 0); std::fill(revealed.begin(), revealed.end(), CellState::HIDDEN); cleardevice(); initUI(); } void mineSweeper::handleMouseClick(int x, int y, bool isLeft) { // 点击笑脸区域重置游戏 printf("%s点击: (%d, %d)\n", (isLeft == true) ? "左键" : "右键", x, y); // 调试用 int facesize = CELL + 7; if (x >= facex && x <= facex + facesize && y >= facey && y <= facey + facesize) { resetGame(); return; } // 游戏失败或成功后除了笑脸区域都不能点击 if (gameover || gamewin) return; // 判断是否在区域内 if (x < offsetX || x >= offsetX + col * CELL || y < offsetY || y >= offsetY + row * CELL) { return; // 不在游戏区域内,直接返回 } // !!!!-5/CELL == 0,这会导致计算错误,所以先要判断是否在游戏区域内部 int c = (x - offsetX) / CELL; int r = (y - offsetY) / CELL; if (c < 0 || c >= col || r < 0 || r >= row) return; if (isLeft) { leftClick(r, c); updateAllCellImage(); updateFaceImage(); updateMinenumDisplay(); if (gameover) { FailureShow(); } } else { rightClick(r, c); updateCellImage(r, c); // 右键只需要改变一个图标即可? updateMinenumDisplay(); } } void mineSweeper::updateAllCellImage() { for (int r = 0; r < row; ++r) { for (int c = 0; c < col; ++c) { updateCellImage(r, c); } } } void mineSweeper::updateFaceImage() { int index = 0; if (gameover) index = 4; // 失败 else if (gamewin) index = 3; // 胜利,酷 else return; // 正常,不需要改变图标 putimage(facex, facey, &img_f[index]); } void mineSweeper::updateMinenumDisplay() { int temp = mines - flagcount; // 剩余地雷数 int index; for (int i = 0; i < 3; ++i) { index = temp % 10; temp = temp / 10; putimage(20 + (2 - i) * imaget_x, 21, &img_t[index]); } } void mineSweeper::quickReveal(int r, int c) { if (gameover || gamewin) return; if (r < 0 || r >= row || c < 0 || c >= col) return; auto n = static_cast<size_t>(r * col + c); int aroundflag = 0; // 周围插旗的数量 std::vector<std::pair<int, int>> hiddencells; for (int dr = -1; dr <= 1; ++dr) { for (int dc = -1; dc <= 1; ++dc) { if (dr == 0 && dc == 0) continue; int nr = r + dr; int nc = c + dc; if (nr < 0 || nr >= row || nc < 0 || nc >= col) continue; auto idx1 = static_cast<size_t>(nr * col + nc); if (revealed[idx1] == CellState::FLAGGED) ++aroundflag; else if (revealed[idx1] == CellState::HIDDEN || revealed[idx1] == CellState::QUESTION) { hiddencells.push_back({nr, nc}); } } } // 周围旗帜的数量等于显示的数字 if (aroundflag == mineMap[n] && mineMap[n] > 0) { bool hitmine = false; for (auto &cell : hiddencells) { int cr = cell.first; int cc = cell.second; auto idx2 = static_cast<size_t>(cr * col + cc); if (mineMap[idx2] == -1) { gameover = true; revealed[idx2] = CellState::REVEALED; hitmine = true; } else { revealEmptyCell(cr, cc); } } if (hitmine) FailureShow(); else checkWin(); } else { // 闪烁提示 for (auto &cell : hiddencells) { int x = offsetX + cell.second * CELL; int y = offsetY + cell.first * CELL; putimage(x, y, &img_d[0]); } Sleep(100); // 恢复原来的图像 for (auto &cell : hiddencells) { updateCellImage(cell.first, cell.second); } } flushmessage(EX_MOUSE); //清除堆积的鼠标信息,防止延时操作 } void mineSweeper::FailureShow() { // 游戏失败时,显示所有雷 for (int r = 0; r < row; ++r) { for (int c = 0; c < col; ++c) { size_t n = static_cast<size_t>(r * col + c); int x = offsetX + c * CELL; int y = offsetY + r * CELL; if (mineMap[n] == -1) { if (revealed[n] == CellState::FLAGGED) { continue; // 不需要改变,进行下一轮检查 } else if (revealed[n] == CellState::HIDDEN || revealed[n] == CellState::QUESTION) { putimage(x, y, &img_c[2]); // 没标出的雷 } } else if (mineMap[n] != -1 && revealed[n] == CellState::FLAGGED) { putimage(x, y, &img_c[4]); // 旗标错了 } } } } void mineSweeper::updateTime() { if (gameover || gamewin || firstgame) return; time++; if (time > 999) time = 999; updateTimeUI(); } void mineSweeper::run() { ExMessage msg; bool running = true; clock_t lastTime = clock(); initUI(); while (running) { while (peekmessage(&msg, EM_MOUSE | EM_KEY)) { switch (msg.message) { case WM_LBUTTONDOWN: // 按下左键 if (!gameover && !gamewin) { bool inGame = (msg.x >= offsetX && msg.x < offsetX + col * CELL && msg.y >= offsetY && msg.y < offsetY + row * CELL); bool onFace = (msg.x >= facex && msg.x <= facex + CELL + 7 && msg.y >= facey && msg.y <= facey + CELL + 7); if (onFace) { putimage(facex, facey, &img_f[1]); } else { if (!inGame) { putimage(facex, facey, &img_f[2]); // 点击非cell区显示惊讶 } } } break; case WM_LBUTTONUP: // 松开左键 if (!gameover && !gamewin) { putimage(facex, facey, &img_f[0]); // 松开左键恢复笑脸 } handleMouseClick(msg.x, msg.y, true); // 处理点击 break; case WM_RBUTTONDOWN: // 按下右键 handleMouseClick(msg.x, msg.y, false); break; case WM_KEYDOWN: // 处理键盘输入 if (msg.vkcode == VK_ESCAPE) { running = false; // 按 ESC 退出 } break; } } clock_t currentTime = clock(); if (currentTime - lastTime >= 1000) { updateTime(); lastTime = currentTime; } Sleep(10); } }