基于C语言实现的经典扫雷游戏开发详解

摘要:本文将通过一个完整的C语言扫雷项目,讲解控制台小游戏的开发思路与实现细节。项目采用多文件编程,支持三种难度级别,涵盖二维数组操作、随机数生成、递归算法等核心知识点。

一、游戏设计思路

1.1 核心规则

  • 9x9方格(简单模式)埋藏10个雷

  • 玩家通过坐标翻开格子

  • 踩雷即失败,显示全部雷区

  • 非雷格子显示周围8格雷数

  • 标记所有雷即胜利

二、关键技术实现

2.1 双棋盘设计

// 使用ROWS/COLS宏定义解决边界检测问题
#define ROWS 20
#define COLS 30

char mine[ROWS][COLS]; // 雷区实际数据
char show[ROWS][COLS]; // 玩家可见界面

优势:通过创建两个二维数组,分别存储真实雷区数据和玩家可见界面,实现数据与显示的分离。

2.2 智能初始化

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

说明:通过参数化初始化字符,灵活创建不同状态的棋盘('0'表示无雷,'1'表示有雷,'*'表示未翻开)

三、核心算法解析

3.1 随机布雷

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

关键点:通过循环+随机数生成实现不重复布雷,确保首步安全(可优化点)

3.2 雷数计算

int GetMineCount(char mine[ROWS][COLS], int x, int y) {
    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');
}

创新点:利用ASCII码特性将字符计算转换为数值计算


四、功能扩展建议

4.1 递归展开空白区域

void ExpandEmpty(char mine[ROWS][COLS], char show[ROWS][COLS], 
                int x, int y) {
    if(x<1 || x>ROW || y<1 || y>COL) return;
    if(show[x][y] != '*') return;
    
    int count = GetMineCount(mine, x, y);
    if(count > 0) {
        show[x][y] = count + '0';
        return;
    }
    
    show[x][y] = ' ';
    // 递归展开周围8格
    ExpandEmpty(mine, show, x-1, y);
    ExpandEmpty(mine, show, x+1, y);
    // ...其他6个方向
}

效果:实现类似Windows扫雷的空白区域自动展开效果

4.2 优化建议列表

  1. 增加标记旗帜功能

  2. 添加计时器和计步器

  3. 实现保存/读取游戏进度

  4. 使用图形库升级界面


五、编译与测试

5.1 环境要求

  • Windows/Linux系统

  • 支持C99标准的编译器

  • 建议使用VS2019/CLion进行开发

5.2 常见问题排查

# 遇到数组越界错误时检查:
1. 全局变量ROW/COL是否正确初始化
2. 数组索引是否在有效范围内
3. 边界格子的雷数计算是否正确

六、功能扩展

6.1 增加标记旗帜功能

方法一 通过增加选择步骤实现
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
    int x = 0;
    int y = 0;
    int win = 0;
    int action = 0; // 新增:操作类型变量
    
    while (win < row * col - MINES)
    {
        printf("请选择操作:\n1.排雷 2.标记/取消标记\n");
        printf("请输入操作编号:>");
        scanf("%d", &action);
        
        if (action != 1 && action != 2) {
            printf("无效操作,请重新输入\n");
            continue;
        }
        
        printf("请输入要操作的坐标(x y):>");
        scanf("%d %d", &x, &y);
        
        if (x < 1 || x > row || y < 1 || y > col) {
            printf("坐标非法,重新输入\n");
            continue;
        }

        switch (action)
        {
        case 1: // 排雷操作
            if (show[x][y] == 'F') {
                printf("该位置已标记,请先取消标记\n");
                break;
            }
            if (mine[x][y] == '1') {
                printf("很遗憾,你被炸死了\n");
                DisplayBoard(mine, row, col);
                return;
            } else {
                int count = GetMineCount(mine, x, y);
                show[x][y] = count + '0';
                DisplayBoard(show, row, col);
                win++;
            }
            break;
            
        case 2: // 标记操作
            if (show[x][y] == '*') {
                show[x][y] = 'F'; // 标记为旗帜
            } else if (show[x][y] == 'F') {
                show[x][y] = '*'; // 取消标记
            } else {
                printf("该位置无法标记\n");
            }
            DisplayBoard(show, row, col);
            break;
            
        default:
            break;
        }
    }
    
    if (win == row * col - MINES) {
        printf("恭喜你,排雷成功\n");
        DisplayBoard(mine, row, col);
    }
}
方法二 通过输入负坐标实现

在 FindMine 中增加对旗帜标记的处理

