1 产品方案
1.1 经典扫雷游戏玩法
要求实现经典扫雷游戏(Minesweeper)玩法。这里对经典扫雷游戏玩法的规则逻辑整理如下:
- 扫雷阵地由若干行、若干列的方块构成,方块下面随机藏有一定数量的地雷
- 用户不断选择一个方块并进行操作:
- 点开检查方块下面是否有地雷
- 标记方块为地雷
- 取消标记方块
- 用户点开检查某方块:
- 如果方块下面有地雷,则游戏结束(游戏失败)
- 如果方块下面无地雷,则显示该方块周围的 8 个方块下存在的地雷数。同时,如果方块周围地雷数为 0,则自动翻开与之连续相邻的所有周围地雷数为 0 的方块,以及这些方块周围的方块
- 当用户已经点开检查所有下面没有地雷的方块,则游戏结束(游戏胜利)
玩法可参考扫雷游戏网页版:https://www.minesweeper.cn/
1.2 其他辅助功能需求
- 在控制台提供简单而清晰的用户界面
- 进入程序/每一局扫雷游戏结束,用户可以选择进行/继续进行扫雷游戏或者退出程序
- 用户可以选择游戏难度:
- 入门(Beginner),阵地大小 9*9,地雷数 10
- 中等(Medium),阵地大小 16*16,地雷数 40
- 专家(Expert),阵地大小 20*25,地雷数 99
1.3 业务流程图
这里从用户视角给出其体验游戏的主流程图。
1.4 游戏界面设计
虽然只是简单的控制台程序,为保证用户体验,我仍然希望能做出清晰的用户界面。同时,虽然只是个小项目,我也想要尽量贴合完整项目的工作流程。所以,这里专门对游戏界面进行规划设计。
1.4.1 游戏数据和选项
首次进入程序/每次结束一局游戏后,提示游戏数据和选项。
---------------- STATS -----------------
-- PLAYED 00 --
-- WIN 00 --
--------------- OPTIONS ----------------
-- 1. PLAY --
-- 0. EXIT --
----------------------------------------
>>>
用户在输入时,需要校验输入值是否为 1
或 0
。如果不是就需要提示非法输入,并再次提示输入:
---------------- STATS -----------------
-- PLAYED 00 --
-- WIN 00 --
--------------- OPTIONS ----------------
-- 1. PLAY --
-- 0. EXIT --
----------------------------------------
>>> 5
invalid input
>>> a
invalid input
>>>
如果用户输入 1
,则调用单局游戏程序,并提示:
---------------- STATS -----------------
-- PLAYED 00 --
-- WIN 00 --
--------------- OPTIONS ----------------
-- 1. PLAY --
-- 0. EXIT --
----------------------------------------
>>> 1
starting minesweeper...
如果用户数输入 0
,则退出主程序:
---------------- STATS -----------------
-- PLAYED 00 --
-- WIN 00 --
--------------- OPTIONS ----------------
-- 1. PLAY --
-- 0. EXIT --
----------------------------------------
>>> 0
exiting program...
1.4.2 选择游戏难度
进入一局游戏后,首先提示让用户选择游戏难度。
------------- SELECT LEVEL -------------
-- 1. BEGINNER --
-- 2. MEDIUM --
-- 3. EXPERT --
----------------------------------------
>>>
用户在选择难度时,需要校验输入值是否为 1
、2
或 3
。如果不是就需要提示非法输入,并再次提示输入:
------------- SELECT LEVEL -------------
-- 1. BEGINNER --
-- 2. MEDIUM --
-- 3. EXPERT --
----------------------------------------
>>> 5
invalid input
>>> a
invalid input
>>>
如果用户输入为 1
、2
或 3
,则提示用户所选难度,然后对游戏相关参数进行设置:
------------- SELECT LEVEL -------------
-- 1. BEGINNER --
-- 2. MEDIUM --
-- 3. EXPERT --
----------------------------------------
>>> 1
you have selected level 1
1.4.3 扫雷阵地
用户选择好游戏难度后,以及每次用户进行操作后,如果游戏没有结束,就显示当前阵地界面结果,并提示用户可以进行下一步操作。
阵地整体结构/常规操作提示
----------------------------------------
-- UNMARKED 10 --
----------------------------------------
0 0 0 0 0 0 0 0 0
C1 2 3 4 5 6 7 8 9
R
01 * * * * * * * * *
02 * * * * * * * * *
03 * * * * * * * * *
04 * * * * * * * * *
05 * * * * * * * * *
06 * * * * * * * * *
07 * * * * * * * * *
08 * * * * * * * * *
09 * * * * * * * * *
----------------------------------------
Select a square (R C)
>>> 2 3
square (2, 3) selected
Select an operation
1. Check
2. Mark
3. Unmark - disabled
0. Skip
>>>
难度选择为中等或专家(行数/列数大于 9)
----------------------------------------
-- UNMARKED 10 --
----------------------------------------
0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1
C1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6
R
01 * * * * * * * * * * * * * * * *
02 * * * * * * * * * * * * * * * *
03 * * * * * * * * * * * * * * * *
04 * * * * * * * * * * * * * * * *
05 * * * * * * * * * * * * * * * *
06 * * * * * * * * * * * * * * * *
07 * * * * * * * * * * * * * * * *
08 * * * * * * * * * * * * * * * *
09 * * * * * * * * * * * * * * * *
10 * * * * * * * * * * * * * * * *
11 * * * * * * * * * * * * * * * *
12 * * * * * * * * * * * * * * * *
13 * * * * * * * * * * * * * * * *
14 * * * * * * * * * * * * * * * *
15 * * * * * * * * * * * * * * * *
16 * * * * * * * * * * * * * * * *
----------------------------------------
Select a square (R C)
>>> 2 3
square (2, 3) selected
Select an operation
1. Check
2. Mark
3. Unmark - disabled
0. Skip
>>>
游戏继续进行时方块的各种状态样式
- 已点开检查的方块,数字
'0-8'
,表示周围存在地雷数 - 未点开检查的方块
- 未标记,星号
'*'
表示(隐藏) - 已标记,字符
'F'
表示(Flag)
- 未标记,星号
----------------------------------------
-- UNMARKED 10 --
----------------------------------------
0 0 0 0 0 0 0 0 0
C1 2 3 4 5 6 7 8 9
R
01 0 0 0 0 0 0 1 * *
02 0 0 0 0 0 0 1 * *
03 0 0 0 0 0 0 1 * *
04 0 0 0 0 1 1 1 * *
05 0 0 0 0 1 F 1 * *
06 1 1 1 0 2 * * * *
07 * * 1 0 1 * * * *
08 1 1 1 0 1 2 * * *
09 0 0 0 0 0 1 * * *
----------------------------------------
Select a square (R C)
>>> 1 8
square (1, 8) selected
Select an operation
1. Check
2. Mark
3. Unmark - disabled
0. Skip
>>>
选择方块
用户选择方块时,需要进行校验:
- 输入是否为合法的数字且范围
- 输入的方块是否尚未被检查点开
如果不满足校验,则根据情况进行不同的提示:
----------------------------------------
-- UNMARKED 10 --
----------------------------------------
0 0 0 0 0 0 0 0 0
C1 2 3 4 5 6 7 8 9
R
01 0 0 0 0 0 0 1 * *
02 0 0 0 0 0 0 1 * *
03 0 0 0 0 0 0 1 * *
04 0 0 0 0 1 1 1 * *
05 0 0 0 0 1 F 1 * *
06 1 1 1 0 2 * * * *
07 * * 1 0 1 * * * *
08 1 1 1 0 1 2 * * *
09 0 0 0 0 0 1 * * *
----------------------------------------
Select a square (R C)
>>> 10 3
invalid input
>>> a cd
invalid input
>>> 2 3
square already checked, select another
>>>
满足校验,则提示已经选择的方块:
----------------------------------------
-- UNMARKED 10 --
----------------------------------------
0 0 0 0 0 0 0 0 0
C1 2 3 4 5 6 7 8 9
R
01 0 0 0 0 0 0 1 * *
02 0 0 0 0 0 0 1 * *
03 0 0 0 0 0 0 1 * *
04 0 0 0 0 1 1 1 * *
05 0 0 0 0 1 F 1 * *
06 1 1 1 0 2 * * * *
07 * * 1 0 1 * * * *
08 1 1 1 0 1 2 * * *
09 0 0 0 0 0 1 * * *
----------------------------------------
Select a square (R C)
>>> 1 8
square (1, 8) selected
选择操作
所有可选的操作包括:
- 点开检查(Check)
- 标记(Mark)
- 取消标记(Unmark)
- 跳过本次操作(Skip)
可选操作可能在某些情况下会被禁用:
- 点开检查(Check),当所选方块状态为“未标记”时可用;“已标记”时禁用
- 标记(Mark),当所选方块状态为“未标记”且尚未标记达总地雷数时可用;“已标记”或标记达总地雷数时禁用
- 取消标记(Unmark),当所选方块为“已标记”时可用;“未标记”时禁用
- 跳过本次操作(Skip),使用可用
对于当前禁用的操作,会有一定的提示:
----------------------------------------
-- UNMARKED 10 --
----------------------------------------
0 0 0 0 0 0 0 0 0
C1 2 3 4 5 6 7 8 9
R
01 0 0 0 0 0 0 1 * *
02 0 0 0 0 0 0 1 * *
03 0 0 0 0 0 0 1 * *
04 0 0 0 0 1 1 1 * *
05 0 0 0 0 1 F 1 * *
06 1 1 1 0 2 * * * *
07 * * 1 0 1 * * * *
08 1 1 1 0 1 2 * * *
09 0 0 0 0 0 1 * * *
----------------------------------------
Select a square (R C)
>>> 1 8
square (1, 8) selected
Select an operation
1. Check
2. Mark
3. Unmark - disabled
0. Skip
>>>
----------------------------------------
-- UNMARKED 10 --
----------------------------------------
0 0 0 0 0 0 0 0 0
C1 2 3 4 5 6 7 8 9
R
01 0 0 0 0 0 0 1 * *
02 0 0 0 0 0 0 1 * *
03 0 0 0 0 0 0 1 * *
04 0 0 0 0 1 1 1 * *
05 0 0 0 0 1 F 1 * *
06 1 1 1 0 2 * * * *
07 * * 1 0 1 * * * *
08 1 1 1 0 1 2 * * *
09 0 0 0 0 0 1 * * *
----------------------------------------
Select a square (R C)
>>> 5 7
square (5, 7) selected
Select an operation
1. Check - disabled
2. Mark - disabled
3. Unmark
0. Skip
>>>
用户输入后,需要进行校验是否为可用操作,否则进行提示,并再次提示输入。
----------------------------------------
-- UNMARKED 10 --
----------------------------------------
0 0 0 0 0 0 0 0 0
C1 2 3 4 5 6 7 8 9
R
01 0 0 0 0 0 0 1 * *
02 0 0 0 0 0 0 1 * *
03 0 0 0 0 0 0 1 * *
04 0 0 0 0 1 1 1 * *
05 0 0 0 0 1 F 1 * *
06 1 1 1 0 2 * * * *
07 * * 1 0 1 * * * *
08 1 1 1 0 1 2 * * *
09 0 0 0 0 0 1 * * *
----------------------------------------
Select a square (R C)
>>> 1 8
square (1, 8) selected
Select an operation
1. Check
2. Mark
3. Unmark - disabled
0. Skip
>>> 3
operation disabled, select another
>>> 5
invalid input
如果输入校验通过,则提示所选方块及操作,并执行操作。同时界面会暂时停留,等待用户确认后,再显示操作执行结果:
----------------------------------------
-- UNMARKED 10 --
----------------------------------------
0 0 0 0 0 0 0 0 0
C1 2 3 4 5 6 7 8 9
R
01 0 0 0 0 0 0 1 * *
02 0 0 0 0 0 0 1 * *
03 0 0 0 0 0 0 1 * *
04 0 0 0 0 1 1 1 * *
05 0 0 0 0 1 F 1 * *
06 1 1 1 0 2 * * * *
07 * * 1 0 1 * * * *
08 1 1 1 0 1 2 * * *
09 0 0 0 0 0 1 * * *
----------------------------------------
Select a square (R C)
>>> 1 8
square (1, 8) selected
Select an operation
1. Check
2. Mark
3. Unmark - disabled
0. Skip
>>> 1
checking square (1, 8)...
press enter to continue...
游戏结束(游戏失败)
- 已点开检查的方块
- 数字
'0-8'
,表示周围存在地雷数 - 刚刚踩到的地雷,字符
'B'
表示(Boom!)
- 数字
- 未点开检查的方块
- 未标记且没有地雷,星号
'*'
表示(继续隐藏) - 未标记且存在地雷,字符
'X'
表示(致命危险) - 已标记且存在地雷,字符
'F'
表示(Flag) - 已标记但没有地雷,字符
'f'
表示(Flag 错误)
- 未标记且没有地雷,星号
同时,在阵地下方提示游戏失败。
----------------------------------------
-- UNMARKED 07 --
----------------------------------------
0 0 0 0 0 0 0 0 0
C1 2 3 4 5 6 7 8 9
R
01 0 0 0 0 0 0 1 * X
02 0 0 0 0 0 0 1 X X
03 0 0 0 0 0 0 1 * *
04 0 0 0 0 1 1 1 * *
05 0 0 0 0 1 F 1 * X
06 1 1 1 0 2 * X * *
07 f F 1 0 1 B * * *
08 1 1 1 0 1 2 * * *
09 0 0 0 0 0 1 X * X
----------------------------------------
Oops, you have stepped on the mine... BOOOOM!
press enter to continue...
游戏结束(游戏胜利)
- 存在地雷的方块,数字 ‘0-8’,表示周围存在地雷数
- 不存在地雷的方块,字符 ‘F’ 表示(Flag)
同时,在阵地下方提示游戏胜利。
----------------------------------------
-- UNMARKED 07 --
----------------------------------------
0 0 0 0 0 0 0 0 0
C1 2 3 4 5 6 7 8 9
R
01 1 F 1 0 1 1 2 1 1
02 2 2 2 0 1 F 2 F 1
03 1 F 2 2 2 2 2 1 1
04 1 2 F 2 F 1 0 0 0
05 0 1 2 3 2 1 0 0 0
06 1 1 3 F 2 0 0 0 0
07 2 F 4 F 2 0 0 0 0
08 2 F 3 1 1 0 0 0 0
09 1 1 1 0 0 0 0 0 0
----------------------------------------
Good job! You have cleared all the mines without hurt~
press enter to continue...
2 技术方案
这里只对主要的流程和难点进行分析和阐述。可能会跳过一些比较简单的技术实现方案;也有可能,在具体编码实现的过程中,遇到一些突然的问题,会做出另外的处理。
无论如何,在第 3 节中,给出了完整的代码;其中每个文件顶部,都对变量、常量和函数做了声明;打码中添加了必要的注释。相信可以从这里看到最后实现的文件结构,所需要的变量和函数,以及对一些细节问题的提示。
2.1 文件结构设计
设计 3 个文件来实现:
main.c
,实现主程序- 提供主菜单
- 控制进入游戏/再次进入游戏/退出程序的主流程
- 调用游戏模块
- 统计并显示已玩局数和胜利局数
minesweeper.h
,声明单局游戏模块所需要的常量、全局变量和函数minesweeper.c
,实现单局游戏模块及各种辅助程序- 提供游戏难度选择菜单
- 展示阵地界面
- 提供操作选项
- 执行用户操作
- 实现扫雷游戏核心逻辑,控制游戏进程,判断游戏继续和结束(失败/胜利)
- 返回给主程序游戏结果
注:后来实现的过程中发现还需要一个新的文件 input.h
和 input.c
来专门存放一个标准输入缓冲区清理函数 clear_stdin_buffer()
。按照模块化的思想,这个函数不属于主程序也不属于 Minesweeper 单局游戏模块,而且又被两者同时使用。所以,虽然这个文件中只包含一个函数,还是认为独立出来最好了。
2.2 数据结构设计
2.2.1 阵地核心数据
对于阵地这种二维结构,我们很自然地使用二维数组来表示。进一步地,为了解耦方便,我们将核心数据用三个数组分别存放:
mines[ROWS][COLS]
表示每个方块上是否存在地雷,值为0
表示没有地雷,值为1
表示存在地雷nums[ROWS][COLS]
表示每个方块周围存在的地雷数,取值0-8
square[ROWS][COLS]
表示每个方块的状态:- 已被打开检查过,取值
0-8
- 未被打开检查过,且未被标记,取值
-1
- 未被打开检查过,且已被标记,取值
-2
- 已被打开检查过,取值
由于在每一局游戏中,其大小都有可能被重新定义,因此只能放在单局游戏模块的 minesweeper()
函数中进行定义。当 minesweeper()
函数调用模块内其他子函数时,会将这些数组作为参数传递给它们。
2.2.2 阵地大小和地雷数
三种难度等级的阵地大小和总地雷数:
- 入门(Beginner)
ROWB
COLB
TOTALB
- 中等(Medium)
ROWM
COLM
TOTALM
- 专家(Expert)
ROWE
COLE
TOTALE
这是游戏的基本参数,将作为字符常量在游戏模块的头文件 minesweeper.h
中进行定义,且只需要在游戏模块中使用。
在用户选定难度等级后,本局游戏的阵地大小和地雷数就确定下来了,使用全局变量 ROW
、COL
和 TOTAL
保存该数据。
为了方便后续程序中进行判断、计算,实际保存核心数据的数组,要比需要显示的阵地四周扩大一圈。其行数和列数使用全局变量 ROWS
和 COLS
表示。
全局变量 ROW
、COL
、TOTAL
、ROWS
和 COLS
都在游戏模块的头文件 minesweeper.h
中进行定义,且只需要在游戏模块中使用。
2.2.3 游戏局数和胜利局数
本次程序运行中,总共玩扫雷游戏局数和胜利局数,分别使用变量 rounds_played
和 rounds_winned
来表示。
这是在 main.c
中定义的全局变量。
2.3 函数与流程设计
2.3.1 主程序 main()
在 main.c
文件中定义。其流程图:
2.3.2 单局游戏程序 minesweeper()
在 minesweeper.c
文件中定义,在 minesweeper.h
中声明,在 main.c
的 main()
函数中调用。其流程图:
单局游戏程序,返回值为 1
表示游戏胜利;返回值为 0
表示游戏失败。
自动打开所有连续相邻且周围地雷数为 0 的方块
这是扫雷的一个算法上的小难点,实现的方法可能也有很多种。这里根据我们设计的数据结构,以及受限于我目前有限的 C 语言基础知识,采用如下方案来实现:
使用一个变量 are_all_checked
表示是否“已经自动打开了全部连续相邻且周围地雷数为 0 的方块”:值为 1
表示已经全部打开,不需要继续执行;值为 0
表示还没有全部打开,还需要继续执行。
在用户首次选择一个方块,并执行点开检查后,如果这个方块周围地雷数为 0,则自动打开检查周围所有方块。如果新打开的方块中,存在周围地雷数为 0 的方块,就说明需要继续执行。
详细流程图如下:
2.3.3 等待用户输入通用流程
在上述流程中,很多地方都有“等待用户输入”环节。这里的重点是对用户的输入合法性进行校验,保证程序运转正常;同时也要保证一定的用户体验。
由于各处判断用户输入是否合法的条件不同,所以不会统一写成一个函数。但是它们执行的流程大同小异,这里统一进行一个归纳整理:
3 代码实现
3.1 主程序 main.c
#include <stdio.h>
#include "minesweeper.h"
#include "input.h"
int rounds_played = 0;
int rounds_winned = 0;
void display_menu();
int main()
{
int input = -1;
// the main loop
while (1)
{
// display stats and main menu
display_menu();
// wait for user input
printf(">>> ");
scanf("%d", &input);
clear_stdin_buffer();
while (input != 1 && input != 0)
{
printf("invalid input\n");
printf(">>> ");
scanf("%d", &input);
clear_stdin_buffer();
}
if (input == 1)
{
printf("starting minesweeper...\n");
int r = minesweeper();
// update stats
++rounds_played;
rounds_winned += r;
}
else if (input == 0)
{
printf("exiting program...\n");
break;
}
}
return 0;
}
// display the stats and main menu
void display_menu()
{
printf("\n---------------- STATS -----------------\n");
printf("-- PLAYED %02d --\n", rounds_played);
printf("-- WIN %02d --\n", rounds_winned);
printf("--------------- OPTIONS ----------------\n");
printf("-- 1. PLAY --\n");
printf("-- 0. EXIT --\n");
printf("----------------------------------------\n");
}
3.2 单局游戏模块
3.2.1 minesweeper.h
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "input.h"
#define ROWB 9 // number of rows for beginner level
#define COLB 9 // number of cols for beginner level
#define TOTALB 10 // number of mines for beginner level
#define ROWM 16 // number of rows for medium level
#define COLM 16 // number of cols for medium level
#define TOTALM 40 // number of mines for medium level
#define ROWE 20 // number of rows for expert level
#define COLE 25 // number of cols for expert level
#define TOTALE 99 // number of mines for expert level
static int ROW = ROWB; // the actual number of rows in a game
static int COL = COLB; // the actual number of cols in a game
static int TOTAL = TOTALB; // the actual number of mines in a game
static int ROWS = ROWB+ 2; // the rows of spanned array
static int COLS = COLB + 2; // the cols of spanned array
static int unmarked = TOTALB; // the number of squares can be marked
int minesweeper();
static void display_level_options();
static void set_level(int input);
static void init_game_board(int rows, int cols, int mines[rows][cols], int nums[rows][cols], int squares[rows][cols], int news[rows][cols]);
static void set_mines(int rows, int cols, int mines[rows][cols]);
static void display_mines(int rows, int cols, int mines[rows][cols]);
static void display_mines_spanned(int rows, int cols, int mines[rows][cols]);
static void generate_numbers(int rows, int cols, int mines[rows][cols], int nums[rows][cols]);
static void display_nums(int rows, int cols, int nums[rows][cols]);
static void initialize_squares(int rows, int cols, int squares[rows][cols]);
static void display_squares(int rows, int cols, int squares[rows][cols]);
static void display_square_values_spanned(int rows, int cols, int squares[rows][cols]);
static void display_board(int rows, int cols, int squares[rows][cols]);
static void initialize_news(int rows, int cols, int news[rows][cols]);
static void mark_square(int rows, int cols, int squares[rows][cols], int r, int c);
static void unmark_square(int rows, int cols, int squares[rows][cols], int r, int c);
static int check_square(int rows, int cols, int mines[rows][cols], int nums[rows][cols], int squares[rows][cols], int r, int c);
static void auto_check_all_safe(int rows, int cols, int nums[rows][cols], int squares[rows][cols]);
static int check_surrounding_squares(int rows, int cols, int nums[rows][cols], int squares[rows][cols], int r, int c);
static int are_all_cleared(int rows, int cols, int mines[rows][cols], int squares[rows][cols]);
static void display_board_win(int rows, int cols, int mines[rows][cols], int nums[rows][cols]);
static void display_board_lose(int rows, int cols, int mines[rows][cols], int squares[rows][cols], int last_r, int last_c);
3.2.2 minesweeper.c
注意,这部分代码量较大。
#include "minesweeper.h"
// a single round of the game
int minesweeper()
{
int input = -1;
int r = 0, c = 0;
int last_r = 0, last_c = 0;
// flag = 0 continue the game
// flag = 1 end the game & win
// flag = -1 end the game & lose
int flag = 0;
display_level_options();
// wait for user input
printf(">>> ");
scanf("%d", &input);
clear_stdin_buffer();
while (input != 1 && input != 2 && input != 3)
{
printf("invalid input\n");
printf(">>> ");
scanf("%d", &input);
clear_stdin_buffer();
}
printf("you have selected level %d\n", input);
set_level(input);
// reset
input = -1;
// create the arrays to hold the key data and initialize the data
int mines[ROWS][COLS];
int nums[ROWS][COLS];
int squares[ROWS][COLS];
int news[ROWS][COLS];
init_game_board(ROWS, COLS, mines, nums, squares, news);
// the main game loop
while (1)
{
display_board(ROWS, COLS, squares);
// ask user to select a square
printf("Select a square (R C)\n");
// wait for user input
printf(">>> ");
scanf("%d %d", &r, &c);
clear_stdin_buffer();
while ((r < 1 || r > ROW) || (c < 1 || c > COL) || (squares[r][c] >= 0))
{
if (squares[r][c] >= 0)
printf("square (%d, %d) already checked, select another\n", r, c);
else
printf("invalid input\n");
printf(">>> ");
scanf("%d %d", &r, &c);
clear_stdin_buffer();
}
printf("square (%d, %d) selected\n", r, c);
// ask user to select an operation
printf("Select an operation\n");
printf("1. Check%s\n", ((squares[r][c]==-2)?" - disabled":""));
printf("2. Mark%s\n", ((squares[r][c]==-2 || unmarked <= 0)?" - disabled":""));
printf("3. Unmark%s\n", ((squares[r][c]==-1)?" - disabled":""));
printf("0. Skip\n");
// wait for user input
printf(">>> ");
scanf("%d", &input);
clear_stdin_buffer();
// while ((squares[r][c] == -1 && (input != 0 && input != 1 && input != 2)) || (squares[r][c] == -2 && (input != 0 && input != 3)))
while ((input==1 && squares[r][c]==-2) || (input==2 && (squares[r][c]==-2 || unmarked<=0)) || (input==3 && squares[r][c]==-1) || (input>3 || input<0))
{
// if ((squares[r][c]==-1 && input == 3) || (squares[r][c]==-2 && (input == 1 || input == 2)))
if ((input==1 && squares[r][c]==-2) || (input==2 && squares[r][c]==-2) || (input==3 && squares[r][c]==-1))
printf("operation disabled, select another\n");
else if (input==2 && squares[r][c]==-1 && unmarked<=0)
printf("could not mark more than %d squares\n", TOTAL);
else
printf("invalid input\n");
printf(">>> ");
scanf("%d", &input);
clear_stdin_buffer();
}
// the execution of operation
if (input == 1)
{
printf("checking square (%d, %d)...\n", r, c);
flag = check_square(ROWS, COLS, mines, nums, squares, r, c);
}
else if (input == 2)
{
printf("marking square (%d, %d)...\n", r, c);
mark_square(ROWS, COLS, squares, r, c);
}
else if (input == 3)
{
printf("unmarking square (%d, %d)...\n", r, c);
unmark_square(ROWS, COLS, squares, r, c);
}
else if (input == 0)
{
printf("skipping operation...\n");
}
last_r = r;
last_c = c;
// reset
r = 0;
c = 0;
input = -1;
printf("press enter to continue...\n");
getchar();
if (flag == 1 || flag == -1)
break;
}
// display the result (win/lose)
if (flag == 1)
{
display_board_win(ROWS, COLS, mines, nums);
printf("Good job! You have cleared all the mines without hurt~\n");
printf("press enter to continue...\n");
getchar();
return 1;
}
else if (flag == -1)
{
display_board_lose(ROWS, COLS, mines, squares, last_r, last_c);
printf("Oops, you have stepped on the mine... BOOOOM!\n");
printf("press enter to continue...\n");
getchar();
return 0;
}
}
// display the level options
static void display_level_options()
{
printf("\n------------- SELECT LEVEL -------------\n");
printf("-- 1. BEGINNER --\n");
printf("-- 2. MEDIUM --\n");
printf("-- 3. EXPERT --\n");
printf("----------------------------------------\n");
}
// set the game level
static void set_level(int input)
{
if (input == 1)
{
ROW = ROWB;
COL = COLB;
TOTAL = TOTALB;
}
else if (input == 2)
{
ROW = ROWM;
COL = COLM;
TOTAL = TOTALM;
}
else if (input == 3)
{
ROW = ROWE;
COL = COLE;
TOTAL = TOTALE;
}
ROWS = ROW + 2;
COLS = COL + 2;
unmarked = TOTAL;
}
// initialize the game board
static void init_game_board(int rows, int cols, int mines[rows][cols], int nums[rows][cols], int squares[rows][cols], int news[rows][cols])
{
set_mines(rows, cols, mines);
generate_numbers(rows, cols, mines, nums);
initialize_squares(rows, cols, squares);
initialize_news(rows, cols, news);
// for test
// display_mines(rows, cols, mines);
// display_mines_spanned(rows, cols, mines);
// display_nums(rows, cols, nums);
// display_squares(rows, cols, squares);
// display_square_values_spanned(rows, cols, squares);
}
// set the mines
static void set_mines(int rows, int cols, int mines[rows][cols])
{
// set all the values to be 0 -- no mine
for (int i = 0; i < rows; ++i)
{
for (int j = 0; j < cols; ++j)
{
mines[i][j] = 0;
}
}
// set mines randomly to the number of total
srand((unsigned)time(NULL));
int count = 0;
while (count < TOTAL)
{
int r = rand() % (rows - 2) + 1;
int c = rand() % (cols - 2) + 1;
if (mines[r][c] != 1)
{
mines[r][c] = 1;
++count;
}
}
}
// display the mines directly for test
static void display_mines(int rows, int cols, int mines[rows][cols])
{
int i, j;
printf("\n--------- TEST: DISPLAY MINES ----------\n");
printf(" ");
for (j = 1; j <= cols - 2; ++j) printf("%d ", j/10);
printf("\n C");
for (j = 1; j <= cols - 2; ++j) printf("%d ", j%10);
printf("\n R\n");
for (i = 1; i <= rows - 2; ++i)
{
printf("%02d ", i);
for (j = 1; j <= cols - 2; ++j) printf("%d ", mines[i][j]);
printf("\n");
}
printf("----------------------------------------\n");
}
// display the mines spanned directly for test
static void display_mines_spanned(int rows, int cols, int mines[rows][cols])
{
int i, j;
printf("\n----- TEST: DISPLAY MINES SPANNED ------\n");
printf(" ");
for (j = 0; j < cols; ++j) printf("%d ", j/10);
printf("\n C");
for (j = 0; j < cols; ++j) printf("%d ", j%10);
printf("\n R\n");
for (i = 0; i < rows; ++i)
{
printf("%02d ", i);
for (j = 0; j < cols; ++j) printf("%d ", mines[i][j]);
printf("\n");
}
printf("----------------------------------------\n");
}
// generate the numbers of surrounding mines for each square
static void generate_numbers(int rows, int cols, int mines[rows][cols], int nums[rows][cols])
{
int i, j;
for (i = 1; i <= rows - 2; ++i)
{
for (j = 1; j <= cols - 2; ++j)
{
nums[i][j] = mines[i-1][j-1] + mines[i-1][j] + mines[i-1][j+1] +
mines[i][j-1] + mines[i][j+1] +
mines[i+1][j-1] + mines[i+1][j] + mines[i+1][j+1];
}
}
}
// display the numbers directly for test
static void display_nums(int rows, int cols, int nums[rows][cols])
{
int i, j;
printf("\n---------- TEST: DISPLAY NUMS ----------\n");
printf(" ");
for (j = 1; j <= cols - 2; ++j) printf("%d ", j/10);
printf("\n C");
for (j = 1; j <= cols - 2; ++j) printf("%d ", j%10);
printf("\n R\n");
for (i = 1; i <= rows - 2; ++i)
{
printf("%02d ", i);
for (j = 1; j <= cols - 2; ++j) printf("%d ", nums[i][j]);
printf("\n");
}
printf("----------------------------------------\n");
}
// initialize the square status
static void initialize_squares(int rows, int cols, int squares[rows][cols])
{
int i, j;
// set all status value to be -1 -- unchecked & unmarked
for (i = 0; i < rows; ++i)
{
for (j = 0; j < cols; ++j)
{
squares[i][j] = -1;
}
}
}
// display the square values directly for test
static void display_squares(int rows, int cols, int squares[rows][cols])
{
int i, j;
printf("\n-------- TEST: DISPLAY SQUARES ---------\n");
printf(" ");
for (j = 1; j <= cols - 2; ++j) printf("%d ", j/10);
printf("\n C");
for (j = 1; j <= cols - 2; ++j) printf("%d ", j%10);
printf("\n R\n");
for (i = 1; i <= rows - 2; ++i)
{
printf("%02d ", i);
for (j = 1; j <= cols - 2; ++j)
{
if (squares[i][j] >= 0) printf("%d ", squares[i][j]);
else if (squares[i][j] == -1) printf("U ");
else if (squares[i][j] == -2) printf("M");
}
printf("\n");
}
printf("----------------------------------------\n");
}
// display the values of the squares spanned for test
static void display_square_values_spanned(int rows, int cols, int squares[rows][cols])
{
printf("\n");
for (int s = 0; s < ROWS; ++s)
{
printf("%d ", &squares[s][0]);
for (int t = 0; t < COLS; ++t)
printf("%d ", squares[s][t]);
printf("\n");
}
printf("\n");
}
// initialize news data
static void initialize_news(int rows, int cols, int news[rows][cols])
{
int i, j;
for (i = 0; i < rows; ++i)
{
for (j = 0; j < cols; ++j)
{
news[i][j] = 0;
}
}
}
// display the board
static void display_board(int rows, int cols, int squares[rows][cols])
{
int i,j;
printf("\n----------------------------------------\n");
printf("-- UNMARKED %02d --\n", unmarked);
printf("----------------------------------------\n");
printf(" ");
for (j = 1; j <= cols - 2; ++j) printf("%d ", j/10);
printf("\n C");
for (j = 1; j <= cols - 2; ++j) printf("%d ", j%10);
printf("\n R\n");
for (i = 1; i <= rows - 2; ++i)
{
printf("%02d ", i);
for (j = 1; j <= cols - 2; ++j)
{
if (squares[i][j] >= 0)
{
printf("%d ", squares[i][j]);
}
else if (squares[i][j] == -1)
{
printf("* ");
}
else if (squares[i][j] == -2)
{
printf("F ");
}
}
printf("\n");
}
printf("----------------------------------------\n");
}
// mark the selected square
static void mark_square(int rows, int cols, int squares[rows][cols], int r, int c)
{
if (squares[r][c] == -1 && unmarked >= 0)
{
squares[r][c] = -2;
--unmarked;
}
}
// unmark the selected square
static void unmark_square(int rows, int cols, int squares[rows][cols], int r, int c)
{
if (squares[r][c] == -2)
{
squares[r][c] = -1;
++unmarked;
}
}
// check the square
static int check_square(int rows, int cols, int mines[rows][cols], int nums[rows][cols], int squares[rows][cols], int r, int c)
{
// printf("entered check_squares()\n"); // for test
// checked
squares[r][c] = nums[r][c];
if (mines[r][c] == 1)
{
return -1;
}
else if (mines[r][c] == 0)
{
auto_check_all_safe(rows, cols, nums, squares);
if (are_all_cleared(rows, cols, mines, squares))
return 1;
else
return 0;
}
}
// auto check all the continous and adjacent squares that
static void auto_check_all_safe(int rows, int cols, int nums[rows][cols], int squares[rows][cols])
{
// printf("entered auto_check_all_safe()\n"); // for test
int i, j;
int are_all_checked = 1;
do
{
are_all_checked = 1;
for (i = 1; i <= rows-2; ++i)
{
for (j = 1; j <= cols-2; ++j)
{
if (squares[i][j] >= 0 && nums[i][j] == 0)
{
if (check_surrounding_squares(rows, cols, nums, squares, i, j) == 0)
are_all_checked = 0;
}
}
}
} while (are_all_checked == 0);
}
// check the surouding 8 squares
static int check_surrounding_squares(int rows, int cols, int nums[rows][cols], int squares[rows][cols], int r, int c)
{
// printf("entered check_surrounding_squares(%d, %d)\n", r, c); // for test
int flag = 1;
if (squares[r-1][c-1] < 0)
{
if ((squares[r-1][c-1] = nums[r-1][c-1]) == 0)
flag = 0;
// printf("square[%d][%d] checked, flag = %d\n", r-1, c-1,flag); // for test
}
if (squares[r-1][c] < 0)
{
if ((squares[r-1][c] = nums[r-1][c]) == 0)
flag = 0;
// printf("square[%d][%d] checked, flag = %d\n", r-1, c,flag); // for test
}
if (squares[r-1][c+1] < 0)
{
if ((squares[r-1][c+1] = nums[r-1][c+1]) == 0)
flag = 0;
// printf("square[%d][%d] checked, flag = %d\n", r-1, c+1,flag); // for test
}
if (squares[r][c-1] < 0)
{
if ((squares[r][c-1] = nums[r][c-1]) == 0)
flag = 0;
// printf("square[%d][%d] checked, flag = %d\n", r, c-1,flag); // for test
}
if (squares[r][c+1] < 0)
{
if ((squares[r][c+1] = nums[r][c+1]) == 0)
flag = 0;
// printf("square[%d][%d] checked, flag = %d\n", r, c+1,flag); // for test
}
if (squares[r+1][c-1] < 0)
{
if ((squares[r+1][c-1] = nums[r+1][c-1]) == 0)
flag = 0;
// printf("square[%d][%d] checked, flag = %d\n", r+1, c-1,flag); // for test
}
if (squares[r+1][c] < 0)
{
if ((squares[r+1][c] = nums[r+1][c]) == 0)
flag = 0;
// printf("square[%d][%d] checked, flag = %d\n", r+1, c,flag); // for test
}
if (squares[r+1][c+1] < 0)
{
if ((squares[r+1][c+1] = nums[r+1][c+1]) == 0)
flag = 0;
// printf("square[%d][%d] checked, flag = %d\n", r+1, c+1,flag); // for test
}
// printf("flag = %d\n", flag); // for test
return flag;
}
// check if all the mines are cleared
static int are_all_cleared(int rows, int cols, int mines[rows][cols], int squares[rows][cols])
{
// printf("entered are_all_cleared()\n"); // for test
int i, j;
for (int i = 1; i <= rows-2; ++i)
{
for (int j = 1; j <= cols-2; ++j)
{
if (mines[i][j] == 0 && squares[i][j] < 0)
{
// printf("not all cleared at (%d, %d)\n", i, j); // for test
return 0;
}
}
}
// printf("all cleared\n"); // for test
return 1;
}
// display the board (win)
static void display_board_win(int rows, int cols, int mines[rows][cols], int nums[rows][cols])
{
int i,j;
printf("\n----------------------------------------\n");
printf("-- UNMARKED %02d --\n", unmarked);
printf("----------------------------------------\n");
printf(" ");
for (j = 1; j <= cols - 2; ++j) printf("%d ", j/10);
printf("\n C");
for (j = 1; j <= cols - 2; ++j) printf("%d ", j%10);
printf("\n R\n");
for (i = 1; i <= rows - 2; ++i)
{
printf("%02d ", i);
for (j = 1; j <= cols - 2; ++j)
{
if (mines[i][j] == 0)
{
printf("%d ", nums[i][j]);
}
else if (mines[i][j] == 1)
{
printf("F ");
}
}
printf("\n");
}
printf("----------------------------------------\n");
}
// display the board (lose)
static void display_board_lose(int rows, int cols, int mines[rows][cols], int squares[rows][cols], int last_r, int last_c)
{
int i,j;
printf("\n----------------------------------------\n");
printf("-- UNMARKED %02d --\n", unmarked);
printf("----------------------------------------\n");
printf(" ");
for (j = 1; j <= cols - 2; ++j) printf("%d ", j/10);
printf("\n C");
for (j = 1; j <= cols - 2; ++j) printf("%d ", j%10);
printf("\n R\n");
for (i = 1; i <= rows - 2; ++i)
{
printf("%02d ", i);
for (j = 1; j <= cols - 2; ++j)
{
if (squares[i][j] >= 0)
{
if (i == last_r && j == last_c)
printf("B ");
else
printf("%d ", squares[i][j]);
}
else if (squares[i][j] == -1 && mines[i][j] == 0)
{
printf("* ");
}
else if (squares[i][j] == -1 && mines[i][j] == 1)
{
printf("X ");
}
else if (squares[i][j] == -2 && mines[i][j] == 0)
{
printf("f ");
}
else if (squares[i][j] == -2 && mines[i][j] == 1)
{
printf("F ");
}
}
printf("\n");
}
printf("----------------------------------------\n");
}
3.3 input 模块
3.3.1 input.h
#include <stdio.h>
void clear_stdin_buffer();
3.3.2 input.c
#include "input.h"
// clear stdin buffer, especially needed after illegal input
void clear_stdin_buffer()
{
int ch;
while ((ch = getchar()) != EOF && ch != '\n')
{}
}
4 总结感想
这是学习一段时间 C 语言之后的综合练习。在练习中,主要使用 C 语言函数和数组,实现了扫雷游戏的经典玩法,并在控制台提供了一个用户体验良好的游戏界面。
项目预计耗时 1 天,实际耗时 2.5 天。预料之外的时间主要花在:
- 受产品思维影响,我不仅要实现游戏的核心逻辑,还想要实现一个比较完整的用户界面。这就增加了许多逻辑,处理起来花费更多时间。
- 出现一个一开始令我难以捉摸的 bug,检查了 3 个小时,定位问题后,寻找解决方案、处理好又花了一两个小时。这个 bug 跟二维数组作为函数参数有关,是自己学习的漏洞导致的。但是通过这个 bug,我对数组、指针和地址的理解变得更加深刻。关于这一块知识的思考和整理,后面再专门写文章阐述。
通过这一次练习,练习巩固了近期所学的 C 语言知识,模拟了项目开发的基本流程,锻炼了程序调试能力。不过,除去这些常规的收获,我最大的感想是:
- 这样一个强度较大的练习,全面调动我所掌握的(以及尚未掌握的) C 语言知识,让我对 C 语言有了一个更加丰富而感性的认知。
- 我切身体会到了 C 语言是一门更加接近计算机底层的编程语言。我之前学习过 Python,这是一门上来就能应用的编程语言;相比而言,要熟练使用 C 语言,需要对计算机基础知识有更扎实的理解。之前使用 Python,用得再多,也不觉得自己是计算机专业人士;现在深入学习 C 语言,才觉得有一些专业的感觉。
未尽事宜:
- 受限于自己目前有限的 C 语言及编程知识,虽然实现了扫雷游戏,但是很多实现方案可能不够优雅,希望日后能不断改进优化。
- 控制台界面无论怎么设计,在扫雷游戏上,始终没有图形界面体验好,希望日后学习图形界面编程,用 GUI 实现扫雷游戏界面。