用C语言从0开始的扫雷编程思路与重构

对于许多和我一样的编程爱好者来说,Windows系统自带的扫雷游戏或许是我们最早接触到的逻辑游戏之一。它规则简单,却又充满挑战。今天,我不想仅仅是玩这个游戏,而是想和大家分享一下我亲手用C语言创造它的完整过程。

这篇文章将详细记录我开发一个控制台版扫雷游戏的完整历程。我将深入剖析开发过程中的每个决策点、遇到的困难、解决问题的思路,以及对最终方案的反思,希望能为大家提供一个从“能用”到“好用”的完整开发蓝图。

第一章:奠定基石 - 棋盘的搭建
思路与决策

我的第一个设计决策是:如何存储棋盘状态?

起初我考虑过用一个二维数组,通过不同的数值(如-1代表雷,0代表空白,1-8代表数字,正负区分是否揭开)来表示格子的所有状态。但我很快意识到,这会让单个格子的状态变得过于复杂,后续的逻辑判断会非常混乱。

因此,我采用了经典的双棋盘结构

  1. mine 棋盘:作为后端,存储地雷的真实布局 ('1'为雷, '0'为安全)。

  2. show 棋盘:作为前端,显示给玩家看 ('*'为遮蔽)。

// game.h
typedef struct {
    // ...
    char mine[ROWS][COLS];    // 存储地雷的数组(答案)
    char show[ROWS][COLS];    // 显示给玩家看的数组(视图)
    // ...
} GameState;
  • 优点:职责分离非常清晰。mine数组是静态的“数据源”,show数组是动态的“视图”。检查是否踩雷 (if (gs->mine[x][y] == '1')) 和更新玩家视图 (gs->show[x][y] = '...') 的逻辑互不干扰,代码简洁明了。

  • 缺点:内存占用是单个棋盘的两倍。但对于扫雷这个规模的游戏来说,这点内存开销换来逻辑上的清晰是完全值得的。

困难与解决

如何生成每局都不同的随机地雷?我使用了C语言的 rand() 函数,但很快发现,如果不做任何处理,每次程序运行生成的地雷位置都完全一样。

解决思路rand() 生成的是“伪随机数”,其序列由一个“种子(seed)”决定。种子不变,序列就不变。为了让种子变化,我需要一个每次运行程序都不同的值来初始化它。当前时间戳 time(NULL) 是最完美的解决方案。因此,我在 main 函数的开头加入了srand()来设定种子,确保了游戏的随机性。

// main.c
// 这行代码在整个程序中只执行一次,确保了全局的随机性
srand((unsigned int)time(NULL));
第二章:核心玩法 - 递归展开的智慧
思路与决策

扫雷的精髓在于点开空白格后,安全区自动“涟漪”般展开。我立刻想到了用递归 (Recursion) 来实现这个功能,因为它能非常自然地模拟这个“扩散”的过程。

我的 ExpandMine 函数逻辑是:检查当前格子,如果为空,则把它揭开,然后对其周围的8个邻居重复相同的过程。

困难与解决

遇到的最大困难:程序直接崩溃,报错“堆栈溢出 (Stack Overflow)”。

解决思路:我立刻意识到这是无限递归。我的脑海里模拟了最简单的场景:两个相邻的空白格 ABExpandMine(A) 会调用 ExpandMine(B),而 ExpandMine(B) 又会回头调用 ExpandMine(A),程序陷入了 A -> B -> A -> B ... 的死亡循环。问题在于,我没有记录哪些格子已经被访问过

我考虑过建立一个独立的 bool visited[ROWS][COLS] 数组来跟踪状态,但这会增加数据管理的复杂性。这时我灵光一闪:show 数组本身就可以作为“访问记录”!任何一个已经被揭开的格子(值不再是 '*'),就意味着它已经被访问并处理过了。

于是,我在 ExpandMine 的入口处加入了这个关键的停止条件:

// game.c -> ExpandMine()
void ExpandMine(GameState* gs, int x, int y) {
    // 任何越界或已被揭开的格子,都是递归的终点
    if (x < 1 || x > gs->config.row || y < 1 || y > gs->config.col || gs->show[x][y] != '*') {
        return;
    }
    // ...
}

这个利用现有状态来避免重复的方案,我认为非常优雅,它在不增加额外数据结构的情况下完美解决了问题。

