ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MFC围棋双人对弈源码解析:VC++6.0工程移植与落子算法实现

MFC围棋双人对弈源码解析:VC++6.0工程移植与落子算法实现 简介这是一份基于Visual C与MFC框架编写的双人对弈围棋程序源码包面向学习VC游戏开发的初学者及希望深入理解棋类编程的开发者。压缩包共24个文件主要由7个头文件.h、6个C源文件.cpp构成另含3个位图、2个图标、1个资源脚本.rc/.rc2及Visual C工程文件.dsw/.dsp/.aps/.clw完整保留了MFC项目的标准结构可直接编译运行。源码中I_goView、I_goDoc、MainFrm等类的划分清晰配合Toolbar、toRight、toLeft位图资源展示了窗口框架搭建、棋盘绘制与鼠标事件响应的具体做法。程序支持两种尺寸棋盘突出快速落子节奏涉及合法性检查、胜负判定及可能的搜索优化是研究棋类对弈逻辑的直观样例。目前已有260人学习下载适合通过逐文件阅读和调试掌握MFC界面设计、文档视图分离及棋局状态管理等实用技能。1. 先说清楚这个 go 是什么VC 6.0 时代的双人围棋源码包搜索 go 这个词的人多半想看 Go 语言怎么装环境或者 Win to Go 这种工具盘这份 I_go.rar 里的 go 是围棋的 go——一个用 VC 6.0 写的双人对弈围棋程序。压缩包里没有打包好的 exe而是一整套 MFC 工程源码I_go.dsp、I_goView.cpp、I_goDoc.cpp、Grafic.cpp外加一组图标和工具栏位图。它最实在的用处是三个第一次做 MFC 的开发者能拿到一个结构完整的窗口程序当模板要做课设的人能省掉画棋盘、写落子校验的重复劳动想搞博弈算法的人能先把棋盘状态管理跑通再往上加搜索。适合的人群很明确VC 入门者、MFC 课设选手、以及所有想把围棋从棋盘游戏变成代码的从业者。下面的内容我不会去复述什么是围棋规则而是按工程拆解的顺序把这份资粮里真正能复用的部分刨出来。2. 从 I_go.dsp 看工程骨架MFC 单文档程序的文件结构与编译流程2.1 压缩包里的每一个文件是干什么的把 I_go.rar 解压之后第一眼是十来个文件后缀从 .cpp、.h 到 .dsp、.dsw 再到 .bmp、.ico 都有。很多新手拿到这种老工程会直接双击 .dsw发现打不开就以为包坏了。其实这套文件是一个标准 VC 6.0 MFC 单文档SDI工程的完整骨架先分清角色再动手后面会省很多事。文件角色说明I_go.dsw / I_go.dsp工作区与工程文件VC 6.0 的工程配置新版 Visual Studio 不直接支持I_go.h / I_go.cpp应用类继承 CWinApp负责程序初始化和主窗口创建MainFrm.h / MainFrm.cpp主框架窗口管理工具栏、状态栏、菜单是 View 的容器I_goDoc.h / I_goDoc.cpp文档类存棋盘状态、当前手数、对局数据是逻辑层I_goView.h / I_goView.cpp视图类负责显示棋盘、接收鼠标输入、调用 Grafic 绘制Grafic.h / Grafic.cpp绘图支持棋盘网格、棋子、位图的绘制封装StdAfx.h / StdAfx.cpp预编译头MFC 标准头文件汇总编译第一遍最久的就是它resource.h / I_go.rc / res\I_go.rc2资源脚本菜单、对话框、字符串、图标、位图的声明与引用I_go.ico / I_goDoc.ico / Toolbar.bmp / toRight.bmp / toLeft.bmp图标与位图程序图标、文档图标、工具栏按钮、左右箭头按钮I_go.aps / I_go.clw向导缓存VS 资源编辑器生成物可以安全删除这里有一个容易误判的点I_go.aps 和 I_go.clw 不是源码是资源编辑器留下的缓存。我一般拿到手会先把这两类生成物清掉避免之后在 Git 里造成无意义的冲突。源码和资源则按类型归档# 源码、头文件、资源脚本、位图分目录存放方便后续维护 mkdir -p I_go_src I_go_res cp *.cpp *.h I_go_src/ # 所有 .cpp 和 .h 收进源码目录 cp *.rc resource.h I_go_res/ # 资源脚本单独放.rc 是文本格式可直接读 cp *.bmp *.ico I_go_res/ # 位图和图标单独放 rm -f I_go.aps I_go.clw # 删除向导缓存不影响编译这样分类之后要注意一个问题I_go.rc 内部通常用相对路径引用资源比如res\\I_go.rc2、I_go.ico。如果把 .rc 挪了目录但没有同步修改 .rc 里的路径编译时资源编译器会报cannot open file。分完目录后先打开 I_go.rc 看一眼里面的路径前缀再决定是保持原结构还是整体搬。2.2 让老工程在新版 Visual Studio 里重新站起来VC 6.0 的 .dsp / .dsw 是九十年代末的工程格式VS2012 之后就不再支持直接打开VS2022 双击只会提示格式不兼容。想在现在的环境里把它跑起来我的习惯是“新建壳、换核心”用新版 VS 创建一个同样名字的 MFC 单文档工程然后把老源码文件替换进去而不是尝试转换工程格式。# 1. 安装 VS2022(或 2019) 时必须勾选两个关键组件 # - 使用 C 的桌面开发 # - 适用于最新 v143 生成工具的 C MFC (x86 和 x64) # 2. 用 VS 创建新项目搜索 MFC 应用 - 应用程序类型选“单个文档” # 3. 将新工程里的这四个文件替换为老源码 cp I_go_src/I_goView.cpp 新工程目录/I_goView.cpp cp I_go_src/I_goView.h 新工程目录/I_goView.h cp I_go_src/I_goDoc.cpp 新工程目录/I_goDoc.cpp cp I_go_src/Grafic.cpp 新工程目录/Grafic.cpp # 4. 资源文件不要混用老 .rc 里的位图路径不一样建议重新添加这个方案比强行打开 .dsw 稳得多。原因在于 MFC 的工程结构三十年来变化不大Doc 管数据、View 管显示、MainFrm 管框架。新版工程生成的文件骨架和老代码基本能对应上。唯一麻烦的是资源部分老工程里自定义的菜单 ID、位图 ID 在 resource.h 里定义替换时要么把 resource.h 里的#define合并进来要么就用新版资源编辑器重新拖一份位图我一般选后者省得 ID 冲突。编译时还有两个高频错误提前说老 C 代码里for(int i0; in; i)这种写法在 VS2022 的默认标准下没问题但char*到LPCTSTR的隐式转换会报错因为新版工程默认字符集是 Unicode。解法是在工程属性 → 配置属性 → 高级 → 字符集中把“使用 Unicode 字符集”改成“使用多字节字符集”或者逐个加_T()宏。2.3 MainFrm、I_goView、I_goDoc 各管一摊MFC 单文档程序的运行流程是一条固定链路I_goApp 初始化时创建 MainFrameMainFrame 内部创建 I_goView 作为客户区I_goDoc 负责保存棋盘数据I_goView 通过GetDocument()拿到这份数据。理解这个三角关系比看懂任何一个具体函数都重要。// I_goView.cpp 里典型的取数据入口 CI_goDoc* pDoc GetDocument(); // View 拿到 Doc 指针 ASSERT_VALID(pDoc); int color pDoc-currentPlayer; // 当前执子方, 1黑 -1白 pDoc-TryPlaceStone(m_clickX, m_clickY); // 落子逻辑在 Doc 里做这套分工是双人对弈程序最顺的写法View 只做两件事——把鼠标坐标换算成棋盘坐标、调 Invalidate 触发重绘Doc 管全部棋局规则——落子、提子、判胜负。如果代码里把落子逻辑写进 View 的 OnLButtonDown短期能用后面一旦要加悔棋、复盘或人机就会改成麻烦。3. 双人对弈怎么落子棋盘状态、坐标换算与合法性检查3.1 棋盘状态二维数组和“该谁走”变量围棋程序的棋盘状态核心就是一张二维表坐标从 0 到 1819 路盘。I_go 既然是双人对弈逻辑上只需要区分空、黑、白三种状态。用 0、1、-1 这种取值有一个额外好处换手时只要写currentPlayer -currentPlayer不用做任何分支判断。// 棋盘数据结构常见做法是直接在 Doc 类里定义 class CGoBoard { public: static const int MAX_BOARD 19; // 预留最大棋盘 19 路 int grid[MAX_BOARD][MAX_BOARD]; // 0空, 1黑, -1白 int size; // 当前对局路数, 9/13/19 int currentPlayer; // 当前轮到谁: 1 黑, -1 白 int moveCount; // 总手数, 用于判超时和复盘 void SwitchTurn() { currentPlayer -currentPlayer; } bool IsEmpty(int x, int y) { return grid[x][y] 0; } bool InBoard(int x, int y) { return x 0 x size y 0 y size; } };这里的size字段对应 I_go 提供的“两种棋盘尺寸”功能。数组按 19 路最大值分配size 只决定哪些坐标有效这样切换路数时不需要重新分配内存只要把绘制和落子判断的范围收紧到 size 以内。3.2 鼠标点到棋盘上坐标换算与容差判断双人对弈最影响手感的代码是“落子吸附”——鼠标点下去的那一下不能点在屏幕像素坐标上要换算成最接近的棋盘交叉点而且不能点在棋盘的边缘留白区。// 把鼠标点换算成棋盘交叉点坐标px/py 为输出的格点坐标 bool ScreenToBoard(CPoint p, CRect rc, int boardSize, int px, int py) { int margin 20; // 棋盘四周留白必须和绘图时一致 int span rc.Width() - 2 * margin; // 棋盘可绘制区域宽度 if (span 0) return false; double step (double)span / (boardSize - 1); // 交叉点间距 int cx (int)((p.x - rc.left - margin) / step 0.5); int cy (int)((p.y - rc.top - margin) / step 0.5); // 超出棋盘范围直接拒绝 if (cx 0 || cy 0 || cx boardSize || cy boardSize) return false; // 四舍五入后的像素位置 int sx rc.left margin (int)(cx * step); int sy rc.top margin (int)(cy * step); // 点得离交叉点太远(超过半个格距)不落子防止误触 if (abs(p.x - sx) 12 || abs(p.y - sy) 12) return false; px cx; py cy; return true; }这段代码里最容易翻车的参数是margin。如果画棋盘时网格起点是rc.left margin落子换算也必须用同一个 margin两边差一个像素都会让人感觉“点得不准”。我见过不少改动是绘图时把边距从 20 改成 15忘了同步改换算函数结果所有落子整体偏移 5 像素。3.3 落子合法性禁着点、提子与劫的判别顺序围棋判定逻辑分三层每层都依赖前一层。第一步检查目标位置是否为空第二步做临时落子看己方新棋串有没有气第三步针对四邻的对方棋串数气气为 0 就提掉。这里用到的搜索算法并不是 Alpha-Beta 这类博弈搜索而是从当前点向四个方向扩散的 DFS 泛洪。// 数气从 (x,y) 开始统计同色连通块有多少个空邻点 int CountLiberties(int x, int y, int color, bool visited[MAX_BOARD][MAX_BOARD]) { int lib 0; const int dx[4] {1, -1, 0, 0}; const int dy[4] {0, 0, 1, -1}; for (int i 0; i 4; i) { int nx x dx[i], ny y dy[i]; if (!InBoard(nx, ny)) continue; if (grid[nx][ny] 0) { lib; // 发现一口气 } else if (grid[nx][ny] color !visited[nx][ny]) { visited[nx][ny] true; lib CountLiberties(nx, ny, color, visited); } } return lib; }落子后的完整处理顺序是临时放下棋子 → 数相邻对方棋串的气气为 0 则提掉 → 数己方新棋串的气若为 0 则回退这一步自杀→ 一切正常就把当前手作为正式落子。注意“劫”的判断需要额外记录上一手落点如果这一步导致棋盘状态和上一手完全一致规则上要禁止。这是双人对弈模式里最容易漏掉的一条很多教学版围棋程序都会把劫漏掉导致来回提子的死循环。4. 速度从哪来不是 AlphaGo是无锁状态更新与 GDI 直绘4.1 “搜索算法”的双人模式真相DFS 数气很多资料在介绍围棋程序时会习惯性写上“应用了 Alpha-Beta 剪枝搜索”这类话但在这个 I_go 双人模式里真正跑在落子路径上的搜索只是上面那段数气的 DFS复杂度是 O(连通块大小)效率极高局面上几乎所有气都能一次遍历数完。Alpha-Beta 剪枝这种东西在人机对战里才有用双人对弈阶段代码里大概率没做。下棋“速度快”的原因其实比搜索算法更简单落子就是一次数组写入提子就是一次数组清零没有任何 AI 层面的局面展开。19 路棋盘最多 361 个点DFS 遍历一个连通块的耗时在微秒级人的手速远远构不成瓶颈。如果你在阅读这份源码时带着“找高级搜索算法”的预期大概率会扑空——但这恰恰是它好的地方把基础状态管理写得足够干净后续加任何搜索算法都容易。4.2 GDI 直绘与那两个箭头位图不要小看 Toolbar.bmpI_go 的界面用到一组位图资源Toolbar.bmp 是工具栏按钮图toRight.bmp 和 toLeft.bmp 是两个方向箭头。从位图命名推断这两个箭头是用来切换棋盘尺寸或翻看历史步骤的。MFC 里工具栏按钮的标准做法是给按钮挂位图索引在 MainFrm 里创建工具栏时按顺序绑定。// MainFrm.cpp 里典型的工具栏按钮位图绑定 static UINT indicators[] { ID_SEPARATOR, ID_TOOLBAR_RIGHT, // toRight.bmp 对应的命令 ID ID_TOOLBAR_LEFT // toLeft.bmp }; m_wndToolBar.LoadToolBar(IDR_MAINFRAME); m_wndToolBar.SetHeight(32); // 老程序默认 16/24 太矮新系统建议调高关于界面刷新的一个常识MFC 的双人对弈程序里落子后界面要立刻更新最直接的做法是调Invalidate(FALSE)让视图重绘。如果棋盘上有棋子层OnDraw 里是按数组状态从头画的所以刷新逻辑本身没有状态同步负担——只要你把 board 数组维护正确绘图函数永远只对当前状态负责。void CI_goView::OnDraw(CDC* pDC) { // 双缓冲: 先在内存 DC 画完整棋盘, 再一次 BitBlt 到屏幕, 避免闪烁 CRect rc; GetClientRect(rc); CDC memDC; CBitmap bmp; memDC.CreateCompatibleDC(pDC); bmp.CreateCompatibleBitmap(pDC, rc.Width(), rc.Height()); CBitmap* pOld memDC.SelectObject(bmp); DrawBoard(memDC, rc); // 画木纹底色、横竖线、星位 DrawStones(memDC, rc); // 按 board 数组循环画黑白棋 pDC-BitBlt(0, 0, rc.Width(), rc.Height(), memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOld); }这里BitBlt是最后一步落盘操作。双缓冲的目的是把绘图过程全部搬到内存里避免直接在屏幕 DC 上一根线一根线地画。老机器上这一步尤其重要因为 GDI 的逐笔绘制如果直接在屏幕 DC 上做窗口拖拽或频繁落子时会产生明显闪烁。4.3 两种棋盘尺寸像素映射与刷新边界I_go 支持两种棋盘尺寸最常见的组合是 19 路标准盘和 13 路小盘。切换尺寸时真正要改的地方只有两个board.size的值和绘制函数里boardSize参数。前面写的 ScreenToBoard 代码就接受boardSize参数所以切换只需要把新值传给所有函数而不需要改绘图循环本身。// 切换棋盘尺寸的常见做法: 在 View 里加一个响应函数 void CI_goView::OnChangeBoardSize() { CI_goDoc* pDoc GetDocument(); pDoc-size (pDoc-size 19) ? 13 : 19; // 19 换 13, 13 换 19 pDoc-InitializeBoard(); // 清空当前棋子 Invalidate(TRUE); // 触发全部重绘 }注意Invalidate(TRUE)和Invalidate(FALSE)的区别TRUE 会擦掉背景再重绘适合棋盘尺寸变化这种整体重画FALSE 保留背景适合落子这类局部变化。尺寸切换时必须用 TRUE否则旧棋盘线的残影会留在界面上。5. 避坑与排查让 MFC 围棋程序稳定跑起来的六个常见问题5.1 现象VC6 写的代码在 VS2022 编译报一堆 C4996、C2664 错误原因VC 6.0 的 C 标准是上个世纪的很多函数名和类型转换规则跟现代编译器不兼容典型的是strcpy这种 CRT 函数被标记为不安全以及char*自动转LPCTSTR因为字符集不同而失败。解决工程属性 → C/C → 预处理器 → 预处理器定义里加_CRT_SECURE_NO_WARNINGS再把字符集设为“使用多字节字符集”。如果还有for(int i0; ...)在循环外继续使用变量 i 的写法把循环变量移到外面声明。5.2 现象棋盘上的中文标题或按钮文字变成乱码原因老工程资源文件是 GB2312 编码VS2022 新建工程默认按 UTF-8 或 Unicode 处理字符串造成 .rc 里的中文串读成乱码。解决用记事本打开 .rc 文件另存为时选择“ANSI 编码”与老系统保持一致。如果新工程本身要支持中文建议统一用_T(中文)宏包住代码里的字符串字面量资源编辑器里的文字另存为 UTF-8 with BOM。5.3 现象落子点和鼠标点明显对不上整体偏移 35 像素原因绘图时的 margin 或 step 计算和 ScreenToBoard 里的不一致。这是围棋程序特有的“玄学”——两边各自用了一套边距编译不报错跑起来手感全偏。解决把 margin、span、step 这三个值抽成同一个函数或常量绘图和落子都调它。我一般直接在 CGoBoard 里加一个GetGridMetrics(CRect rc, double step, int margin)保证两处永远拿到同一组数。5.4 现象提子之后棋盘上残留棋子或者下一手落子位置被旧棋子挡住原因提子逻辑只改了 board 数组里对应坐标的值但没有触发视图重绘。或者更隐蔽——提子代码改了 board 数组但落子函数里先做临时判断判断失败后没有恢复数组原始值。解决提子结束加一行Invalidate(FALSE)落子回退时把临时写入的棋子坐标清回 0。建议在调试时用断言检查棋盘数组的总子数提交一次落子前后总数守恒。5.5 现象双击编译生成的 exe 提示缺少 MFC42.dll原因老工程默认动态链接 MFC 库目标机器没有 VC6 运行时。MFC42.dll 是 Visual C 6.0 的运行时库Win10/Win11 默认不带。解决在工程设置里改“在静态库中使用 MFC”VC6 术语叫 “Use MFC in a Static Library”重新编译后 exe 就不再依赖外部 MFC 运行库。新版 VS 对应的是“使用 MFC 的静态库”性质一样。注意静态链接后 exe 体积会明显变大这是正常现象。6. 把它变成自己的课设验证胜负逻辑再挂一个人机分支6.1 三个测试用例验证提子与胜负拿到源码后建议先跑三个用例确认逻辑完整再改代码单提测试、双气存活测试、劫争测试。用例设计直接决定你对这份源码的掌握程度。用例操作预期结果单提测试在 (10,10) 落黑子四周摆满白子且白无气白子被提掉黑子保留轮到白方双气存活测试黑棋带两口气被白棋包围黑子不被提落子合法劫争测试提劫后立刻反提系统禁止反提提示“劫”实测时用 9 路小棋盘最快因为摆位不需要那么多步。把这三个用例手跑一遍基本就能判断源码的落子判定是否完整。6.2 按“数空”思路扩一个简单 AI双人模式跑通后下一步自然是加人机。最简单的搜索权重就是“数空”——把棋盘上未被提掉的黑白空点的数量当成当前局面的估值。// 简单的局面估值: 数空点, 谁的棋盘上空点多谁占优 int EvaluateBoard(CGoBoard board) { int blackEmpty 0, whiteEmpty 0; for (int x 0; x board.size; x) for (int y 0; y board.size; y) { if (board.grid[x][y] 0) { // 按邻子颜色计入对应方地盘 if (HasNeighborColor(board, x, y, 1)) blackEmpty; if (HasNeighborColor(board, x, y, -1)) whiteEmpty; } } return blackEmpty - whiteEmpty; }这个估值函数很简单但足够撑起一个“贪心 AI”对每个空点尝试落子、做一次提子判断、用 EvaluateBoard 算分、选分数最高的点落子。双人模式的 DFS 数气正好被复用不需要额外写搜索框架。我从这类老 MFC 项目里学到的一条教训是别急着重构先把落子校验和坐标换算两个基础函数测扎实再往上加任何花活。从那以后我每次拿到这种完整源码工程都会先按后缀归档文件、再跑一遍标准用例确认基础逻辑没有暗伤之后才动其他部分。希望这份 I_go 源码包能帮你把棋盘状态管理这一课补扎实——毕竟围棋程序的起点从来不是搜索算法而是那块棋盘本身。本文还有配套的精品资源点击获取
返回列表