前言

前段时间接到一个有趣的外包项目,客户提供了一个基础的扫雷游戏代码,希望我为其添加AI辅助功能。经过需求分析和设计,我实现了一个智能提示系统:通过鼠标滚轮触发AI分析,自动标记确定的地雷位置或高亮显示安全格子。本文将详细记录这个项目的实现过程,分享技术细节和开发心得。

项目环境: C++ , EGE图形库


一、项目需求分析

1.1 原始需求

客户提供的是一个10×10的经典扫雷游戏,具备基本功能:

  • 左键点击翻开格子
  • 右键插旗标记
  • 自动展开空白区域
  • 游戏胜负判定

1.2 新增需求

客户希望添加AI辅助功能,但没有明确具体实现方式。经过思考,我设计了以下方案:

核心功能: 点击鼠标滚轮(中键)触发AI分析

  • 智能标旗: 自动为100%确定是地雷的格子插旗
  • 安全提示: 高亮显示100%安全的格子,供玩家点击

这种设计既能辅助玩家,又不会完全自动化游戏,保持了游戏的趣味性。


二、技术方案设计

2.1 AI算法核心思路

扫雷游戏的AI辅助本质上是逻辑推理问题。我采用了两种经典的扫雷推理规则:

规则一:安全格子识别

逻辑: 如果一个已翻开的数字格周围的旗子数量 = 该数字,则其余未翻开的格子都是安全的。

示例:

[?] [🚩] [?]
[🚩] [2] [?]
[?] [?] [?]

中间格子显示数字2,周围已有2个旗子,那么其余的?格子都是安全的。

规则二:地雷格子识别

逻辑: 如果一个已翻开的数字格周围的(未翻开格子数 + 旗子数)= 该数字,则所有未翻开的格子都是地雷。

示例:

[?] [?] [已翻]
[🚩] [3] [已翻]
[已翻] [已翻] [已翻]

中间格子显示数字3,周围有1个旗子+2个未翻开格子=3,所以这2个?都是地雷。

2.2 数据结构设计

游戏使用二维数组map[Row][COL]存储游戏状态,采用了巧妙的编码方式:

数值范围 含义
-1 已翻开的地雷(游戏结束)
0-8 已翻开的数字格(周围地雷数量)
19-28 未翻开的格子(实际值-20=真实内容)
39+ 已插旗的格子(实际值-40=真实内容)

这种编码方式通过数值范围区分格子状态,避免了使用多个数组,代码更简洁。


三、核心代码实现

3.1 AI决策函数

struct AISuggestion {
    int row = -1;
    int col = -1;
    int moveType = 0; // 0:无建议, 1:安全格子, 2:地雷格子
};