第三章:体验升级 - 丰富游戏功能
思路与决策
  1. 标记地雷:如何让玩家在不增加额外交互步骤的情况下,选择“挖开”或“标记”?我考虑过几种方案:用负坐标标记、每次操作后提问等,但最终选择了扩展输入指令为 x y action 的方式。这最为直观且易于扩展。

  2. 增加计时器:我使用了 <time.h> 库。在游戏开始时记录一个时间戳,每次操作后和游戏结束时用当前时间戳与开始时间戳作差,即可得到游戏用时。

// game.c -> GameLoop()
// 在玩家每次有效操作后,都计算并显示时间
time_t current_time = time(NULL);
printf("Time elapsed: %.0f seconds.\n", difftime(current_time, gs->startTime));
第四章:从“能用”到“好用” - 彻底的代码重构
困难与解决

随着功能增多,我的代码开始变得臃肿。GameLoop 函数的参数列表越来越长(大概有7个!),DisplayBoard 里充满了重复的 switch 语句。这些都是典型的“代码坏味道”,表明代码的耦合度太高,且违反了 DRY (Don't Repeat Yourself) 原则。我决定进行一次彻底的重构。

解决思路

  1. 引入 GameState 结构体:这是我解决高耦合问题的核心手段。我将所有零散的游戏状态变量(棋盘、行、列、地雷数等)全部封装进一个 GameState 结构体。

    • 优点:函数签名立刻变得干净。所有函数都只传递一个 GameState* 指针,代码耦合度大幅降低。未来如果需要增加新的状态(比如剩余地雷数),我只需要修改 GameState 结构,而不用修改几十个函数调用。

    • 缺点:所有的数据访问都变成了 gs-> 的形式,可能会让代码看起来稍微长一点,但这是完全值得的。

  2. 消除 DisplayBoard 的冗余: 我发现 switch 语句中每个 case 的代码几乎完全一样,唯一的区别是格式化字符串("%-3c"%-2c")。我用一个三元运算符在循环开始前就确定好这个字符串,从而彻底删除了整个 switch 结构。

// game.c -> DisplayBoard()
// 用一行代码取代了20多行的switch-case结构
const char* col_padding = (gs->config.difficulty == 2 || gs->config.difficulty == 3) ? "%-3c" : "%-2c";

这个改动是 DRY 原则的经典应用,极大地提升了代码的可维护性。

第五章:画上句点 - 显示最终答案
思路与决策

游戏结束后,需要显示答案。我最初的想法是直接修改 show 棋盘来植入答案,但感觉这会“污染”show 棋盘的数据。我最终决定采纳一个职责更分离的方案:创建一个全新的 DisplayAnswerBoard 函数。

  • 优点DisplayBoard 专心负责显示游戏过程,DisplayAnswerBoard 专心负责显示最终答案。两个函数的职责都非常单一,符合高质量软件设计的原则。

  • 实现:这个函数直接读取 mine 棋盘,将 '1' 打印为地雷符号 'X',将 '0' 打印为安全符号 '_'

// game.c -> GameLoop() 失败和胜利时
// 直接调用专用的答案显示函数,逻辑清晰
DisplayAnswerBoard(gs);

最终代码以及细节讨论

经过这一系列的迭代与重构,我得到了结构清晰、功能完备的最终代码。下面是完整的项目文件,方便大家参考。

game.h
#pragma once

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define MAX_ROW 30
#define MAX_COL 30
#define ROWS (MAX_ROW + 2)
#define COLS (MAX_COL + 2)

// Player operation
typedef enum {
    ACTION_INVALID = 0,
    ACTION_REVEAL = 1,
    ACTION_FLAG = 2
} PlayerAction;

// Game configuration structure
typedef struct {
    int row;
    int col;
    int mineCount;
    int difficulty;
} GameConfig;

// Game state structure
typedef struct {
    GameConfig config;
    char mine[ROWS][COLS];    // Store the minefield
    char show[ROWS][COLS];    // Store the player's view
    time_t startTime;      // Game start time
} GameState;

void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
void DisplayAnswerBoard(const GameState* gs);
void DisplayBoard(const GameState* gs);
void SetMine(GameState* gs);
void GameLoop(GameState* gs);
void ExpandMine(GameState* gs, int x, int y);
main.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"

// Home Menu
void HomeMenu() {
    printf("\n=== Minesweeper ===\n");
    printf("===== 1. Start ====\n");
    printf("===== 0. Exit =====\n");
    printf("===================\n");
}

// Difficulty Selection Menu
void DifficultyMenu() {
    printf("\n=== Select a difficulty level ===\n");
    printf("1. Easy (9x9 with 10 mines)\n");
    printf("2. Medium (16x16 with 40 mines)\n");
    printf("3. Hard (24x24 with 99 mines)\n");
    printf("4. test (6x6 with 2 mines)\n");
}

int ConfigureDifficulty(GameConfig* config) {
    int level = 0;
    DifficultyMenu();
    printf("\nPlease select a difficulty level(1/2/3):>");
    if (scanf("%d", &level) != 1) {
        while (getchar() != '\n');
        level = 0;
    }

    switch (level) {
    case 1:
        printf("Easy level\n");
        *config = (GameConfig){ 9, 9, 10, 1 };
        break;
    case 2:
        printf("Medium level\n");
        *config = (GameConfig){ 16, 16, 40, 2 };
        break;
    case 3:
        printf("Hard level\n");
        *config = (GameConfig){ 24, 24, 99, 3 };
        break;
    case 4:
        printf("Test level\n");
        *config = (GameConfig){ 6, 6, 2, 0 };
        break;
    default:
        printf("Invalid input, please try again.\n");
        return 0;
    }
    return 1;
}

void StartGame(const GameConfig config) {
    GameState gs = { 0 };
    gs.config = config;
    gs.startTime = time(NULL);

    InitBoard(gs.mine, config.row + 2, config.col + 2, '0');
    InitBoard(gs.show, config.row + 2, config.col + 2, '*');
    
    SetMine(&gs);
    GameLoop(&gs);
}

int main() {
    int input = 0;
    srand((unsigned int)time(NULL));
    do {
        HomeMenu();
        printf("\nPlease enter your choice(1/0):>");
        if (scanf("%d", &input) != 1) {
            while (getchar() != '\n');
            input = -1;
        }

        if (input == 1) {
            GameConfig config = { 0 };
            while (!ConfigureDifficulty(&config));
            StartGame(config);
        }
        else if (input == 0) {
            printf("\nExit game.\n");
        }
        else {
            printf("\nInvalid input, please try again.\n");
        }
    } while (input != 0);
    
    return 0;
}
game.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"

void InitBoard(char board[ROWS][COLS], int rows, int cols, char set) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            board[i][j] = set;
        }
    }
}

