FeH2的博客

静态Patch优化扫雷的地雷布置算法

在经典的扫雷游戏中,玩家常常会遇到一个令人沮丧的情况:即使运用了所有可能的推理技巧,仍然不得不纯靠运气来选择下一步。这种"不得不猜"的设计一直是扫雷游戏最为人诟病的缺陷之一。虽然市面上已经有了一些无猜版本的扫雷实现,但我对Windows 7版本扫雷中那个经典的界面情有独钟。于是萌生了一个想法:能否在保留原版界面的基础上,将其改造成一个真正的"逻辑游戏"? 逆向工程分析 由于无法获取原版扫雷的源代码,我们需要通过逆向工程来理解和修改游戏逻辑。所幸这款游戏自带了调试符号(PDB文件),这大大简化了我们的分析工作。 使用IDA进行初步分析 使用IDA Pro打开Minesweeper.exe,IDE自动下载了对应的PDB文件。通过符号信息,我们可以直接定位到关键函数Board::Board: __int64 __fastcall Board::Board(__int64 a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, char a9) { // ... 初始化代码省略 ... if ( *(_DWORD *)(a1 + 8) > *(_DWORD *)(a1 + 12) * *(_DWORD *)(a1 + 16) - 9 ) { v22 = Str::Str((Str *)v25, L"Too many mines for tile count"); LOBYTE(v23) = 1; StrErr(v22, v23); *(_DWORD *)(a1 + 8) = *(_DWORD *)(a1 + 12) * *(_DWORD *)(a1 + 16) - 9; } return a1; } 通过分析反汇编代码,我们可以推断出Board类的主要结构。特别值得注意的是,代码中包含了一个边界检查,确保地雷数量不会超过(width * height - 9)个,这个"-9"正好对应着首次点击时需要预留的3×3空白区域。 深入分析地雷布置算法 进一步分析发现,核心的地雷布置逻辑位于Board::placeMines函数中。经过分析和代码美化,其基本逻辑如下: void Board::placeMines(int firstX, int firstY) { // 保存当前随机种子状态 unsigned int oldSeed = Seed; srand(this->randSeed); Seed = this->randSeed; // 创建可放置地雷的位置列表 Array<int>* availablePositions = new Array<int>(); // 收集所有合法的地雷放置位置 // 排除首次点击的3×3范围 for(int i = 0; i < height * width; i++) { int x = i % width; int y = i / width; int dx = x - firstX; int dy = y - firstY; if(abs(dx) > 1 || abs(dy) > 1) { availablePositions->Add(i); } } // 随机选择指定数量的位置放置地雷 while(minePositions->size < mineCount && availablePositions->size > 0) { int randomIndex = rand() % availablePositions->size; int position = availablePositions->data[randomIndex]; minePositions->Add(position); // ... 从可用位置列表中移除已选位置 ... } // 恢复随机种子 srand(oldSeed); Seed = oldSeed; } 分析这段代码,我们可以看出: ...

十二月 24, 2024 · 6 分钟 · FeH2