void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
    int x = 0, y = 0;
    int win = 0, flag_count = 0;

    printf("提示:输入负坐标(如 -3 5)标记旗帜,输入正坐标(如 3 5)排雷。\n");

    while (win < (row * col - MINES) || flag_count < MINES)
    {
        printf("请输入坐标(x y):>");
        scanf("%d %d", &x, &y);

        // 处理负坐标标记旗帜
        if (x < 0)
        {
            x = -x;
            if (x >= 1 && x <= row && y >= 1 && y <= col)
            {
                if (show[x][y] == '*') // 仅允许在未翻开的位置标记
                {
                    show[x][y] = 'F';
                    flag_count++;
                    printf("已标记旗帜\n");
                }
                else if (show[x][y] == 'F') // 撤销标记
                {
                    show[x][y] = '*';
                    flag_count--;
                    printf("撤销标记旗帜\n");
                }
                else
                {
                    printf("该位置无法标记\n");
                }
                DisplayBoard(show, row, col);
            }
            else
            {
                printf("坐标非法,重新输入\n");
            }
        }
        // 处理正坐标排雷
        else if (x >= 1 && x <= row && y >= 1 && y <= col)
        {
            if (show[x][y] == 'F')
            {
                printf("该位置已标记,请先取消标记\n");
            }
            else if (mine[x][y] == '1')
            {
                printf("很遗憾,你被炸死了\n");
                DisplayBoard(mine, row, col);
                break;
            }
            else
            {
                int count = GetMineCount(mine, x, y);
                show[x][y] = count + '0';
                DisplayBoard(show, row, col);
                win++;
            }
        }
        else
        {
            printf("坐标非法,重新输入\n");
        }

        // 检查胜利条件
        if (CheckWin(show, row, col, MINES, flag_count))
        {
            printf("恭喜你,排雷成功\n");
            DisplayBoard(mine, row, col);
            break;
        }
    }
}

DisplayBoard 中显示旗帜

void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
    int i = 0;
    printf("--------扫雷游戏-------\n");
    for (i = 0; i <= col; i++) {
        printf("%d ", i);
    }
    printf("\n");
    for (i = 1; i <= row; i++)
    {
        printf("%d ", i);
        int j = 0;
        for (j = 1; j <= col; j++)
        {
            // 显示旗帜标记
            if (board[i][j] == 'F')
            {
                printf("F ");
            }
            else
            {
                printf("%c ", board[i][j]);
            }
        }
        printf("\n");
    }
}

main.c 中记录旗帜标记计数

void game()
{
    char mine[ROWS][COLS]; // 存放布置好的雷
    char show[ROWS][COLS]; // 存放排查出的雷的信息
    int flag_count = 0; // 添加旗帜计数

    // 初始化棋盘
    InitBoard(mine, ROWS, COLS, '0');
    InitBoard(show, ROWS, COLS, '*');

    // 打印棋盘
    DisplayBoard(show, ROW, COL);

    // 布置雷
    SetMine(mine, ROW, COL);

    // 排查雷
    FindMine(mine, show, ROW, COL);
}

新增检查胜利条件的函数

int CheckWin(char show[ROWS][COLS], int row, int col, int mines, int flags)
{
    int unopened = 0;
    for (int i = 1; i <= row; i++)
    {
        for (int j = 1; j <= col; j++)
        {
            if (show[i][j] == '*' || show[i][j] == 'F')
            {
                unopened++;
            }
        }
    }
    return (unopened == mines && flags == mines);
}
功能性对比

方案一

  • 优点:通过菜单选择操作类型(排雷/标记),逻辑分离清晰,适合需要明确操作步骤的场景。

  • 缺点

    • 未实现完整的胜利条件:仅依赖排雷完成判定胜利,未验证标记是否全部正确(传统扫雷中正确标记所有雷也是胜利条件之一)。

    • 操作冗余:用户需要多一步选择操作类型(输入 1 或 2),增加了操作成本。

方案二

  • 优点

    • 完整的胜利条件:要求用户正确标记所有雷排完所有非雷区域,符合经典扫雷规则。

    • 输入更简洁:通过负坐标(如 -3 5)触发标记功能,无需额外选择操作类型,减少用户输入步骤。

  • 缺点

    • 负坐标可能不够直观:用户需要学习输入负坐标的规则,可能对新手不友好

项目源码:Project seek bomb/Project seek bomb · lumos_tq/C language - 码云 - 开源中国

大概半年内会将建议优化列表想办法慢慢完善

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值