int GetMineCount(const GameState* gs, int x, int y) {
    const char (*mine)[COLS] = gs->mine;
    return (mine[x - 1][y] + mine[x - 1][y - 1] + mine[x][y - 1] + mine[x + 1][y - 1] + mine[x + 1][y] +
            mine[x + 1][y + 1] + mine[x][y + 1] + mine[x - 1][y + 1] - 8 * '0');
}

void DisplayBoard(const GameState* gs) {
    printf("\n----------Mine Sweeper Game----------\n");
    const char* col_padding = (gs->config.difficulty == 2 || gs->config.difficulty == 3) ? "%-3c" : "%-2c";
    const char* header_padding = (gs->config.difficulty == 2 || gs->config.difficulty == 3) ? "%-3d" : "%-2d";

    printf("   ");
    for (int i = 1; i <= gs->config.col; i++) {
        printf(header_padding, i);
    }
    printf("\n");

    for (int i = 1; i <= gs->config.row; i++) {
        printf("%-3d", i);
        for (int j = 1; j <= gs->config.col; j++) {
            printf(col_padding, gs->show[i][j]);
        }
        printf("\n");
    }
}

void DisplayAnswerBoard(const GameState* gs) {
    printf("\n---------- The Answer Key ----------\n");
    const char* col_padding = (gs->config.difficulty == 2 || gs->config.difficulty == 3) ? "%-3c" : "%-2c";
    const char* header_padding = (gs->config.difficulty == 2 || gs->config.difficulty == 3) ? "%-3d" : "%-2d";

    printf("   ");
    for (int i = 1; i <= gs->config.col; i++) {
        printf(header_padding, i);
    }
    printf("\n");

    for (int i = 1; i <= gs->config.row; i++) {
        printf("%-3d", i);
        for (int j = 1; j <= gs->config.col; j++) {
            char display_char;
            if (gs->mine[i][j] == '1') {
                display_char = 'X'; // Mine
            }
            else {
                display_char = '_'; // No mine
            }
            printf(col_padding, display_char);
        }
        printf("\n");
    }
}

void SetMine(GameState* gs) {
    int count = gs->config.mineCount;
    while (count) {
        int x = rand() % gs->config.row + 1;
        int y = rand() % gs->config.col + 1;

        if (gs->mine[x][y] == '0') {
            gs->mine[x][y] = '1';
            count--;
        }
    }
}