AISuggestion findBestMove(int map[][COL]) {
    AISuggestion suggestion;

    // 第一优先级:寻找绝对安全的格子(规则一)
    for (int r = 0; r < Row; r++) {
        for (int c = 0; c < COL; c++) {
            if (map[r][c] >= 1 && map[r][c] <= 8) {
                int num = map[r][c];
                int hiddenNeighbors = 0;
                int flaggedNeighbors = 0;

                // 统计周围格子状态
                for (int i = r - 1; i <= r + 1; i++) {
                    for (int j = c - 1; j <= c + 1; j++) {
                        if (i >= 0 && i < Row && j >= 0 && j < COL) {
                            if (map[i][j] >= 19 && map[i][j] <= 28) 
                                hiddenNeighbors++;
                            else if (map[i][j] >= 39) 
                                flaggedNeighbors++;
                        }
                    }
                }
                
                // 规则一:旗子数=数字,剩余格子安全
                if (num == flaggedNeighbors && hiddenNeighbors > 0) {
                    for (int i = r - 1; i <= r + 1; i++) {
                        for (int j = c - 1; j <= c + 1; j++) {
                            if (i >= 0 && i < Row && j >= 0 && j < COL) {
                                if (map[i][j] >= 19 && map[i][j] <= 28) {
                                    suggestion.row = i;
                                    suggestion.col = j;
                                    suggestion.moveType = 1; // 安全格子
                                    return suggestion;
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    // 第二优先级:寻找绝对是地雷的格子(规则二)
    for (int r = 0; r < Row; r++) {
        for (int c = 0; c < COL; c++) {
            if (map[r][c] >= 1 && map[r][c] <= 8) {
                int num = map[r][c];
                int hiddenNeighbors = 0;
                int flaggedNeighbors = 0;

                for (int i = r - 1; i <= r + 1; i++) {
                    for (int j = c - 1; j <= c + 1; j++) {
                        if (i >= 0 && i < Row && j >= 0 && j < COL) {
                             if (map[i][j] >= 19 && map[i][j] <= 28) 
                                 hiddenNeighbors++;
                             else if (map[i][j] >= 39) 
                                 flaggedNeighbors++;
                        }
                    }
                }
                
                // 规则二:未翻开+旗子=数字,未翻开的都是雷
                if (num == hiddenNeighbors + flaggedNeighbors && hiddenNeighbors > 0) {
                    for (int i = r - 1; i <= r + 1; i++) {
                        for (int j = c - 1; j <= c + 1; j++) {
                            if (i >= 0 && i < Row && j >= 0 && j < COL) {
                                if (map[i][j] >= 19 && map[i][j] <= 28) {
                                    suggestion.row = i;
                                    suggestion.col = j;
                                    suggestion.moveType = 2; // 地雷格子
                                    return suggestion;
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    return suggestion; // 没找到建议
}

3.2 鼠标交互处理

在原有的左键、右键基础上,新增了中键(滚轮)的处理逻辑:

void mouseMsg(int map[][COL]) {
    mouse_msg msg = getmouse();
    int r = msg.y / ImgSize;
    int c = msg.x / ImgSize;

    if (r < 0 || r >= Row || c < 0 || c >= COL) return;

    if (msg.is_left()) {
        // 左键点击逻辑(原有代码)
        highlightRow = -1;
        highlightCol = -1;
        // ...
    }
    else if (msg.is_right()) {
        // 右键插旗逻辑(原有代码)
        // ...
    }
    else if (msg.is_mid()) {  // 新增:中键触发AI
        AISuggestion aiMove = findBestMove(map);
        
        if (aiMove.moveType == 1) {  // 找到安全格子
            highlightRow = aiMove.row;
            highlightCol = aiMove.col;
        }
        else if (aiMove.moveType == 2) {  // 找到地雷格子
            // 自动插旗
            if (map[aiMove.row][aiMove.col] >= 19 && map[aiMove.row][aiMove.col] <= 28) {
                PlaySound("./music/click.wav", NULL, SND_ASYNC | SND_FILENAME);
                map[aiMove.row][aiMove.col] += 20;
            }
        }
    }
}

3.3 高亮显示实现

为了让玩家清楚看到AI推荐的安全格子,我添加了高亮显示功能:

// 全局变量
int highlightRow = -1;
int highlightCol = -1;

void draw(int map[][COL]) {
    for (int i = 0; i < Row; i++) {
        for (int k = 0; k < COL; k++) {
            // 高亮显示AI推荐的格子
            if (i == highlightRow && k == highlightCol) {
                putimage(k * ImgSize, i * ImgSize, imgs[12]);  // 使用特殊图片
                continue;
            }
            
            // 正常绘制逻辑
            int index = -1;
            if (map[i][k] >= 0 && map[i][k] <= 8) index = map[i][k];
            else if (map[i][k] == -1) index = 9;
            else if (map[i][k] >= 19 && map[i][k] <= 28) index = 10;
            else if (map[i][k] >= 39) index = 11;
            
            if (index != -1) {
                putimage(k * ImgSize, i * ImgSize, imgs[index]);
            }
        }
    }
}

四、技术亮点与优化

4.1 优先级策略

AI决策采用了优先级机制:

  1. 优先推荐安全格子:让玩家主动点击,保持游戏参与感
  2. 其次自动标记地雷:减少重复操作,提升游戏体验

这种设计平衡了辅助性和游戏性。

4.2 视觉反馈

  • 高亮显示:使用特殊颜色(图片12)标记安全格子
  • 音效反馈:插旗时播放点击音效
  • 即时响应:点击左键后自动清除高亮,避免混淆

4.3 边界处理

代码中大量使用了边界检查:

if (i >= 0 && i < Row && j >= 0 && j < COL)

确保遍历周围格子时不会越界,这是扫雷游戏开发的常见陷阱。


五、测试与效果

5.1 功能测试

测试场景 预期结果 实际结果
中键点击-有安全格子 高亮显示安全格子 ✅ 通过
中键点击-有确定地雷 自动插旗 ✅ 通过
中键点击-无明确建议 无操作 ✅ 通过
左键点击后高亮消失 高亮清除 ✅ 通过
边界格子AI分析 不越界崩溃 ✅ 通过

5.2 实际效果

AI辅助功能显著提升了游戏体验:

  • 新手友好:不会因为猜测而频繁失败
  • 提高效率:自动标记地雷节省时间
  • 学习价值:玩家可以通过AI提示学习扫雷技巧

六、项目总结与反思

6.1 收获与成长

  1. 需求分析能力:从模糊需求中提炼出合理的功能设计
  2. 算法应用:将扫雷逻辑规则转化为代码实现
  3. 用户体验思维:平衡辅助功能与游戏趣味性

6.2 可优化方向

如果有更多时间,可以进一步优化:

  1. 概率计算:当无法100%确定时,计算每个格子是地雷的概率
  2. 多步推理:结合多个数字格进行复杂推理
  3. AI难度选择:提供"提示"、“半自动”、"全自动"三种模式

6.3 开发心得

这个项目虽然规模小,但涉及了:

  • 图形界面开发(EGE库的使用)
  • 游戏逻辑设计(状态管理、事件处理)
  • 算法实现(逻辑推理、遍历搜索)
  • 用户交互设计(鼠标事件、视觉反馈)

是一个很好的综合实战项目。


七、完整代码结构

扫雷/
├── main.cpp           # 主程序代码
├── images/            # 游戏图片资源
│   ├── 0.jpg         # 数字0-8的图片
│   ├── ...
│   ├── 9.jpg         # 地雷图片
│   ├── 10.jpg        # 未翻开格子
│   ├── 11.jpg        # 旗子图片
│   └── 12.jpg        # 高亮格子
├── music/             # 音效文件
│   ├── start.wav     # 开始音效
│   ├── click.wav     # 点击音效
│   ├── blank.wav     # 空白展开音效
│   └── gameover.wav  # 游戏结束音效
├── include/           # EGE图形库头文件
└── lib/               # EGE图形库链接库

八、运行说明

8.1 环境配置

  • 编译器:支持C++11的编译器(MinGW、MSVC等)
  • 图形库:EGE(Easy Graphics Engine)
  • 操作系统:Windows

8.2 编译运行

使用Dev-C++或Code::Blocks:

  1. 打开扫雷.dev项目文件
  2. 配置EGE库路径(include和lib)
  3. 编译运行

8.3 操作说明

  • 左键:翻开格子
  • 右键:插旗/取消插旗
  • 中键(滚轮):触发AI辅助
    • 自动标记确定的地雷
    • 高亮显示安全的格子

结语

这个项目让我深刻体会到,好的功能设计不仅要实现需求,更要考虑用户体验。AI辅助功能既要帮助玩家,又不能剥夺游戏乐趣,这个平衡点的把握是项目成功的关键。

通过这次外包项目,我不仅巩固了C++编程能力,还锻炼了需求分析、算法设计、用户体验等综合能力。希望这篇文章能为在学习游戏开发提供一些参考和启发。

如果觉得这个项目有趣,欢迎在评论区交流讨论! 🎮


项目标签: #C++ #游戏开发 #EGE图形库 #扫雷游戏 #AI算法

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