☰
VS2010环境下VC围棋游戏老工程:从编译到规则实现
2026/10/1 3:14:07 网站建设 项目流程

简介:在VS2010环境下使用VC++开发的围棋游戏工程,面向想通过实际项目学习MFC与Win32编程的C++爱好者。工程压缩包内共三十三个文件,由十个头文件、八个C++源文件、若干bmp位图、ico图标以及MFC工程配置文件组成。头文件和源文件分别负责类声明与棋盘、棋子、胜负判断等功能实现,位图资源用于棋盘和棋子的图形化展示,txt文档记录了项目说明与待解决问题,整体包体仅一百六十四KB,结构紧凑便于快速阅读。目前已有九十九人浏览学习。代码中应用了面向对象方法划分棋盘类、棋子类和玩家类,并利用二维数组管理棋局、实现落子校验和五子连珠判定逻辑,整个工程包含MyFirstMFC文档视图架构,读者可借助该工程掌握控制台或MFC界面下围棋游戏的基础架构,在此基础上可进一步扩展AI搜索或GUI美化,对初学游戏开发具有较高参考价值。

1. VS2010 + VC围棋:一个能跑通、能讲清楚的老工程

VS2010还是主力开发环境时,用VC++配合MFC写一个19路围棋双人对弈程序,是很多课程设计和毕业设计的高频选题。这个标题"VS2010环境编写的VC围棋游戏.rar"里装的就是这类工程:一个.sln带上全套源码,棋盘用GDI画、鼠标点选落子、规则覆盖"气—提子—劫"判定,编译环境锁定在VS2010的v100工具集。它解决两件事:一是让你在几天内跑通一个能用的棋类桌面程序,二是给你一份能对着逐函数讲清规则的代码。适合谁?想快速捡起老Windows桌面编程的人,以及在课程设计里需要自己把规则实现讲明白的人。老框架不等于好跑,把这份老工程跑通本身,就是一次实打实的环境排障练习。

2. 跑通老工程:VS2010环境还原、编译与最小参数配置

2.1 装VS2010还是让新版IDE直接打开:先做选型

常见做法是先装VS2010本体。这个选择不是情怀,是工具链对齐:老工程依赖v100平台工具集、Windows SDK 7.0A和VC10的MFC运行库,用新IDE强行编译虽然也能出exe,但资源脚本、条件编译和MFC宏定义都要吃一遍兼容性的亏。装VS2010建议用离线ISO,在Win10/11上安装时会被"需要重启"的提示拦一道,这个坑第5章单独讲。Express版本本身免费,这也是当年"免费VC"说法的主要来源,完全够编译一个MFC围棋程序。

另一个务实路径是让VS2015或VS2017直接打开.sln,向导会问是否升级平台工具集。选择不升级,前提是机器上已经装了VS2010组件;选择升级到v140,代码大概率能过,但MFC头文件路径和atlbase的宏定义会有细微差别。我一般会在虚拟机里先装Win7加VS2010跑通一遍,再决定要不要在新环境里追着兼容问题折腾。别在没备份的情况下让新版IDE自动升级工程文件,它会改写.vcxproj里的自定义include路径,属于不可逆操作,后悔药都没有。

2.2 解压与工程文件核对:先看清包里有什么

rar包解压后第一件事不是双击.sln,而是核对工程结构。用7-Zip或WinRAR解压都能胜任,命令行方式更直接:

# 用7-Zip解压到指定目录 7z x VS2010环境编写的VC围棋游戏.rar -oD:\go_project # 核对关键工程文件是否存在 dir D:\go_project\*.sln dir D:\go_project\*.vcxproj

正常情况下应该能看到一个.sln、同名.vcxproj、StdAfx.h和StdAfx.cpp(MFC预编译头)、主窗体对应的.cpp和.h,以及res目录下的.ico和.rc资源脚本。如果解压出来只有.dsw和.dsp,那是VC6.0时代的旧工程,VS2010不能直接打开,需要新建工程再手动拖入源码,这类情况不在"VS2010环境编写"的范围内。

核对这一步是为了避开老资源包最常见的残缺问题:包主为了省体积只放了半套工程,缺了resource.h,导致RC资源脚本编译时报一堆ID未定义。.aps文件缺失不用慌,那只是资源编辑器缓存,重新编译会自动生成;resource.h缺失才会真的让RC编译失败。

2.3 用命令行走一遍编译:最小可复现流程

