用 C 语言在控制台实现经典扫雷游戏(Minesweeper)

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                            --
----------------------------------------
>>> 

用户在输入时,需要校验输入值是否为 10。如果不是就需要提示非法输入,并再次提示输入:

---------------- 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                          --
----------------------------------------
>>> 

用户在选择难度时,需要校验输入值是否为 123。如果不是就需要提示非法输入,并再次提示输入:

------------- SELECT LEVEL -------------
-- 1. BEGINNER                        --
-- 2. MEDIUM                          --
-- 3. EXPERT                          --
----------------------------------------
>>> 5
invalid input
>>> a
invalid input
>>> 

如果用户输入为 123,则提示用户所选难度,然后对游戏相关参数进行设置:

------------- 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.hinput.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 中进行定义,且只需要在游戏模块中使用。

在用户选定难度等级后,本局游戏的阵地大小和地雷数就确定下来了,使用全局变量 ROWCOLTOTAL 保存该数据。

为了方便后续程序中进行判断、计算,实际保存核心数据的数组,要比需要显示的阵地四周扩大一圈。其行数和列数使用全局变量 ROWSCOLS 表示。

全局变量 ROWCOLTOTALROWSCOLS 都在游戏模块的头文件 minesweeper.h 中进行定义,且只需要在游戏模块中使用。

2.2.3 游戏局数和胜利局数

本次程序运行中,总共玩扫雷游戏局数和胜利局数,分别使用变量 rounds_playedrounds_winned 来表示。

这是在 main.c 中定义的全局变量。

2.3 函数与流程设计

2.3.1 主程序 main()

main.c 文件中定义。其流程图:
在这里插入图片描述

2.3.2 单局游戏程序 minesweeper()

minesweeper.c 文件中定义,在 minesweeper.h 中声明,在 main.cmain() 函数中调用。其流程图:
在这里插入图片描述

单局游戏程序,返回值为 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 实现扫雷游戏界面。
  • 56
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值