赞
踩
(tips: 因为很多同学都只学过c语言或刚刚入门c++,所以本系列文章主要以由浅入深的方式进行讲解,后续才会使用语言的新特性、标准模板库、面向对象、图形化编程等。请莫要心急,迷雾会慢慢被揭开)
扫雷是Windows平台的家喻户晓的经典小游戏,本节内容将讲解如何在控制台完成扫雷的开发。
先看一下运行效果图:
我们先给出代码的雏形,每个全局变量后面都有注释,如果暂时无法理解它们的用处,没关系,待会你就明白了。
#include <iostream> #include <Windows.h> #include <time.h> #include <conio.h> using std::cout; using std::endl; const int MAP_WIDTH = 24; //地图宽度 const int MAP_HEIGHT = 24; //地图高度 const float PROP_OF_MINES = 0.1f; //地雷比例 const char CHAR_MINE = '*'; //地雷符号 const char CHAR_UNKNOW = '?'; //格子符号 const int GAME_OVER_FAILED = 0; //游戏失败标识 const int GAME_OVER_SUCCESSED = 1; //游戏成功标识 const int GAME_NOTH = 2; //游戏正常标识 int g_cursorPosx = 0; //光标横坐标 int g_cursorPosy = 0; //光标纵坐标 int g_isNotOpen = MAP_WIDTH * MAP_HEIGHT; //未打开的格子数 int g_numOfMines = MAP_WIDTH * MAP_HEIGHT * PROP_OF_MINES; //地雷数 //格子 struct Block { bool isOpen; //格子是否打开 bool isMine; //此格是不是地雷 unsigned int countOfMines; //此格周围的地雷数 } map[MAP_WIDTH + 2][MAP_HEIGHT + 2]; //二维数组比地图大一圈 int main() { srand(unsigned int(time(NULL))); //初始化随机数种子 _getch(); return 0; }
看过前几篇文章的同学都应该知道,游戏程序的流程大致可以分成三个部分:
1.游戏初始化阶段:用于加载资源,生成地图数据等操作
2.游戏运行阶段:主要是由一个循环负责,在循环里做输入处理、状态更新、图像绘制等操作
3.游戏结束阶段:可以用于释放资源,输出结束信息等操作
先定义一个名为GotoPos的函数来移动光标在控制台中的位置(窗口坐标系我在前面的文章有过介绍,这里不再赘述),这个函数尤其重要:
void GotoPos(int x, int y) {
HANDLE hout = GetStdHandle(STD_OUTPUT_HANDLE);
COORD cor = { x, y };
SetConsoleCursorPosition(hout, cor);
}
好了,话不多说,先来第一步:游戏初始化。我们用一个名为InitGame的函数进行初始化:
void InitGame() { memset(map, 0, sizeof(map)); //置零未初始化的结构体数组 //生成地雷 for (int index = 1; index < g_numOfMines; ) { //生成的下标范围在[1, 99],实际容器范围是[0, 100] int randomIndexX = rand() % (MAP_WIDTH - 1) + 1; int randomIndexY = rand() % (MAP_HEIGHT - 1) + 1; //如果此格不是地雷(为防止地雷重复生成到同一格) if (!map[randomIndexX][randomIndexY].isMine) { map[randomIndexX][randomIndexY].isMine = true; //标记为地雷 //地雷四周的格子的地雷计数加1 for (int indexX = randomIndexX - 1; indexX < randomIndexX + 2; ++indexX) { for (int indexY = randomIndexY - 1; indexY < randomIndexY + 2; ++indexY) { map[indexX][indexY].countOfMines++; } } ++index; //生成一颗雷,index才加一 } } //打印地图 for (int indexY = 1; indexY <= MAP_HEIGHT; ++indexY) { for (int indexX = 1; indexX <= MAP_WIDTH; ++indexX) { cout << CHAR_UNKNOW; //输出字符 '?' map[indexX][indexY].isOpen = false; //把格子的状态置为未打开 } cout << endl; } GotoPos(g_cursorPosx, g_cursorPosy); }
我们用随机数生成地雷,且生成的地雷的坐标在 [1, MAP_WIDTH] 这个闭区间内。我们为地雷附近的格子的地雷计数加一:
但是,当地雷生成到地图边界时,为防止越界访问,我们就不得不判断其周围还有没有格子:
为了避免作出多余的判断,我们将二维数组扩大"一圈",这也是代码中为什么定义map时数组尺寸是MAP_WIDTH + 2 和 MAP_HEIGHT + 2 的原因。
扩大前
扩大后
因此我们在生成地雷对其周围格子地雷计数加一时,就不用担心什么越界,只要将下标访问控制在绿色的范围内。
//打印地图
for (int indexY = 1; indexY <= MAP_HEIGHT; ++indexY) {
for (int indexX = 1; indexX <= MAP_WIDTH; ++indexX) {
cout << CHAR_UNKNOW; //输出字符 '?'
map[indexX][indexY].isOpen = false; //把格子的状态置为未打开
}
cout << endl;
}
GotoPos(g_cursorPosx, g_cursorPosy);
我们用字符‘?’代表格子,并遍历所有格子把它们的状态置为关闭。这便是游戏初始化的所有内容。
我们再用非阻塞的方式获取并处理键盘输入(switch里的数字是字母W、S、A、D大小写的ASCII值,
32是空格符的ASCII值),也就是说我们用WSAD键控制光标移动, 用空格键打开格子。
//处理键盘输入 const int InputProcess() { char cinput; if (_kbhit()) { cinput = _getch(); //使用_getch()需要 #include <conio.h> unsigned int xtrans = 0; unsigned int ytrans = 0; switch (cinput) { case 87: case 119: { if (g_cursorPosy > 0) { ytrans = -1; } break; } case 53: case 115: { if (g_cursorPosy < MAP_HEIGHT - 1) { ytrans = 1; } break; } case 65: case 97: { if (g_cursorPosx > 0) { xtrans = -1; } break; } case 68: case 100: { if (g_cursorPosx < MAP_WIDTH - 1) { xtrans = 1; } break; } case 32: { //如果打开的位置是雷,返回信号GAME_OVER if (map[g_cursorPosx + 1][g_cursorPosy + 1].isMine) { return GAME_OVER_FAILED; } DFS(g_cursorPosx + 1, g_cursorPosy + 1); break; } default: { break; } } g_cursorPosx += xtrans; g_cursorPosy += ytrans; GotoPos(g_cursorPosx, g_cursorPosy); } return GAME_NOTH; }
上述代码中的DFS函数定义:
//深度优先搜索 void DFS(int posx, int posy) { //已经点开过或者越界就返回 if (map[posx][posy].isOpen || posx <= 0 || posx >= MAP_WIDTH || posy <= 0 || posy >= MAP_HEIGHT) { return; } //点开此格 GotoPos(posx - 1, posy - 1); cout << (0 == map[posx][posy].countOfMines ? ' ' : char(map[posx][posy].countOfMines + 48)); map[posx][posy].isOpen = true; g_isNotOpen--; //如果此格周围存在地雷,则返回 if (map[posx][posy].countOfMines > 0) { return; } //以递归深度优先遍历地图格子 DFS(posx + 1, posy); DFS(posx - 1, posy); DFS(posx, posy + 1); DFS(posx, posy - 1); }
我们以递归的方式进行深度优先搜索,即从打开的位置向上下左右四个方向继续打开,如果遇到了周围有雷的格子,打开它,然后不再继续打开直接返回。在InputProcess函数的case 32中,我们可以看到,如果这个格子是雷,便直接返回游戏结束标识。
如果不是雷,就调用DFS函数。在DFS函数里,如果这个格子周围有地雷,就直接打开它:
如果这个格子周围没有地雷,就继续递归搜索,疯狂打开,直到遇到有地雷的格子:
然后定义一个CheckSweeping函数,用来检测玩家是否扫完了所有的雷,其原理很简单,就是判断剩下的未打开的格子数量是否与地雷数量相等,如果相等,就说明游戏扫雷成功。
const int CheakSweeping() {
if (g_numOfMines != g_isNotOpen) {
return GAME_NOTH;
}
return GAME_OVER_SUCCESSED;
}
最后定义一个游戏结束的函数GameOver:
void GameOver(int type) { system("cls"); //清空控制台 GotoPos(0, 0); //光标回到左上角 //将所有格子的打开状态打印出来 for (int indexY = 1; indexY <= MAP_HEIGHT; ++indexY) { for (int indexX = 1; indexX <= MAP_WIDTH; ++indexX) { if (map[indexX][indexY].isMine) { cout << CHAR_MINE; } else { cout << (0 == map[indexX][indexY].countOfMines ? ' ' : char(map[indexX][indexY].countOfMines + 48)); } } cout << endl; } //判断是哪种游戏结束,踩到雷了还是扫雷成功 if (0 == type) { cout << "-Game Over-\n-Don't lose heart, you'll do better!-"; } else if (1 == type) { cout << "-Game Over-\n-Congratulations!-\n-You won!-"; } GotoPos(g_cursorPosx, g_cursorPosy); }
到这里,游戏核心代码全部完成啦~,以下是所有代码
#include <iostream> #include <Windows.h> #include <time.h> #include <conio.h> using std::cout; using std::endl; const int MAP_WIDTH = 24; //地图宽度 const int MAP_HEIGHT = 24; //地图高度 const float PROP_OF_MINES = 0.1f; //地雷比例 const char CHAR_MINE = '*'; //地雷符号 const char CHAR_UNKNOW= '?'; //未知符号 const int GAME_OVER_FAILED = 0; //游戏失败标识 const int GAME_OVER_SUCCESSED = 1; //游戏成功标识 const int GAME_NOTH = 2; //游戏正常标识 int g_cursorPosx = 0; //光标横坐标 int g_cursorPosy = 0; //光标纵坐标 int g_isNotOpen = MAP_WIDTH * MAP_HEIGHT; //未打开的格子数 int g_numOfMines = MAP_WIDTH * MAP_HEIGHT * PROP_OF_MINES; //地雷数 struct Block { bool isOpen; //格子是否打开 bool isMine; //此格是不是地雷 unsigned int countOfMines; //此格周围的地雷计数 } map[MAP_WIDTH + 2][MAP_HEIGHT + 2]; //二维数组比地图大一圈 void GotoPos(int x, int y) { HANDLE hout = GetStdHandle(STD_OUTPUT_HANDLE); COORD cor = { x, y }; SetConsoleCursorPosition(hout, cor); } void InitGame() { memset(map, 0, sizeof(map)); //置零未初始化的结构体数组 //生成地雷 for (int index = 1; index < g_numOfMines; ) { //生成的下标范围在[1, 99],实际容器范围是[0, 100] int randomIndexX = rand() % (MAP_WIDTH - 1) + 1; int randomIndexY = rand() % (MAP_HEIGHT - 1) + 1; //如果此格不是地雷(为防止地雷重复生成到同一格) if (!map[randomIndexX][randomIndexY].isMine) { map[randomIndexX][randomIndexY].isMine = true; //标记为地雷 //地雷四周的格子的地雷计数加1 for (int indexX = randomIndexX - 1; indexX < randomIndexX + 2; ++indexX) { for (int indexY = randomIndexY - 1; indexY < randomIndexY + 2; ++indexY) { map[indexX][indexY].countOfMines++; } } ++index; //生成一颗雷,index才加一 } } //打印地图 for (int indexY = 1; indexY <= MAP_HEIGHT; ++indexY) { for (int indexX = 1; indexX <= MAP_WIDTH; ++indexX) { cout << CHAR_UNKNOW; //输出字符 '?' map[indexX][indexY].isOpen = false; //把格子的状态置为未打开 } cout << endl; } GotoPos(g_cursorPosx, g_cursorPosy); } //深度优先搜索 void DFS(int posx, int posy) { //已经点开过或者越界就返回 if (map[posx][posy].isOpen || posx <= 0 || posx >= MAP_WIDTH || posy <= 0 || posy >= MAP_HEIGHT) { return; } //点开此格 GotoPos(posx - 1, posy - 1); cout << (0 == map[posx][posy].countOfMines ? ' ' : char(map[posx][posy].countOfMines + 48)); map[posx][posy].isOpen = true; g_isNotOpen--; //如果此格周围存在地雷,则返回 if (map[posx][posy].countOfMines > 0) { return; } //以递归深度优先遍历地图格子 DFS(posx + 1, posy); DFS(posx - 1, posy); DFS(posx, posy + 1); DFS(posx, posy - 1); } //处理键盘输入 const int InputProcess() { char cinput; if (_kbhit()) { cinput = _getch(); //使用_getch()需要 #include <conio.h> unsigned int xtrans = 0; unsigned int ytrans = 0; switch (cinput) { case 87: case 119: { if (g_cursorPosy > 0) { ytrans = -1; } break; } case 53: case 115: { if (g_cursorPosy < MAP_HEIGHT - 1) { ytrans = 1; } break; } case 65: case 97: { if (g_cursorPosx > 0) { xtrans = -1; } break; } case 68: case 100: { if (g_cursorPosx < MAP_WIDTH - 1) { xtrans = 1; } break; } case 32: { //如果点开的位置是雷,返回信号GAME_OVER if (map[g_cursorPosx + 1][g_cursorPosy + 1].isMine) { return GAME_OVER_FAILED; } DFS(g_cursorPosx + 1, g_cursorPosy + 1); break; } default: { break; } } g_cursorPosx += xtrans; g_cursorPosy += ytrans; GotoPos(g_cursorPosx, g_cursorPosy); } return GAME_NOTH; } const int CheakSweeping() { if (g_numOfMines != g_isNotOpen) { return GAME_NOTH; } return GAME_OVER_SUCCESSED; } void GameOver(int type) { system("cls"); GotoPos(0, 0); for (int indexY = 1; indexY <= MAP_HEIGHT; ++indexY) { for (int indexX = 1; indexX <= MAP_WIDTH; ++indexX) { if (map[indexX][indexY].isMine) { cout << CHAR_MINE; } else { cout << (0 == map[indexX][indexY].countOfMines ? ' ' : char(map[indexX][indexY].countOfMines + 48)); } } cout << endl; } if (0 == type) { cout << "-Game Over-\n-Don't lose heart, you'll do better!-"; } else if (1 == type) { cout << "-Game Over-\n-Congratulations!-\n-You won!-"; } GotoPos(g_cursorPosx, g_cursorPosy); } int main() { srand(unsigned int(time(NULL))); //初始化随机数种子 int gameOverType; InitGame(); while (1) { if (GAME_OVER_FAILED == InputProcess()) { gameOverType = 0; break; }; if (GAME_OVER_SUCCESSED == CheakSweeping()) { gameOverType = 1; break; } Sleep(16); } GameOver(gameOverType); _getch(); return 0; }
文章持续更新中!
下节将开启图形化编程之旅~~~
求点赞、收藏!欢迎到评论区留言,有问必答!
作者水平有限,如果有误,欢迎指正!
编译环境:Visual Studio 2019
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。