VS2010的命令行编译入口是devenv.com,不是MSBuild。用IDE打开F7当然更快,但命令行方式能更快暴露工具链问题,而且在服务器或虚拟机里没有图形界面时也能编译:

# 打开VS2010的编译环境,x86参数对应Win32平台 call "C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\Tools\vcvarsall.bat" x86 # 增量编译Release版 devenv.com D:\go_project\GoGame.sln /Build "Release|Win32" # 彻底清理并重编Debug版 devenv.com D:\go_project\GoGame.sln /Rebuild "Debug|Win32"

devenv.com和devenv.exe是同名不同程序,前者把编译日志输出到控制台而不会弹出IDE窗口,适合脚本调用。参数"Release|Win32"里的竖线不能去掉,整个字符串必须用引号包住。如果机器上只装了新版IDE,这条路就走不通,退而求其次用MSBuild:

msbuild D:\go_project\GoGame.sln /p:Configuration=Release /p:Platform=Win32

MSBuild的/p传参方式与devenv的配置名基本对应,但老工程如果设置了自定义生成步骤或后期事件脚本,两条路的输出可能有差异,遇到诡异行为就以devenv的结果为准。

2.4 工程属性里三个必查参数:字符集、MFC使用方式、预编译头

跑通老工程,IDE里不用改代码,先把三个属性对齐。字符集是头号雷区:老围棋程序里常见char和CString混用、中文提示消息用char数组,这类代码必须在"项目属性→常规→字符集"里选"使用多字节字符集"。报错"无法从const char*转换为CString"的根源大多在这里,而不是代码写错。

第二个是MFC使用方式。老工程通常选"在共享DLL中使用MFC",运行时依赖MFC100.dll,开发机上都装过VS2010还能跑,但要把exe拷到干净的机器上就会缺DLL。这时改成"在静态库中使用MFC",并顺手把"CC++→代码生成→运行库"改成"多线程(/MT)",把所有运行依赖打包进exe,体积会涨到2MB左右但能免安装运行。

第三个是预编译头。工程设置了/Yu"stdafx.h",就必须保证每个.cpp第一行是#include "stdafx.h",顺序不能被前置宏定义破坏。老项目里常见的C1010"在查找预编译头时遇到意外的文件结尾",就是某个新加的.cpp漏了这一行,排查时只看出错文件前三行就能定位。

提示:字符集和MFC使用方式在VS2010里改完后写进.vcxproj.user,不污染原工程文件;预编译头选项在.vcxproj里,改之前先复制一份备份。

3. 棋盘闪烁与坐标换算:GDI双缓冲棋盘的实现取舍

3.1 为什么老工程用GDI而不是OpenGL:规模决定选型

vs2010 opengl也是同时代的热词,但多数VS2010围棋工程不选OpenGL渲染棋盘。这个取舍很实际:GDI的MoveTo、LineTo、Ellipse三个函数就能画完网格和棋子,代码不到一百行,调试时屏幕坐标可以直接对照;OpenGL挂进MFC要先处理像素格式、渲染上下文和DC句柄绑定,对19路棋盘这点绘制量纯属增加耦合。

不用OpenGL不等于不能用。想把棋盘做得更有现代感,比如棋子高光、棋盘木纹渐变,再迁到OpenGL也不迟,MFC下集成OpenGL的写法在那个时代已经成熟,照搬即可。但迁移应该发生在规则模块稳定之后,而不是一开始就两头一起改,否则棋子和网格都画不出来,规则再对也无从验证。

3.2 双缓冲画棋盘:OnPaint里的一次完整绘制

MFC视图类里实现双缓冲的标准写法是:先在内存DC上画完所有内容,再一次BitBlt复制到屏幕。这样做的直接价值是取消落子瞬间的整屏闪烁。下面是核心代码,放进CGoGameView::OnPaint即可替换默认绘制:

