实现一个控制台扫雷,玩家可以通过移动控制台光标来点开格子或者标记格子。
原版扫雷的格子上一共有三种标志,数字 旗子和问号。
数字标志从1~9,如果是0的话则显示空格。因为地图一般大部分是0,显示0的话不好看,原版的扫雷也没有显示0。
旗子用来表示玩家认为这个格子上有地雷。这里使用’@‘符号来表示旗子。
问号用来表示玩家不确定这个格子上是否有地雷。这里使用’?'符号来表示问号。
enum cell_flag
{
// 什么标志都没有
cf_none,
// 数字标志,显示周围雷的个数,格子被点开时自动被标记
cf_number,
// 旗子标志,标记你认为有地雷的格子
cf_flag,
// 问号标志,标记你不确定是否有雷的格子
cf_qmark
};
一个格子除了可能有标志以外,还需要有一个bool变量来存储这个格子上是否有地雷。
struct cell
{
// 这个格子有没有地雷
bool has_mine;
// 这个格子的标志
enum cell_flag flag;
};
首先是最大/最小值管理,游戏地图最大行数不超过20,最大列数不超过40,最小行数和列数不小于2。
其次是默认值管理,因为如果自定义地图的话,地图的大小以及地雷数就要设计得比较好才会好玩。如果地雷太少就会很容易,且地图大部分都是空的;太多就会很难。所以提供一个默认地图,让玩家可以不用自定义地图就能开玩。默认值是参考原版扫雷的高级模式。这里也可以把代码改成提供初级 中级和高级三种模式给玩家选择。
最后,使用current_rows
和current_cols
来存储当前地图的行列数,使用num_mines
来存储当前地图中的地雷数。定义一个struct cell
类型的二维数组来表示地图。
// 地图最大行数
#define MAX_ROWS 20
// 地图最大列数
#define MAX_COLS 40
// 地图最小行数
#define MIN_ROWS 2
// 地图最小列数
#define MIN_COLS 2
// 默认地图行数
#define DEFAULT_ROWS 16
// 默认地图列数
#define DEFAULT_COLS 30
// 默认地图中地雷数
#define DEFAULT_MINES 99
// 当前地图行数
int current_rows = DEFAULT_ROWS;
// 当前地图列数
int current_cols = DEFAULT_COLS;
// 地雷个数
int num_mines = DEFAULT_MINES;
// 地图
struct cell map[MAX_ROWS][MAX_COLS];
使用is_first_try
来存储是否是第一次点开格子,用于后续避免第一次点开就踩到地雷的情况。
使用is_game_over
来存储游戏是否失败。用于结束处理玩家键盘输入的循环。
// 是不是第一次打开格子
// 用来防止第一下就踩雷
bool is_first_try = true;
// 游戏是否结束
bool is_game_over = false;
首先,清空控制台,因为如果是第二局游戏的话,控制台上会有上一局游戏的内容。
然后,由玩家输入自定义地图信息,或者选择使用默认地图。
其次,根据地图信息,即地图的行列数和地雷数,初始化地图。
其次,随机抽取地图上的格子,生成地雷。
其次,在控制台上绘制地图。
其次,打印游戏的帮助信息,包括如何操作,以及地图信息。
最后,循环接收玩家输入,执行输入相对应的操作。
int main()
{
// 游戏主循环
while (true)
{
// 清空控制台
system("cls");
// 输入地图信息
input_map();
// 初始化地图
init_map();
// 生成地雷
generate_mines();
// 绘制地图
draw_map();
// 输出帮助信息
print_help();
// 处理玩家的键盘输入
handle_player_input();
}
}
首先我们需要一个tmp_buf
变量来接收输入错误后剩余的字符串,或者正常输入后的回车字符。因为使用scanf
输入之后,可能会有残留的字符串(输入错误的情况下)或者回车键在输入缓冲区中,不进行处理的话后续的输入(第二局游戏的时候)会出现错误。
这个函数主要是让玩家选择是否使用默认地图,或者自定义地图。
如果自定义地图,则输入地图行列数以及地雷数。如果输入格式错误,比如应当输入数字却输入字母,则提示重新输入。
同时还会检测输入的地图信息是否合理,即输入的行列数不能超过最大值 小于最小值,以及至少有一个地雷,不能整个地图都是地雷。
void input_map()
{
// 用来接收输入错误后剩余的字符串,或者正常输入后的回车字符
char tmp_buf[256] = { 0 };
printf("默认地图:16行30列,99颗地雷\n");
printf("使用默认地图(y/n)?:");
char ch = 0;
// 判断是否使用默认地图
if (scanf("%c", &ch) == 1 && (ch == 'y' || ch == 'Y'))
{
// 设置默认行数 列数和地雷数
current_rows = DEFAULT_ROWS;
current_cols = DEFAULT_COLS;
num_mines = DEFAULT_MINES;
// 接收多余字符(回车)
fgets(tmp_buf, sizeof(tmp_buf), stdin);
return;
}
// 接收多余字符(回车)
fgets(tmp_buf, sizeof(tmp_buf), stdin);
// 输入地图大小
printf("请输入地图大小:");
while (scanf("%d%d", ¤t_rows, ¤t_cols) != 2)
{
// 输入错误,重新输入
fprintf(stderr, "输入地图大小格式错误!\n");
// 接收多余字符
fgets(tmp_buf, sizeof(tmp_buf), stdin);
printf("请重新输入:");
}
fgets(tmp_buf, sizeof(tmp_buf), stdin);
// 检查并更正地图大小
if (current_rows < MIN_ROWS)current_rows = MIN_ROWS;
else if (current_rows > MAX_ROWS) current_rows = MAX_ROWS;
if (current_cols < MIN_COLS)current_cols = MIN_COLS;
else if (current_cols > MAX_COLS)current_cols = MAX_COLS;
// 输入地雷个数
printf("请输入地雷个数:");
while (scanf("%d", &num_mines) != 1)
{
// 输入错误,重新输入
fprintf(stderr, "输入地雷个数格式错误!\n");
// 接收多余字符
fgets(tmp_buf, sizeof(tmp_buf), stdin);
printf("请重新输入:");
}
// 检查并更正地雷个数
int max_mines = current_rows * current_cols - 1;
if (num_mines <= 0)num_mines = 1;
else if (num_mines >= max_mines) num_mines = max_mines;
// 接收多余字符(回车)
fgets(tmp_buf, sizeof(tmp_buf), stdin);
}
这个函数首先会将游戏的关键变量is_first_try
和is_gmae_over
重置为初始值,然后会遍历每一个格子,然后将格子的地雷和标志清空。
void init_map()
{
// 下一次点开格子是第一次点开格子
is_first_try = true;
// 游戏结束标志
is_game_over = false;
// 遍历每一行
for (int i = 0; i < current_rows; ++i)
{
// 遍历每一列
for (int j = 0; j < current_cols; ++j)
{
// 清空地雷
map[i][j].has_mine = false;
// 清空标志
map[i][j].flag = cf_none;
}
}
}
这个函数首先会先设置随机数种子,避免每一次游戏地图都一样。
然后会进入一个while
循环,循环体中每次随机取一个格子,如果格子没有地雷就生成地雷,如果已经有地雷了,就重来,直到没有地雷需要生成了。这个生成算法还可以进行优化的。
void generate_mines()
{
// 设置随机数种子
srand(time(0));
// 每次生成一个地雷
// 直到待生成的地雷数为0
int mines_to_generate = num_mines;
while (mines_to_generate > 0)
{
// 随机取地图的一行
int row = rand() % current_rows;
// 随机取地图的一列
int col = rand() % current_cols;
// 获取该行该列的格子
struct cell* cell = &map[row][col];
// 如果这个格子已经有地雷了
if (cell->has_mine)
{
// 那么就跳过这个格子,重新找
continue;
}
// 否则的话
else
{
// 生成地雷
cell->has_mine = true;
// 并将待生成的地雷数减一
mines_to_generate--;
}
}
}
这个函数会先清空控制台,即清除之前输入地图信息时的输入,然后遍历每一个格子,打印格子符号,初始都是没有任何标志的符号,即’*'号。同时也会打印地图边界,因为周围没有地雷时的数字是空格不是0,所以如果没有边界的话,当边缘周围没有地雷时看上去会不好看。
void draw_map()
{
// 清空控制台
system("cls");
// 遍历每一行
for (int i = 0; i < current_rows; ++i)
{
// 遍历每一列
for (int j = 0; j < current_cols; ++j)
{
// *表示一个没有任何标志的格子
printf("*");
// *后面跟一个空格保持地图美观
// 保证行数和列数相等时是个正方形
if (j != current_cols - 1)printf(" ");
}
// 打印右边界并且换行
printf("|\n");
}
// 打印底边界
for (int i = 0; i < current_cols; ++i)
printf("--");
// 删掉最后一个'-'并增加边界然后换行
printf("\b|\n");
}
首先我们需要先控制光标在控制台上的位置。这里我们使用Win32提供的相关接口GetConsoleScreenBufferInfo
以及SetConsoleCursorPosition
来获取/设置光标的位置。get_cursor_xy
和set_cursor_xy
用来直接获取/设置光标在控制台上的位置。set_cursor_rc
用来将光标移动到指定的地图行列位置。因为之前为了地图美观,每一个’*'号之后都跟着空格,所以计算x
时需要将列数乘以2。print_at_rc
用来在指定的地图行列位置输出一个字符,同时保持光标位置。因为printf
会改变光标位置,所以需要在printf
之后恢复光标位置。
// 获取光标所在位置的x y值
void get_cursor_xy(int* x, int* y)
{
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO csbi;
GetConsoleScreenBufferInfo(handle, &csbi);
*x = csbi.dwCursorPosition.X;
*y = csbi.dwCursorPosition.Y;
}
// 将光标移至位置控制台(x, y)处
void set_cursor_xy(int x, int y)
{
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
COORD coord = { x, y };
SetConsoleCursorPosition(handle, coord);
}
// 将光标移至地图的第row行的第col列
void set_cursor_rc(int row, int col)
{
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
COORD coord = {col * 2, row};
SetConsoleCursorPosition(handle, coord);
}
// 在地图的第row行的第col列打印字符ch
void print_at_rc(char ch, int row, int col)
{
set_cursor_rc(row, col);
printf("%c", ch);
set_cursor_rc(row, col);
}
首先需要先保存一开始的光标位置。因为一开始的时候光标应该是在输出的最后位置。
然后将光标移到地图的第一个格子,之后进入循环。
循环条件是游戏没有失败,如果踩到地雷,则游戏失败,结束循环。
循环体内通过_getch()
来得到输入字符,使用这个函数是因为这个函数没有回显。
拿得输入字符后,对字符进行判断。
ESC键的数值是0x1b
,如果是ESC键的话则直接返回。直接返回即可重新开始。
如果是wasd
键,则修改row
或者col
变量,然后还需要检测范围,将光标限制在地图之内,再调用set_cursor_rc
来移动光标至对应格子。
如果是jkli
键,则执行对应操作:点开格子/自动点开邻居/进行标记。
在处理完操作后,判断是否满足胜利条件,胜利条件是所有没有地雷的格子都点开了,所有有地雷的格子都标记成旗子。
胜利或者失败都会结束循环,然后将光标恢复到最后,打印结束语,询问是否再来一局。
// 接受玩家的键盘输入,并进行处理
void handle_player_input()
{
// 先获取光标当前的x y值
// 便于游戏结束时在最后位置输出信息
int x, y;
get_cursor_xy(&x, &y);
// 将光标移至控制台左上角
// 即地图的第一个格子
set_cursor_rc(0, 0);
int row = 0, col = 0;
// 循环直到踩到地雷或者胜利
while (!is_game_over)
{
int ch = _getch();
// 0x1b表示ESC键
// 重新开始
if (ch == 0x1b)
{
return;
}
else
{
switch (ch)
{
case 'w':// 上移
--row;
break;
case 'a':// 左移
--col;
break;
case 's':// 下移
++row;
break;
case 'd':// 右移
++col;
break;
case 'j':// 点开格子(模拟鼠标左键)
player_open_cell(row, col);
break;
case 'k':// 尝试自动点开邻居(模拟鼠标中键)
player_open_neighbor(row, col);
break;
case 'l':// 添加旗子标志(模拟鼠标右键)
player_mark_flag(row, col);
break;
case 'i':// 添加问号标志
player_mark_question(row, col);
break;
}
}
// 将光标限制在地图之内
if (col < 0)col = 0;
else if (col >= current_cols) col = current_cols - 1;
if (row < 0)row = 0;
else if (row >= current_rows) row = current_rows - 1;
// 移动光标至对应位置
set_cursor_rc(row, col);
// 如果没有踩到地雷,判断是否胜利
if (!is_game_over)
{
bool win_flag = true;
// 遍历每一个格子
for (int i = 0; i < current_rows; ++i)
{
for (int j = 0; j < current_cols; ++j)
{
// 如果有任何一个格子没有标志或者有问号标志
// 或者有错误的旗子标志(即在没有地雷的格子标记了旗子)
if (map[i][j].flag == cf_none || map[i][j].flag == cf_qmark
|| (map[i][j].flag == cf_flag && !map[i][j].has_mine))
{
// 那么还未胜利
win_flag = false;
break;
}
}
// 跳出循环
if (!win_flag)break;
}
// 如果胜利了,跳出整个while循环
if (win_flag)break;
}
}
// 恢复光标位置到最后
set_cursor_xy(x, y);
printf("\n");
// 打印结束语
if (is_game_over)printf("你踩到雷了!\n");
else printf("你赢了!\n");
// 判断是否继续游戏
printf("继续游戏(y/n)?:");
char ch;
scanf("%c", &ch);
// 接收回车键
getchar();
if (ch == 'y' || ch == 'Y')return;
else exit(0);
}
在介绍玩家操作前,需要先介绍一个关键宏,以及一个关键函数。
这个宏的用途是遍历一个格子的每一个邻居,包括它自身。定义这个宏是因为在后续的过程中会经常出现遍历邻居的操作,因此在这里定义一个宏可以简化后续的代码。
宏参数中,r
和c
是当前格子的行列索引,nr
和nc
是邻居格子的行列索引,_cell
是邻居格子。
在遍历邻居的过程中也会进行范围检测,避免超出地图边界。
// 遍历每个邻居,包括自己
#define ITERATE_NEIGHBOR_BEGIN(r, c, nr, nc, _cell) \
{\
int _d[3] = {-1, 0, 1};\
for(int _i = 0; _i < 3; ++_i)\
{\
for(int _j = 0; _j < 3; ++_j)\
{\
int nr = r + _d[_i];\
int nc = c + _d[_j];\
if(nr >= 0 && nc >= 0 && nr < current_rows && nc < current_cols)\
{\
struct cell* _cell = &map[nr][nc];
#define ITERATE_NEIGHBOR_END \
}\
}\
}\
}
在后续的过程中会多次用到一个格子周围的地雷数,因此提供这样一个函数来获取周围地雷数。
这个函数会利用上面定义的宏来遍历邻居,然后对地雷进行计数。
// 获取一个格子周围的地雷个数(包括自身)
int get_num_mines_around(int row, int col)
{
int num = 0;
// 遍历每一个邻居
ITERATE_NEIGHBOR_BEGIN(row, col, nr, nc, cell)
{
// 如果有地雷,那地雷数加1
if (cell->has_mine)num++;
}
ITERATE_NEIGHBOR_END;
return num;
}
这个函数执行点开格子的操作,如果格子已经被点开了,那么就直接返回。
否则判断是否有地雷,如果有地雷,则判断是否是第一次点开,如果是的话,则调用handle_first_mine
来找一个没有地雷的格子进行替换。
后面继续判断这个格子有没有地雷(因为可能找不到格子可以替换),如果有地雷,则游戏失败,将is_game_over
置为true
并返回。
如果没有地雷,则获取周围地雷数,打印对应数字字符或者空格。如果没有地雷,则递归点开邻居。
void player_open_cell(int row, int col)
{
struct cell* cell = &map[row][col];
// 已经打开的格子不用再打开
if (cell->flag == cf_number)
return;
// 点开的格子有地雷
if (cell->has_mine)
{
// 如果是一开始就踩雷
if (is_first_try)
// 找一个没有地雷的格子进行交换
handle_first_mine(row, col);
// 不是一开始就踩雷或者
// 一开始就踩雷但没有找到一个可以替换的格子
if(map[row][col].has_mine)
{
// 游戏结束
print_at_rc('!', row, col);
is_game_over = true;
return;
//exit(0);
}
}
// 不再是一开始了
is_first_try = false;
// 设置格子的标志
cell->flag = cf_number;
// 获取周围的地雷数
int nums = get_num_mines_around(row, col);
// 如果周围没有地雷,打印空格
// 打印0的话会太密集,因为没有地雷的区域一般会比较多
// 原本的扫雷里面也是没有打印0的
if (nums == 0)print_at_rc(' ', row, col);
// 非0的话打印对应的数字符号
else print_at_rc(nums + '0', row, col);
// 同时,如果周围没有地雷
if (nums == 0)
{
// 遍历每一个邻居(包括自己)
ITERATE_NEIGHBOR_BEGIN(row, col, nr, nc, cell)
{
// 如果邻居还没有点开标志
if (cell->flag != cf_number)
// 那么就点开这个邻居
player_open_cell(nr, nc);
}
ITERATE_NEIGHBOR_END;
}
}
在第一次点开格子就踩到地雷时,会调用这个函数来找一个没有地雷而且没有标志的格子进行替换。
// 处理第一次就踩雷的情况
void handle_first_mine(int row, int col)
{
// 遍历每一个格子
for (int i = 0; i < current_rows; ++i)
{
for (int j = 0; j < current_cols; ++j)
{
struct cell* cell = &map[i][j];
// 如果这个格子没有地雷,而且也没有标志
if (!cell->has_mine && cell->flag == cf_none)
{
// 将这个格子和踩到的地雷格子进行交换
struct cell tmp = map[row][col];
map[row][col] = map[i][j];
map[i][j] = tmp;
}
}
}
// 后续踩到地雷不会再进行这样的处理
is_first_try = false;
}
这个函数执行自动点开邻居的操作。在原版的扫雷中,如果我们对已经排掉所有雷的格子点击鼠标中键的话,格子的没有被点开的邻居会被自动点开。
首先我们需要先判断这个格子已经是点开了的,然后获取格子周围的地雷数,遍历每个邻居,对标记了旗子的邻居进行计数。如果周围地雷数和标记为旗子的数目相同,则表示地雷全部排除。然后遍历所有没有被点开的邻居,点开。
void player_open_neighbor(int row, int col)
{
struct cell* cell = &map[row][col];
// 只有自己已经被点开了
// 才能尝试自动去点开邻居
if (cell->flag == cf_number)
{
// 先获取周围的雷数
int nm = get_num_mines_around(row, col);
// 标记为cf_flag的格子数
int nf = 0;
// 遍历每个邻居,包括自己
ITERATE_NEIGHBOR_BEGIN(row, col, nr, nc, cell)
{
// 遇到被标记为旗子的格子,nf增加1
if (cell->flag == cf_flag)
nf++;
}
ITERATE_NEIGHBOR_END;
// 如果被标记为旗子的格子的数目
// 与周围地雷的数目相同
if (nf == nm)
{
// 那么遍历每个邻居
ITERATE_NEIGHBOR_BEGIN(row, col, nr, nc, cell)
{
// 点开那些没有被点开的格子
if (cell->flag == cf_none || cell->flag == cf_qmark)
player_open_cell(nr, nc);
}
ITERATE_NEIGHBOR_END;
}
}
}
这两个函数用来对格子进行标记,包括标记旗子和问号。当重复操作时,会消去标志。
// 给格子增加旗子标志
void player_mark_flag(int row, int col)
{
struct cell* cell = &map[row][col];
// 已经点开的格子不能进行标记
if (cell->flag != cf_number)
{
// 如果已经是旗子标志了
if (cell->flag == cf_flag)
{
// 去掉旗子标志
cell->flag = cf_none;
print_at_rc('*', row, col);
}
// 否则的话
else
{
// 加上或者改成旗子标志
cell->flag = cf_flag;
print_at_rc('@', row, col);
}
}
}
// 给格子增加问号标志
void player_mark_question(int row, int col)
{
struct cell* cell = &map[row][col];
// 已经点开的格子不能进行标记
if (cell->flag != cf_number)
{
// 如果已经是问号标志了
if (cell->flag == cf_qmark)
{
// 去掉问号标志
cell->flag = cf_none;
print_at_rc('*', row, col);
}
// 否则的话
else
{
// 加上或者改成问号标志
cell->flag = cf_qmark;
print_at_rc('?', row, col);
}
}
}
#define _CRT_SECURE_NO_WARNINGS // 不使用_s系列函数
#include // 用于控制光标
#include // 用于控制光标
#include
#include
#include
#include
#include
// 格子上的标志
enum cell_flag
{
// 什么标志都没有
cf_none,
// 数字标志,显示周围雷的个数,格子被点开时自动被标记
cf_number,
// 旗子标志,标记你认为有地雷的格子
cf_flag,
// 问号标志,标记你不确定是否有雷的格子
cf_qmark
};
// 地图上的每个格子
struct cell
{
// 这个格子有没有地雷
bool has_mine;
// 这个格子的标志
enum cell_flag flag;
};
// 地图最大行数
#define MAX_ROWS 20
// 地图最大列数
#define MAX_COLS 40
// 地图最小行数
#define MIN_ROWS 2
// 地图最小列数
#define MIN_COLS 2
// 默认地图行数
#define DEFAULT_ROWS 16
// 默认地图列数
#define DEFAULT_COLS 30
// 默认地图中地雷数
#define DEFAULT_MINES 99
// 当前地图行数
int current_rows = DEFAULT_ROWS;
// 当前地图列数
int current_cols = DEFAULT_COLS;
// 地雷个数
int num_mines = DEFAULT_MINES;
// 地图
struct cell map[MAX_ROWS][MAX_COLS];
// 是不是第一次打开格子
// 用来防止第一下就踩雷
bool is_first_try = true;
// 游戏是否结束
bool is_game_over = false;
// 获取一个格子周围的地雷数
int get_num_mines_around(int row, int col);
// 1. 输入地图信息:大小 地雷个数
void input_map();
// 2. 初始化地图:清空地雷和所有标志
void init_map();
// 3. 生成地雷
void generate_mines();
// 4. 绘制地图
void draw_map();
// 5. 输出帮助信息
void print_help();
// 6. 获取/设置光标位置,在指定位置输出符号
void get_cursor_xy(int* x, int* y);
void set_cursor_rc(int row, int col);
void print_at_rc(char ch, int row, int col);
// 7. 处理玩家的键盘输入
void handle_player_input();
// 7.1 处理第一次点开格子是地雷的情况
void handle_first_mine(int row, int col);
// 7.2 按下J键后点开格子
void player_open_cell(int row, int col);
// 7.3 按下K键后自动点开周围格子
void player_open_neighbor(int row, int col);
// 7.4 按下L键后插旗子
void player_mark_flag(int row, int col);
// 7.5 按下I键后打问号
void player_mark_question(int row, int col);
// 入口函数
int main()
{
// 游戏主循环
while (true)
{
// 清空控制台
system("cls");
// 输入地图信息
input_map();
// 初始化地图
init_map();
// 生成地雷
generate_mines();
// 绘制地图
draw_map();
// 输出帮助信息
print_help();
// 处理玩家的键盘输入
handle_player_input();
}
}
// 遍历每个邻居,包括自己
#define ITERATE_NEIGHBOR_BEGIN(r, c, nr, nc, _cell) \
{\
int _d[3] = {-1, 0, 1};\
for(int _i = 0; _i < 3; ++_i)\
{\
for(int _j = 0; _j < 3; ++_j)\
{\
int nr = r + _d[_i];\
int nc = c + _d[_j];\
if(nr >= 0 && nc >= 0 && nr < current_rows && nc < current_cols)\
{\
struct cell* _cell = &map[nr][nc];
#define ITERATE_NEIGHBOR_END \
}\
}\
}\
}
// 获取一个格子周围的地雷个数(包括自身)
int get_num_mines_around(int row, int col)
{
int num = 0;
// 遍历每一个邻居
ITERATE_NEIGHBOR_BEGIN(row, col, nr, nc, cell)
{
// 如果有地雷,那地雷数加1
if (cell->has_mine)num++;
}
ITERATE_NEIGHBOR_END;
return num;
}
// 输入地图信息
void input_map()
{
// 用来接收输入错误后剩余的字符串,或者正常输入后的回车字符
char tmp_buf[256] = { 0 };
printf("默认地图:16行30列,99颗地雷\n");
printf("使用默认地图(y/n)?:");
char ch = 0;
// 判断是否使用默认地图
if (scanf("%c", &ch) == 1 && (ch == 'y' || ch == 'Y'))
{
// 设置默认行数 列数和地雷数
current_rows = DEFAULT_ROWS;
current_cols = DEFAULT_COLS;
num_mines = DEFAULT_MINES;
// 接收多余字符(回车)
fgets(tmp_buf, sizeof(tmp_buf), stdin);
return;
}
// 接收多余字符(回车)
fgets(tmp_buf, sizeof(tmp_buf), stdin);
// 输入地图大小
printf("请输入地图大小:");
while (scanf("%d%d", ¤t_rows, ¤t_cols) != 2)
{
// 输入错误,重新输入
fprintf(stderr, "输入地图大小格式错误!\n");
// 接收多余字符
fgets(tmp_buf, sizeof(tmp_buf), stdin);
printf("请重新输入:");
}
fgets(tmp_buf, sizeof(tmp_buf), stdin);
// 检查并更正地图大小
if (current_rows < MIN_ROWS)current_rows = MIN_ROWS;
else if (current_rows > MAX_ROWS) current_rows = MAX_ROWS;
if (current_cols < MIN_COLS)current_cols = MIN_COLS;
else if (current_cols > MAX_COLS)current_cols = MAX_COLS;
// 输入地雷个数
printf("请输入地雷个数:");
while (scanf("%d", &num_mines) != 1)
{
// 输入错误,重新输入
fprintf(stderr, "输入地雷个数格式错误!\n");
// 接收多余字符
fgets(tmp_buf, sizeof(tmp_buf), stdin);
printf("请重新输入:");
}
// 检查并更正地雷个数
int max_mines = current_rows * current_cols - 1;
if (num_mines <= 0)num_mines = 1;
else if (num_mines >= max_mines) num_mines = max_mines;
// 接收多余字符(回车)
fgets(tmp_buf, sizeof(tmp_buf), stdin);
}
// 初始化地图
void init_map()
{
// 下一次点开格子是第一次点开格子
is_first_try = true;
// 游戏结束标志
is_game_over = false;
// 遍历每一行
for (int i = 0; i < current_rows; ++i)
{
// 遍历每一列
for (int j = 0; j < current_cols; ++j)
{
// 清空地雷
map[i][j].has_mine = false;
// 清空标志
map[i][j].flag = cf_none;
}
}
}
// 生成地雷
void generate_mines()
{
// 设置随机数种子
srand(time(0));
// 每次生成一个地雷
// 直到待生成的地雷数为0
int mines_to_generate = num_mines;
while (mines_to_generate > 0)
{
// 随机取地图的一行
int row = rand() % current_rows;
// 随机取地图的一列
int col = rand() % current_cols;
// 获取该行该列的格子
struct cell* cell = &map[row][col];
// 如果这个格子已经有地雷了
if (cell->has_mine)
{
// 那么就跳过这个格子,重新找
continue;
}
// 否则的话
else
{
// 生成地雷
cell->has_mine = true;
// 并将待生成的地雷数减一
mines_to_generate--;
}
}
}
// 绘制地图
void draw_map()
{
// 清空控制台
system("cls");
// 遍历每一行
for (int i = 0; i < current_rows; ++i)
{
// 遍历每一列
for (int j = 0; j < current_cols; ++j)
{
// *表示一个没有任何标志的格子
printf("*");
// *后面跟一个空格保持地图美观
// 保证行数和列数相等时是个正方形
if (j != current_cols - 1)printf(" ");
}
// 打印右边界并且换行
printf("|\n");
}
// 打印底边界
for (int i = 0; i < current_cols; ++i)
printf("--");
// 删掉最后一个'-'并增加边界然后换行
printf("\b|\n");
}
// 打印帮助信息
void print_help()
{
// 打印操作方式和地图信息
printf(
"\n[操作方式]\n"
"WASD键:移动光标\t"
"J键:点开格子\n"
"K键:自动排雷\t\t"
"L键:插旗子\n"
"I键:打问号\t\t"
"ESC键:重新开始\n"
"\n[地图信息]\n"
"地图大小:%d行%d列\t"
"地雷总数:%d\n",
current_rows,
current_cols,
num_mines
);
}
// 获取光标所在位置的x y值
void get_cursor_xy(int* x, int* y)
{
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO csbi;
GetConsoleScreenBufferInfo(handle, &csbi);
*x = csbi.dwCursorPosition.X;
*y = csbi.dwCursorPosition.Y;
}
// 将光标移至位置控制台(x, y)处
void set_cursor_xy(int x, int y)
{
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
COORD coord = { x, y };
SetConsoleCursorPosition(handle, coord);
}
// 将光标移至地图的第row行的第col列
void set_cursor_rc(int row, int col)
{
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
COORD coord = {col * 2, row};
SetConsoleCursorPosition(handle, coord);
}
// 在地图的第row行的第col列打印字符ch
void print_at_rc(char ch, int row, int col)
{
set_cursor_rc(row, col);
printf("%c", ch);
set_cursor_rc(row, col);
}
// 接受玩家的键盘输入,并进行处理
void handle_player_input()
{
// 先获取光标当前的x y值
// 便于游戏结束时在最后位置输出信息
int x, y;
get_cursor_xy(&x, &y);
// 将光标移至控制台左上角
// 即地图的第一个格子
set_cursor_rc(0, 0);
int row = 0, col = 0;
// 循环直到踩到地雷或者胜利
while (!is_game_over)
{
int ch = _getch();
// 0x1b表示ESC键
// 重新开始
if (ch == 0x1b)
{
return;
}
else
{
switch (ch)
{
case 'w':// 上移
--row;
break;
case 'a':// 左移
--col;
break;
case 's':// 下移
++row;
break;
case 'd':// 右移
++col;
break;
case 'j':// 点开格子(模拟鼠标左键)
player_open_cell(row, col);
break;
case 'k':// 尝试自动点开邻居(模拟鼠标中键)
player_open_neighbor(row, col);
break;
case 'l':// 添加旗子标志(模拟鼠标右键)
player_mark_flag(row, col);
break;
case 'i':// 添加问号标志
player_mark_question(row, col);
break;
}
}
// 将光标限制在地图之内
if (col < 0)col = 0;
else if (col >= current_cols) col = current_cols - 1;
if (row < 0)row = 0;
else if (row >= current_rows) row = current_rows - 1;
// 移动光标至对应位置
set_cursor_rc(row, col);
// 如果没有踩到地雷,判断是否胜利
if (!is_game_over)
{
bool win_flag = true;
// 遍历每一个格子
for (int i = 0; i < current_rows; ++i)
{
for (int j = 0; j < current_cols; ++j)
{
// 如果有任何一个格子没有标志或者有问号标志
// 或者有错误的旗子标志(即在没有地雷的格子标记了旗子)
if (map[i][j].flag == cf_none || map[i][j].flag == cf_qmark
|| (map[i][j].flag == cf_flag && !map[i][j].has_mine))
{
// 那么还未胜利
win_flag = false;
break;
}
}
// 跳出循环
if (!win_flag)break;
}
// 如果胜利了,跳出整个while循环
if (win_flag)break;
}
}
// 恢复光标位置到最后
set_cursor_xy(x, y);
printf("\n");
// 打印结束语
if (is_game_over)printf("你踩到雷了!\n");
else printf("你赢了!\n");
// 判断是否继续游戏
printf("继续游戏(y/n)?:");
char ch;
scanf("%c", &ch);
// 接收回车键
getchar();
if (ch == 'y' || ch == 'Y')return;
else exit(0);
}
// 处理第一次就踩雷的情况
void handle_first_mine(int row, int col)
{
// 遍历每一个格子
for (int i = 0; i < current_rows; ++i)
{
for (int j = 0; j < current_cols; ++j)
{
struct cell* cell = &map[i][j];
// 如果这个格子没有地雷,而且也没有标志
if (!cell->has_mine && cell->flag == cf_none)
{
// 将这个格子和踩到的地雷格子进行交换
struct cell tmp = map[row][col];
map[row][col] = map[i][j];
map[i][j] = tmp;
}
}
}
// 后续踩到地雷不会再进行这样的处理
is_first_try = false;
}
void player_open_cell(int row, int col)
{
struct cell* cell = &map[row][col];
// 已经打开的格子不用再打开
if (cell->flag == cf_number)
return;
// 点开的格子有地雷
if (cell->has_mine)
{
// 如果是一开始就踩雷
if (is_first_try)
// 找一个没有地雷的格子进行交换
handle_first_mine(row, col);
// 不是一开始就踩雷或者
// 一开始就踩雷但没有找到一个可以替换的格子
if(map[row][col].has_mine)
{
// 游戏结束
print_at_rc('!', row, col);
is_game_over = true;
return;
//exit(0);
}
}
// 不再是一开始了
is_first_try = false;
// 设置格子的标志
cell->flag = cf_number;
// 获取周围的地雷数
int nums = get_num_mines_around(row, col);
// 如果周围没有地雷,打印空格
// 打印0的话会太密集,因为没有地雷的区域一般会比较多
// 原本的扫雷里面也是没有打印0的
if (nums == 0)print_at_rc(' ', row, col);
// 非0的话打印对应的数字符号
else print_at_rc(nums + '0', row, col);
// 同时,如果周围没有地雷
if (nums == 0)
{
// 遍历每一个邻居(包括自己)
ITERATE_NEIGHBOR_BEGIN(row, col, nr, nc, cell)
{
// 如果邻居还没有点开标志
if (cell->flag != cf_number)
// 那么就点开这个邻居
player_open_cell(nr, nc);
}
ITERATE_NEIGHBOR_END;
}
}
// 根据标记情况,自动点开周围格子
void player_open_neighbor(int row, int col)
{
struct cell* cell = &map[row][col];
// 只有自己已经被点开了
// 才能尝试自动去点开邻居
if (cell->flag == cf_number)
{
// 先获取周围的雷数
int nm = get_num_mines_around(row, col);
// 标记为cf_flag的格子数
int nf = 0;
// 遍历每个邻居,包括自己
ITERATE_NEIGHBOR_BEGIN(row, col, nr, nc, cell)
{
// 遇到被标记为旗子的格子,nf增加1
if (cell->flag == cf_flag)
nf++;
}
ITERATE_NEIGHBOR_END;
// 如果被标记为旗子的格子的数目
// 与周围地雷的数目相同
if (nf == nm)
{
// 那么遍历每个邻居
ITERATE_NEIGHBOR_BEGIN(row, col, nr, nc, cell)
{
// 点开那些没有被点开的格子
if (cell->flag == cf_none || cell->flag == cf_qmark)
player_open_cell(nr, nc);
}
ITERATE_NEIGHBOR_END;
}
}
}
// 给格子增加旗子标志
void player_mark_flag(int row, int col)
{
struct cell* cell = &map[row][col];
// 已经点开的格子不能进行标记
if (cell->flag != cf_number)
{
// 如果已经是旗子标志了
if (cell->flag == cf_flag)
{
// 去掉旗子标志
cell->flag = cf_none;
print_at_rc('*', row, col);
}
// 否则的话
else
{
// 加上或者改成旗子标志
cell->flag = cf_flag;
print_at_rc('@', row, col);
}
}
}
// 给格子增加问号标志
void player_mark_question(int row, int col)
{
struct cell* cell = &map[row][col];
// 已经点开的格子不能进行标记
if (cell->flag != cf_number)
{
// 如果已经是问号标志了
if (cell->flag == cf_qmark)
{
// 去掉问号标志
cell->flag = cf_none;
print_at_rc('*', row, col);
}
// 否则的话
else
{
// 加上或者改成问号标志
cell->flag = cf_qmark;
print_at_rc('?', row, col);
}
}
}