当前位置:   article > 正文

c/c++游戏编程之扫雷_用c++做扫雷

用c++做扫雷

(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
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

看过前几篇文章的同学都应该知道,游戏程序的流程大致可以分成三个部分:

1.游戏初始化阶段:用于加载资源,生成地图数据等操作

2.游戏运行阶段:主要是由一个循环负责,在循环里做输入处理、状态更新、图像绘制等操作

3.游戏结束阶段:可以用于释放资源,输出结束信息等操作

先定义一个名为GotoPos的函数来移动光标在控制台中的位置(窗口坐标系我在前面的文章有过介绍,这里不再赘述),这个函数尤其重要:

void GotoPos(int x, int y) {
	HANDLE hout = GetStdHandle(STD_OUTPUT_HANDLE);
	COORD cor = { x, y };
	SetConsoleCursorPosition(hout, cor);
}
  • 1
  • 2
  • 3
  • 4
  • 5

好了,话不多说,先来第一步:游戏初始化。我们用一个名为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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

我们用随机数生成地雷,且生成的地雷的坐标在 [1, MAP_WIDTH] 这个闭区间内。我们为地雷附近的格子的地雷计数加一:

在这里插入图片描述

但是,当地雷生成到地图边界时,为防止越界访问,我们就不得不判断其周围还有没有格子:

在这里插入图片描述

为了避免作出多余的判断,我们将二维数组扩大"一圈",这也是代码中为什么定义map时数组尺寸是MAP_WIDTH + 2MAP_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); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

我们用字符?’代表格子,并遍历所有格子把它们的状态置为关闭。这便是游戏初始化的所有内容。

我们再用非阻塞的方式获取并处理键盘输入(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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57

上述代码中的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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

我们以递归的方式进行深度优先搜索,即从打开的位置向上下左右四个方向继续打开,如果遇到了周围有雷的格子,打开它,然后不再继续打开直接返回。在InputProcess函数的case 32中,我们可以看到,如果这个格子是雷,便直接返回游戏结束标识。

在这里插入图片描述

如果不是雷,就调用DFS函数。在DFS函数里,如果这个格子周围有地雷,就直接打开它:

在这里插入图片描述

如果这个格子周围没有地雷,就继续递归搜索,疯狂打开,直到遇到有地雷的格子:

在这里插入图片描述

然后定义一个CheckSweeping函数,用来检测玩家是否扫完了所有的雷,其原理很简单,就是判断剩下的未打开的格子数量是否与地雷数量相等,如果相等,就说明游戏扫雷成功。

const int CheakSweeping() {
	
	if (g_numOfMines != g_isNotOpen) {
		return GAME_NOTH;
	}

	return GAME_OVER_SUCCESSED;

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

最后定义一个游戏结束的函数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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

到这里,游戏核心代码全部完成啦~,以下是所有代码

#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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214

文章持续更新中!
下节将开启图形化编程之旅~~~

求点赞、收藏!欢迎到评论区留言,有问必答!
作者水平有限,如果有误,欢迎指正!
编译环境:Visual Studio 2019

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/90848
推荐阅读
相关标签
  

闽ICP备14008679号