void GameLoop(GameState* gs) {
    int revealed_count = 0;

    DisplayBoard(gs);
    printf("Time elapsed: 0 seconds.\n");

    while (revealed_count < gs->config.row * gs->config.col - gs->config.mineCount) {
        int x = 0, y = 0, action_choice = 0;
        printf("Please enter the coordinates and action (row col 1:Reveal 2:Flag):>");
        if (scanf("%d %d %d", &x, &y, &action_choice) != 3) {
            while (getchar() != '\n');
            printf("Invalid input format. Please enter three numbers (e.g., 1 1 1).\n");
            continue;
        }
        PlayerAction action = (PlayerAction)action_choice;

        if (x < 1 || x > gs->config.row || y < 1 || y > gs->config.col) {
            printf("Coordinates are illegal, re-enter.\n");
            continue;
        }

        if (action == ACTION_REVEAL) {
            if (gs->show[x][y] == '#') {
                printf("This coordinate is flagged. Unflag it first to reveal.\n");
                continue;
            }
            if (gs->show[x][y] != '*') {
                printf("This coordinate has already been revealed.\n");
                continue;
            }

            if (gs->mine[x][y] == '1') {
                time_t end_time = time(NULL);
                printf("\n--- Game Over! ---\n");
                printf("Final Time: %.0f seconds.\n", difftime(end_time, gs->startTime));
                printf("I'm sorry you got blown up.\n");
                DisplayAnswerBoard(gs);
                return;
            }
            ExpandMine(gs, x, y);
        }
        else if (action == ACTION_FLAG) {
            if (gs->show[x][y] == '*') {
                gs->show[x][y] = '#';
            }
            else if (gs->show[x][y] == '#') {
                gs->show[x][y] = '*';
            }
            else {
                printf("This coordinate has been revealed and cannot be flagged.\n");
            }
        }
        else {
            printf("Invalid action. Please use 1 to reveal or 2 to flag.\n");
            continue;
        }

        int current_revealed = 0;
        for (int i = 1; i <= gs->config.row; i++) {
            for (int j = 1; j <= gs->config.col; j++) {
                if (gs->show[i][j] != '*' && gs->show[i][j] != '#') {
                    current_revealed++;
                }
            }
        }
        revealed_count = current_revealed;

        DisplayBoard(gs);
        time_t current_time = time(NULL);
        printf("Time elapsed: %.0f seconds.\n", difftime(current_time, gs->startTime));
    }

    time_t end_time = time(NULL);
    printf("\n--- You Win! ---\n");
    printf("Final Time: %.0f seconds.\n", difftime(end_time, gs->startTime));
    printf("Congratulations on the demining.\n");
    DisplayAnswerBoard(gs);
}

void ExpandMine(GameState* gs, int x, int y) {
    if (x < 1 || x > gs->config.row || y < 1 || y > gs->config.col || gs->show[x][y] != '*') {
        return;
    }

    int count = GetMineCount(gs, x, y);
    if (count > 0) {
        gs->show[x][y] = count + '0';
    }
    else {
        gs->show[x][y] = '0';
        for (int i = x - 1; i <= x + 1; i++) {
            for (int j = y - 1; j <= y + 1; j++) {
                if (i == x && j == y) {
                    continue;
                }
                ExpandMine(gs, i, j);
            }
        }
    }
}

细节讨论:

1. DisplayBoard 与 DisplayMineBoard 的代码重复

  • 细节:DisplayBoard 与 DisplayMineBoard 两个函式在结构上几乎完全相同,都包含列印标题、行号、列号以及遍历棋盘的逻辑。唯一区别是内部 for 循环中获取 display_char 的方式不同。
  • 讨论:可以考虑将这两个函式合并唯一,通过传入一个新参数来获取要显示的字元。

2. 轮回展开效率

  • 细节:目前使用的 ExpandMine 递归展开在棋盘非常大且空白区域极多时,深度的递归可能造成堆栈溢出 (Stack Overflow) 的风险。
  • 讨论:可以尝试使用队列 (Queue) 结构的广度有限搜寻 (BFS) 算法来替代递归。

3. GetMineCount 计算方式

  • 细节:GetMineCount 是在需要时及时计算某个格子周围的地雷数。
  • 讨论:可以尝试预先计算所有非地雷格子的周围地雷数,并将结果储存在另一个额外阵列或 mine 阵列的 '0' 位置上。

结语

从一个简单的想法到这个结构良好、功能完备的应用程序,这个过程本身就是编程最大的乐趣所在。对我而言,这不仅是关于语法和算法,更是关于如何分析问题、权衡不同的实现方案、并在遇到困难时如何思考和调试。希望我分享的这次旅程和其中的思考过程能对您有所启发。继续编码,继续创造!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值