void CGoGameView::OnPaint() { CPaintDC dc(this); // 设备DC,直接对应屏幕 CRect rc; GetClientRect(&rc); // 客户区尺寸,棋盘自适应 CDC memDC; // 内存DC,先画到这里 CBitmap bmp; memDC.CreateCompatibleDC(&dc); bmp.CreateCompatibleBitmap(&dc, rc.Width(), rc.Height()); CBitmap* pOldMemBitmap = memDC.SelectObject(&bmp); // 棋盘底色:木色 memDC.FillSolidRect(&rc, RGB(222, 184, 135)); // 网格参数:边距和格距 int margin = 30; int cellSize = (rc.Width() - 2 * margin) / 18; // 19条线只有18格 for (int i = 0; i < 19; i++) { memDC.MoveTo(margin, margin + i * cellSize); memDC.LineTo(margin + 18 * cellSize, margin + i * cellSize); memDC.MoveTo(margin + i * cellSize, margin); memDC.LineTo(margin + i * cellSize, margin + 18 * cellSize); } // 落子:按全局棋盘数组重绘 for (int row = 0; row < 19; row++) for (int col = 0; col < 19; col++) if (g_board[row][col] != 0) DrawStone(&memDC, row, col, g_board[row][col]); dc.BitBlt(0, 0, rc.Width(), rc.Height(), &memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOldMemBitmap); }

几个参数要解释清楚。cellSize用的是除以18而不是19,这是新手写棋盘程序最常见的"多一格"错误:19条线之间的间距只有18段。margin取30像素是为了给边缘棋子留出半颗子的空间,真正做的时候建议把它存成成员变量m_nMargin,窗口缩放时中心重算。BitBlt用SRCCOPY直接覆盖,不要用透明混合,否则MFC默认的WM_ERASEBKGND会在重绘前把客户区刷成灰色,白忙一场。

DrawStone是独立的绘制函数,黑白子分别用黑色和白色实心圆,再画一圈深色描边让白子在浅色棋盘上看得清:

void CGoGameView::DrawStone(CDC* pDC, int row, int col, int color) { int cx = m_nMargin + col * m_nCellSize; int cy = m_nMargin + row * m_nCellSize; int r = m_nCellSize * 2 / 5; // 半径略小于半格,避免粘连 CBrush brush; brush.CreateSolidBrush(color == 1 ? RGB(0, 0, 0) : RGB(255, 255, 255)); CBrush* pOld = pDC->SelectObject(&brush); CPen pen(PS_SOLID, 1, RGB(80, 80, 80)); CPen* pOldPen = pDC->SelectObject(&pen); pDC->Ellipse(cx - r, cy - r, cx + r, cy + r); pDC->SelectObject(pOld); pDC->SelectObject(pOldPen); }

3.3 鼠标落子与最近交叉点换算:非整数运算的细节

棋盘接收的是客户区像素坐标,要在OnLButtonDown里换算成19x19的row和col。换算公式的关键是"先平移再取整":

void CGoGameView::OnLButtonDown(UINT nFlags, CPoint point) { int col = (point.x - m_nMargin + m_nCellSize / 2) / m_nCellSize; int row = (point.y - m_nMargin + m_nCellSize / 2) / m_nCellSize; // 越界和重复落子直接拒绝 if (row < 0 || row >= 19 || col < 0 || col >= 19) return; if (g_board[row][col] != 0) return; if (!PlaceStone(row, col, m_currentColor)) return; // 规则引擎拒绝则不动 m_currentColor = (m_currentColor == 1) ? 2 : 1; // 换手 Invalidate(); // 触发重绘 }

加m_nCellSize / 2再做整数除法,等价于四舍五入到最近的交叉点,这是最容易被漏掉的一行。去掉它,棋子落在两颗子中间的空隙,视觉上就是"落子跑偏"。row和col的顺序必须和绘制循环一致:绘制时外层row内层col,board[row][col]对应屏幕坐标就是row管纵向、col管横向,混了会得到转置的棋盘,提子逻辑跟着全错。

注意Invalidate只是把客户区标记为无效并安排重绘,真正的绘制发生在下次WM_PAINT。不要在OnLButtonDown里直接调OnPaint,也不要在这函数里改棋盘显示,否则会跟背景擦除消息互相覆盖,反而闪烁。

3.4 窗口缩放后棋盘错位:把边距和格距变成成员变量

老工程里最容易翻车的是把margin和cellSize写成OnPaint里的局部常量,窗口一拉大,重绘用新坐标,鼠标换算却沿用旧值,棋盘和落点整体错位。解决方式很直白:在WM_SIZE里重算成员变量,OnPaint和OnLButtonDown都读它们:

void CGoGameView::OnSize(UINT nType, int cx, int cy) { CView::OnSize(nType, cx, cy); m_nMargin = 30; m_nCellSize = (cx - 2 * m_nMargin) / 18; if (m_nCellSize < 10) m_nCellSize = 10; // 最小格距保护,防止过小和除零 }

这里有个边界坑:视图刚创建时cx可能是0,算出来的m_nCellSize是负数,后续绘制直接出错。要么在OnInitialUpdate里先给默认值,要么像上面一样钳制下限。窗口最小化时OnSize拿到的cx和cy也是0,这个保护不能省。

4. 围棋规则在VC里落地:气、提子、劫与胜负判定

4.1 棋盘数据模型:为什么二维数组比哈希表更合适

老围棋程序几乎全部采用int g_board[19][19]作为核心数据结构,0空、1黑、2白。这个选择在今天看很朴素,但19路棋盘361个格子是常数规模,二维数组随机访问O(1),视觉层、规则层、AI层共享同一份数据,调试时printf出来就是一张棋盘,比哈希表直观太多。若以后要扩展棋力AI,再补一个当前步数全局变量和最近两步棋盘快照即可,基础够用。

我一般会顺手把board收进一个GoEngine类里,而不是散落成全局变量,这样后面做悔棋只需要操作引擎内部的栈。但老工程往往已经是全局数组,能跑通就先不动,重构动作放到第6章的"再投入"阶段再做。先保证规则正确,再谈代码组织,这是棋类项目的一贯顺序。

4.2 提子算法:用DFS统计"气"并回收无气棋子

提子是围棋规则的核心。一个棋子或一串相连同色棋子的气,是它邻接的空交叉点数量,气为0就必须从棋盘拿走。经典实现用DFS蔓延整团棋:

// 方向数组:右、左、下、上 static int dx[4] = { 1, -1, 0, 0 }; static int dy[4] = { 0, 0, 1, -1 }; // 统计(row,col)所在同色棋串的气,并把该棋串所有坐标标记进visited void FindLiberties(int row, int col, int color, bool visited[19][19], int& liberties) { if (row < 0 || row >= 19 || col < 0 || col >= 19) return; if (visited[row][col]) return; visited[row][col] = true; for (int i = 0; i < 4; i++) { int nr = row + dx[i]; int nc = col + dy[i]; if (nr < 0 || nr >= 19 || nc < 0 || nc >= 19) continue; if (g_board[nr][nc] == 0) liberties++; // 空点就是一口气 else if (g_board[nr][nc] == color) FindLiberties(nr, nc, color, visited, liberties); // 同色继续蔓延 } }

核心逻辑就三句:visited防止重复遍历,碰到空点气加一,碰到同色继续递归,碰到异色不动。这里有个非常隐蔽的坑,visited必须以引用或指针传入,如果按值传,每一层递归都拷贝一份新数组,气数累计和访问标记全部丢失,最终结果永远是0气,表现出来就是"提子永远不提"。这个问题当年调试了半个下午,是典型的盯着代码看不出毛病的黑匣子。

有了气统计,提子就是一次循环:

void TryCapture(int row, int col, int enemyColor) { bool visited[19][19] = { false }; int liberties = 0; FindLiberties(row, col, enemyColor, visited, liberties); if (liberties == 0) { // 无气,整串移除 for (int i = 0; i < 19; i++) for (int j = 0; j < 19; j++) if (visited[i][j]) g_board[i][j] = 0; } }

提子循环复用visited数组,看着取巧,其实合法:刚算完气,visited里正好记录了整团棋的坐标。如果提完一串后另一串异色棋变成无气,那是"提多串"的情形,需要把TryCapture按顺序调用两轮,第一轮处理四邻异色,第二轮处理同色,顺序不能反。

4.3 落子主流程:先落子、后提子、再查自杀

规范的落子流程是:先把子放上棋盘,对四个邻居中的异色做TryCapture提掉无气子,再对自己所在棋串查气,若气为0说明这手是自杀,回滚局面并返回失败:

bool PlaceStone(int row, int col, int color) { if (g_board[row][col] != 0) return false; // 有子不能落 // 保存现场,自杀判断失败时回滚 int saved[19][19]; memcpy(saved, g_board, sizeof(g_board)); g_board[row][col] = color; // 先提异色:四个邻居里的敌方棋串 for (int i = 0; i < 4; i++) { int nr = row + dx[i]; int nc = col + dy[i]; if (nr >= 0 && nr < 19 && nc >= 0 && nc < 19 && g_board[nr][nc] != 0 && g_board[nr][nc] != color) TryCapture(nr, nc, g_board[nr][nc]); } // 再查自杀:自己这团还有没有气 bool visited[19][19] = { false }; int selfLiberties = 0; FindLiberties(row, col, color, visited, selfLiberties); if (selfLiberties == 0) { memcpy(g_board, saved, sizeof(g_board)); // 回滚,恢复现场 return false; } return true; }

提异色的循环里判断g_board[nr][nc] != color,这是排除自己,新手常漏掉;不排除的话,TryCapture会把刚落下的子也当敌人去算,出现"落子即提自己"的荒谬行为。回滚用memcpy整盘361个int,成本可以忽略,别为了省这几条指令搞复杂状态机。这里的关键是顺序:先提对方再查自己,正好还原真实围棋的判定,吃子成功的落子不算自杀,因为对方被提掉后自己这团可能重新有了气。

4.4 打劫判断:用最近两步局面快照做"不许循环"

打劫在规则上要求不能立刻提回劫争。可靠的工程实现是保存上一手落子完成后的棋盘快照,当本手落子提子之后,拿当前棋盘和那个快照比较,完全相同则说明这手把棋盘还原成了两步前,属于劫回提,应该拒绝并还原:

// 全局保存"最近一次落子完成后"的棋盘 static int previousBoard[19][19]; // 在PlaceStone完成落子和提子之后调用 bool IsKo() { return memcmp(g_board, previousBoard, sizeof(g_board)) == 0; }

调用位置放在提异色之后、查自杀之前。流程是:落子,提异色,此时若IsKo成立则回滚并拒绝;否则继续查自杀。previousBoard必须在每一步落子被接受后更新为"刚落完的棋盘",而不是更新成"上一次落子前"。写错的典型结果是previousBoard始终等于初始空棋盘,IsKo永远返回false,劫争规则形同虚设。连续提子三五手后行为才完全错乱,Debug时记得在关键分支打印棋盘比较结果验证。

4.5 胜负判定:数子法与数目法的取舍

棋局结束阶段,老工程里普遍做简化数子法:先FloodFill扫描所有空点,如果某个连通空域的边界全是黑棋,这块地归黑;黑白都有则空置不计。最后统计黑子加黑地、白子加白地,按中国规则黑方贴3又3/4子,黑棋合计超过184又1/4子判黑胜,否则白胜。要做得严谨,还得处理死子、双活这些复杂局面,已经超出基础工程范围。所以很多VS2010围棋程序干脆不做自动判胜负,只提供"人工数子"或"双方确认"模式。我一般建议在课程设计里把自动判定做成简化版,并在文档里明确列出简化假设,比假装实现了完整规则老实得多。

5. 老项目避坑实录:编译失败与运行异常的5个典型场景

5.1 现象:编译报"cl.exe failed with exit status 2",但输出窗口没有具体语法错误

这类报错常见于命令行编译,或在IDE生成窗口里只看到一句笼统的Error,真正的错误信息被吞了。原因多半是PATH环境变量被污染,别的cl.exe顶到了VS2010前面。典型的是机器上装过Visual C++ for Python 9.0,它的编译器路径在...\Visual C++ for Python\9.0\vc\bin\amd64\cl.exe,和VS2010工具集抢入口。解决:打开cmd执行where cl.exe,如果路径不在VS2010的Common7\IDE下,说明编译入口乱了;重新用vcvarsall.bat开干净环境,或卸载不用的VC for Python。另一种情况是平台工具集被IDE改成了和解决方案平台不匹配的组合,Win32工程选了x64的编译工具,同样触发这个笼统报错,去项目属性"配置属性→常规→平台工具集"里改回v100即可。

5.2 现象:装VS2010后提示"此安装程序要求您重新启动系统以完成 Microsoft VC redistributable 的安装"

安装程序检测到之前的Visual C++ 2010可再发行组件没装完,这是VS2010装机的经典拦路虎。直接再跑一次vcredist没用,因为安装程序每次都会先做重启检查。解决:正常重启机器后再开安装程序;若还卡着,检查注册表项HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending是否存在,备份后手动清除再装。这是注册表操作,没把握就不要碰,重启无效就换虚拟机装,别硬抠。网上大量vs2010下载安装教程的评论区都在问"为什么又要重启第三次",说明这不是个例,装之前做好心理准备。

5.3 现象:exe拷到没装VS2010的机器上,提示缺少MFC100.dll或MSVCR100.dll

这是动态链接的必然结果。老工程默认"在共享DLL中使用MFC",跑起来就需要VC2010运行库兜底。解决二选一:在目标机器装vcredist_x86.exe,注意区分x86和x64,32位程序在64位系统上也需要x86版运行库;或者回工程属性把MFC使用改成静态、运行库改成/MT,重新编译后exe自带所有依赖,就是体积变大。第二种适合交课程设计,答辩机器上未必有老运行库,提前编一个静态版最省事。我一般两种配置都编一次,静态版留作交付,动态版留作开发调试,省得每次换机器都冒"缺DLL"的险。市面上那些第三方VC运行库修复工具能救急,但装完往往带一堆全家桶,不是首选。

5.4 现象:中文注释变乱码,编译报C2018或"非法字符"

老程序源码多数是ANSI简体中文保存的,在VS2010里默认代码页936本来没问题,但文件被Git或网盘转存成UTF-8无BOM后,VS2010的编辑器会按本地代码页误读,中文注释变乱码,字符串里的汉字一旦被错误解码,编译器直接报C2018或乱码符号引起的诡异错误。解决:用编辑器把文件另存为"简体中文(GB2312)"或"UTF-8带签名",并保持工程字符集选多字节。关键习惯是不要在多套IDE之间反复保存同一个.cpp,每转手一次编码就被"自动修正"一次,越改越乱。VS2010的"文件→高级保存选项"里可以指定编码,这是老工程中文乱码的标准后悔药。

5.5 现象:Debug版能跑,Release版一落子就崩

编译期行为差异主要来自未初始化局部变量和数组越界。围棋程序里最常见两条:落子坐标没做边界保护,board[-1][-1]被写进数组,Debug版内存布局碰巧没炸,Release版开启/O2优化后访问越界直接踩到保护页;另一条是数组下标row和col写反,产生逻辑越界。解决思路:先把Release的优化级别从/O2降到/Od,重新编译后还在同一位置崩,基本可以断定是逻辑问题而不是编译器优化问题,再逐个落子点打印row、col和board访问范围定位。还有一个隐蔽点:方案配置里Debug和Release的"预处理器定义"可能不一致,Release少了某个条件编译变量,让一段只在Debug下执行的初始化代码消失,处理方式相同,在崩溃点加断点看成员变量状态即可。

6. 让老围棋工程活起来:悔棋、简易AI与重构取舍

把老工程跑通只是开始。让这门课设真正拿得出手的改造,我建议按"悔棋→简易AI→规则引擎抽取"三步走,每步都能独立验证,不做大爆炸式重构。

悔棋最容易出彩,也最适合纯VC实现。核心是让引擎在每次成功落子后压栈保存一步完整状态,撤销时弹栈恢复:

struct MoveRecord { int board[19][19]; // 整盘快照 int color; // 落子方 }; std::vector<MoveRecord> g_history; // 历史栈 void DoMove(int row, int col, int color) { MoveRecord rec; memcpy(rec.board, g_board, sizeof(g_board)); rec.color = color; if (PlaceStone(row, col, color)) // 落子成功才压栈 g_history.push_back(rec); } void UndoMove() { if (g_history.empty()) return; memcpy(g_board, g_history.back().board, sizeof(g_board)); g_history.pop_back(); Invalidate(); }

栈里存整盘19x19快照,一步约1.4KB,20路棋局到终盘最多三百来手,总共不到500KB,比增量式撤销简单太多,还天然支持任意步数悔棋。这个取舍值得记住:小数据量场景,快照永远比增量实现简单可靠。

第二步加简易AI,不追求棋力,只要能陪你下一局。评分函数可以做成:对每个空点统计其四邻的敌我比例,再给"气少"的敌方棋串加权,气越少越优先被打吃,构成打吃优先的贪心棋。评估完所有可落点取最高分,平局用随机数打破。这种AI离真正围棋引擎差很远,但答辩演示里"电脑会提子、会打吃"已经足以证明规则模块是通的,也远比写死招法的"伪AI"值得讲。

最后一步是重构取舍。把g_board和PlaceStone从CGoGameView里拆出来,变成独立GoEngine类,视图层只留OnPaint、OnLButtonDown和消息映射。拆完的收益很实际:你可以在控制台里写命令行测试用例,把提子、打劫一条条验证,不用每次打开窗口点鼠标。这是老工程最值得投入的一处,也是我复盘时最后悔没早点做的动作,调试效率提升一个量级。

这几年回头看这套VS2010老工程,最有价值的不是那几百行GDI代码,而是它把围棋规则收敛到几十个函数里,逻辑边界清清楚楚。如果你也要在这个方向投入,先从跑通、加悔棋、拆引擎三件事做起,三步做完,你已经可以给别人讲明白整个程序的来龙去脉了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询