扫雷游戏

从上微机课开始,在没有网而且啥也不会的情况下,扫雷成了打发时间的唯一乐趣,玩过没写过,今天来试试;

需要实现的功能

  1. 显示周围区域 💣 雷的个数;
  2. 周围没有雷则自动展开;
  3. 剩下的全部为雷则自动胜出;

游戏实现的思路

0. 逻辑分析
  1. 布置雷,由于既需要存放布置雷的数据,又要展示隐藏雷后的数据给玩家,所以需要两个 char 数组,分别存放相应的数据;
  2. 排查雷,玩家输入坐标,如果是雷,结束游戏,如果不是雷,则显示周围雷的个数,如果周围区域都没有雷,则自动展开空白区域;
  3. 判定是否自动胜出;
1. main 函数

在 main 函数中写出程序的整体框架,然后再具体实现:

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
#include <stdio.h> //输入输出
#include <stdlib.h>//随机数
#include <time.h>//随机数种子需要的时间

//要显示行和列
#define ROW 9
#define COL 9
//实际的行和列
#define ROWS ROW + 2
#define COLS COL + 2
//雷的个数
#define MINES 10

//显示菜单
void display_menu();
//游戏开始
void play_game(char mine[ROWS][COLS], char show[ROWS][COLS]);

int main()
{
//存放布置雷的数据
char mine[ROWS][COLS] = {0};
//存放游戏数据
char show[ROWS][COLS] = {0};
//用户输入
int input = 0;

//菜单至少会显示一次,do while 正合适
do
{
//显示菜单
display_menu();
//提示用户输入
printf("请选择:>");
//获取用户输入
scanf("%d", &input);

//根据用户输入确定程序走向
switch (input)
{
case 1: //输入 1 表示开始游戏
play_game(mine, show);
break;
case 0: //输入 0 表示结束游戏
printf("退出游戏!\n");
break;
default: //非 0 非 1 不正确,重新输入
printf("输入有误,请重新输入!\n");
break;
}
} while (input); // 0 退出,非 0 为真,刚刚好

return 0;
}

这里为什么要定义 ROW 和 ROWS 以及 COL 和 COLS 呢?以图为例:

ROWS

如图,在后面需要统计以玩家输入的坐标为中心的九宫格(绿色区域)中雷的数量时,如果按照真实展示的网格(红色区域)来计算的话,数组下标会越界,所以在行和列的周围各增加一行或一列,初始化时全部填充为默认内容,这样就不会产生下标越界的问题了;

2. 菜单
1
2
3
4
5
6
7
8
void display_menu()
{
printf("*************************\n");
printf("******** 1. play ********\n");
printf("******** 0. exit ********\n");
printf("*************************\n");
}

菜单简陋,聊胜于无;

3. 开始游戏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void play_game(char mine[ROWS][COLS], char show[ROWS][COLS])
{
//随机布置雷的种子
srand((unsigned int)time(NULL));

//初始化布置雷的数组 - 存储雷盘
init_board(mine, ROWS, COLS, '0');
//初始化要显示的数组 - 展示雷盘
init_board(show, ROWS, COLS, '*');

//将两个数组打印到屏幕上
display_board(mine, ROW, COL);
display_board(show, ROW, COL);

}

首先初始化布置雷的数组:将数组的元素全部填充为字符0(不是数字 0),后面布置的雷则会填充字符1(不是数字 1);

然后初始化展示给玩家的数组:将数组的元素全部填充为字符*

这两步的意义是:当玩家输入坐标后,用坐标去存储雷盘中匹配信息,如果是雷,则退出游戏,如果不是雷,则根据返回信息,在展示雷盘中显示周围雷的个数,如果指定范围没有雷,则自动展开空白区域;

最后,为了方便调试,将两个数组的信息都输出到屏幕上;

4. 初始化存储雷盘和展示雷盘
1
2
3
4
5
6
7
8
9
10
void init_board(char arr[ROWS][COLS], int rows, int cols, char filler)
{
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
arr[i][j] = filler;
}
}
}

上面的代码没啥好说的,遍历数组,然后用给定的字符填充;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void display_board(char arr[ROWS][COLS], int row, int col)
{
//打印列序号,从零开始是为了对齐
for (int i = 0; i <= col; i++)
{
printf("%d ", i);
}
printf("\n");
for (int i = 1; i <= row; i++)
{
//打印行序号
printf("%d ", i);

for (int j = 1; j <= col; j++)
{
printf("%c ", arr[i][j]);
}
printf("\n");
}
}

为了输入坐标时更直观,这里打印行序号和列序号;

至此,就可以测试一下了,输出初始化后的雷盘:

输出初始化后的雷盘

工整的显示,完美,同时也说明了打印列序号为何要从 0 开始;

5. 布置雷

雷盘有了,接下来就是布置雷了;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void set_mine(char arr[ROWS][COLS], int row, int col)
{
//读取雷的数量
int mines = MINES;
//如果雷不为 0 则继续布置
while (mines)
{
//求余 row 的范围为 0 ~ (row - 1),加 1 之后范围为 1 ~ row,col 一样;
int x = rand() % row + 1;
int y = rand() % col + 1;
//坐标合法则将 '0' 替换为 '1',表示是布置的雷
if (arr[x][y] == '0')
{
arr[x][y] = '1';
//每布置成功一次,雷的数量自减 1
mines--;
}
}
}

布置雷很简单,生成随机数后,只要坐标合法,就将存储雷盘对应的坐标位置由字符0替换为字符1即可;

布置完成了,测试一下:

布置完成测试一下

数一数,刚好 10 个雷,与预定义的雷数量一致;

6. 开始玩游戏

获胜的规则很简单:雷盘中没有空白区域则获胜;

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
void find_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
//获取用户输入的坐标
int x = 0;
int y = 0;
//统计没有雷的坐标
int win = 0;

while (1)
{
printf("请输入坐标:>");
scanf("%d%d", &x, &y);
printf("\n");

//判断输入越界
if (x <= 0 || x > row || y <= 0 || y > col)
{
printf("输入有误,请重新输入!\n");
display_board(show, row, col);
printf("\n");
}
else
{
//判断是否是雷
if (mine[x][y] == '1')
{
printf("踩到雷了,游戏结束!\n");
//提示的同时,输出存储雷盘,显示布置雷的位置,让玩家输得心服口服
display_board(mine, row, col);
printf("\n");
break;
}
else
{
//获取以当前坐标为中心的 3 * 3 雷盘中雷的数量
int count = get_mine_count(mine, x, y);
//如果周围的 8 个坐标中都没有雷
if (count == 0)
{
//展开空白区域
show_blank_space(mine, show, x, y, &win);
//然后显示雷盘
display_board(show, row, col);
printf("\n");
}
else
{
/**
*字符 '0' 的 ASCII 值为 48,字符 '1' 的 ASCII 值为 49,以此类推,
*用字符 '0' 加上指定数字,就是当前数字对应的字符值
*例如 '0' + 1 = '1'
*/
//如果周围 8 个区域中有雷,则在玩家输入的坐标中显示周围雷的数量
show[x][y] = '0' + count;
//统计不含雷的区域
win++;
display_board(show, row, col);
printf("\n");
}

//每输入一次坐标,判断一次
//如果不含雷坐标的数量 = 所有坐标 - 雷的数量,则说明已经没有空白区域,玩家自动胜出
if (win == ROW * COL - MINES)
{
printf("排雷成功!\n");
printf("\n");
display_board(mine, row, col);
printf("\n");
break;
}
}
}
}
}
  1. 首先,获取用户输入,然后判断输入是否越界,越界则重新输入;

  2. 判断用户输入的坐标是否是雷,是雷则结束游戏;

  3. 如果用户输入的坐标不是雷,则进行如下操作:

    以当前坐标为中心,计算周围 3 * 3 的九宫格中雷的数量,规则如下:

    • 如果此范围中有雷,则将雷的数量写入展示雷盘对应的坐标中,游戏继续;
    • 如果此范围中没有雷,则以当前九宫格的每个元素为中心,再次统计周围 3 * 3 的范围中雷的数量;
    • 直到坐标周围有雷则结束展开空白区域;
7. 获取坐标周围雷的数量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int get_mine_count(char mine[ROWS][COLS], int x, int y)
{
int count = 0;
for (int i = x - 1; i <= x + 1; i++)
{
if (i == 0)
{
i = 1;
}
for (int j = y - 1; j <= y + 1; j++)
{
if (j == 0)
{
j = 1;
}
count = count + mine[i][j] - '0';
}
}
return count;
}

字符0的 ASCII 值为 48,字符1的 ASCII 值为 49,以此类推,用任意数字字符的值减去字符0就是这个字符的整型值,例如:’1’ - ‘0’ = 1;

8. 自动展开空白区域
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
void show_blank_space(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y, int *p)
{

//下标从 -1 ~ 1,则有三种状态,x - 1, x + 0, x + 1
//两个循环则有 9 种状态
for (int i = -1; i < 2; i++)
{
for (int j = -1; j < 2; j++)
{
//由于同时为 0 表示的是 自身,没有意义,所以跳过
if (i != 0 || j != 0)
{
//判断坐标是否合法 1 ~ ROW 或 1 ~ COL
if (x + i >= 1 && x + i <= ROW && y + j >= 1 && y + j <= COL)
{
//当前坐标没有被自动展开,并且当前坐标不是雷的情况下,才进行统计,否则没有意义
if (show[x + i][y + j] == '*' && mine[x + i][y + j] != '1')
{
//判断以坐标为中心的九宫格是否有雷
int count = get_mine_count(mine, x + i, y + j);
//没有雷
if (count == 0)
{
//展开坐标
show[x + i][y + j] = ' ';
//统计没有雷的坐标数量
(*p)++;
show_blank_space(mine, show, x + i, y + j, p);
}
//有雷
else
{
//显示雷的数量
show[x + i][y + j] = count + '0';
(*p)++;
}
}
}
}
}
}
}

9. 判断是否获胜
1
2
3
4
5
6
7
8
if (win == ROW * COL - MINES)
{
printf("排雷成功!\n");
printf("\n");
display_board(mine, row, col);
printf("\n");
break;
}

刚开始,写了一个计算胜出的函数,无非就是遍历数组的每个元素,判断是不是初始化时填充的字符(这里初始化时填充的是*),如果存储雷盘中有初始化时填充的字符,则游戏继续,否则玩家胜出;

但这种方法性能太低了,每输入一次就需要遍历整个数组,所以这里使用了指针,每次输入且输入有效时,统计不是雷的坐标,然后用坐标总数减去雷的数量与统计的数量进行比较,如果数量相同,则说明剩下的都是雷了,玩家自动胜出;

10. 试玩 - 测试

为了方便测试后面的逻辑,将雷的数量设置为 80,也就是说只有一个不是雷:

80个雷

自动胜出的逻辑正常,现在就差自动展开了;

自动展开

自动展开正常;

11. 完整代码
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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define ROW 9
#define COL 9
#define ROWS ROW + 2
#define COLS COL + 2
#define MINES 10

//显示菜单
void display_menu();
//开始游戏
void play_game(char mine[ROWS][COLS], char show[ROWS][COLS]);
//初始化两个数组
void init_board(char arr[ROWS][COLS], int rows, int cols, char filler);
//显示雷盘
void display_board(char arr[ROWS][COLS], int row, int col);
//布置雷
void set_mine(char arr[ROWS][COLS], int row, int col);
//获取玩家输入并判断
void find_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
//获取某个坐标周围雷的个数
int get_mine_count(char mine[ROWS][COLS], int x, int y);
//展开坐标周围空白区域
void show_blank_space(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y, int *p);
//排雷成功
// int is_win(char show[ROWS][COLS], int row, int col);

int main()
{
//埋雷
char mine[ROWS][COLS] = {0};
//显示雷
char show[ROWS][COLS] = {0};
int input = 0;

do
{
display_menu();
printf("请选择:>");
scanf("%d", &input);
printf("\n");
switch (input)
{
case 1:
play_game(mine, show);
break;
case 0:
printf("退出游戏!\n");
break;
default:
printf("输入有误,请重新输入!\n");
break;
}
} while (input);

return 0;
}

void display_menu()
{
printf("*************************\n");
printf("******** 1. play ********\n");
printf("******** 0. exit ********\n");
printf("*************************\n");
}

void play_game(char mine[ROWS][COLS], char show[ROWS][COLS])
{

srand((unsigned int)time(NULL));
init_board(mine, ROWS, COLS, '0');
init_board(show, ROWS, COLS, '*');
set_mine(mine, ROW, COL);
display_board(show, ROW, COL);
printf("\n");
find_mine(mine, show, ROW, COL);
}

void init_board(char arr[ROWS][COLS], int rows, int cols, char filler)
{
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
arr[i][j] = filler;
}
}
}

void display_board(char arr[ROWS][COLS], int row, int col)
{

for (int i = 0; i <= col; i++)
{

printf("%d ", i);
}
printf("\n");
for (int i = 1; i <= row; i++)
{
printf("%d ", i);

for (int j = 1; j <= col; j++)
{
printf("%c ", arr[i][j]);
}
printf("\n");
}
}

void set_mine(char arr[ROWS][COLS], int row, int col)
{
int mines = MINES;
while (mines)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (arr[x][y] == '0')
{
arr[x][y] = '1';
mines--;
}
}
}

void find_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;
while (1)
{
printf("请输入坐标:>");
scanf("%d%d", &x, &y);
printf("\n");
if (x <= 0 || x > row || y <= 0 || y > col)
{
printf("输入有误,请重新输入!\n");
display_board(show, row, col);
printf("\n");
}
else
{
if (mine[x][y] == '1')
{
printf("踩雷到了,游戏结束!\n");
display_board(mine, row, col);
printf("\n");
break;
}
else
{
int count = get_mine_count(mine, x, y);
if (count == 0)
{
show_blank_space(mine, show, x, y, &win);
display_board(show, row, col);
printf("\n");
}
else
{
show[x][y] = '0' + count;
win++;
display_board(show, row, col);
printf("\n");
}

if (win == ROW * COL - MINES)
{
printf("排雷成功!\n");
printf("\n");
display_board(mine, row, col);
printf("\n");
break;
}
}
}
}
}

int get_mine_count(char mine[ROWS][COLS], int x, int y)
{
int count = 0;
for (int i = x - 1; i <= x + 1; i++)
{
if (i == 0)
{
i = 1;
}
for (int j = y - 1; j <= y + 1; j++)
{
if (j == 0)
{
j = 1;
}
count = count + mine[i][j] - '0';
}
}
return count;
}

void show_blank_space(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y, int *p)
{

for (int i = -1; i < 2; i++)
{
for (int j = -1; j < 2; j++)
{
if (i != 0 || j != 0) //同时为 0 表示自身,则跳过
{
if (x + i >= 1 && x + i <= ROW && y + j >= 1 && y + j <= COL)
{
if (show[x + i][y + j] == '*' && mine[x + i][y + j] != '1')
{
int count = get_mine_count(mine, x + i, y + j);
if (count == 0)
{
show[x + i][y + j] = ' ';
(*p)++;
show_blank_space(mine, show, x + i, y + j, p);
}
else
{
show[x + i][y + j] = count + '0';
(*p)++;
}
}
}
}
}
}
}