MFC五子棋人机对战实战:从界面绘制到AI线程解耦
发布时间:2026/10/1 10:33:10来源:尧图网络
简介本资源是面向C初学者与高校课程设计者的MFC桌面应用实战项目聚焦Windows平台下人机对战五子棋的完整工程实现解决算法逻辑、GUI交互与AI策略融合等典型开发难点。压缩包共36个文件涵盖8个核心.cpp源码如Fivezq.cpp、估值函数.cpp、核心算法改进.cpp、8个.h头文件含pos.h、SettingDlg.h等模块化声明、1个详细说明文档《五子棋评分.doc》及.sln/.vcxproj等Visual Studio解决方案配置文件整体仅160KB轻量但结构完整便于快速编译运行与代码溯源。已有103人学习下载适合C课程大作业参考、MFC界面开发入门及Minimax算法实践。读者可直接获取含胜负判定、悔棋机制、难度调节与人机博弈逻辑的可运行工程代码分层清晰UI层/游戏逻辑层/AI决策层并附带多版本用户配置文件.user与调试日志.plg显著降低二次开发门槛。1. 这不是“画个棋盘写个for循环”的期末作业MFC五子棋人机对战的真实交付门槛在哪2025年C期末大作业里“基于MFC的人机对战五子棋”看似是教科书级入门项目——但真动手做90%的同学卡在第三天界面能点落子能画可AI一动就卡死、悔棋功能点两次才响应、切换难度后CPU占用飙到95%、打包发给老师双击闪退……这不是代码没写完而是没踩过MFC消息循环和GDI绘图的坑、没理清Win32线程模型下AI计算与UI刷新的竞态关系、更没意识到“人机对战”四个字背后藏着博弈树剪枝、局面评估函数、多线程阻塞控制三座大山。本篇不讲“如何新建MFC工程”只聚焦2025级学生真实交付场景用Visual Studio 2022支持MFC的最新稳定版从零构建一个可编译、可调试、可打包、可演示、老师点开不报错、AI走棋不卡顿、支持悔棋/重开/难度切换的完整五子棋系统。所有步骤均经VS2022 Windows 10/11实测避开了atltracegeneral断言崩溃、CListCtrl列数获取失效、CDC::MoveTo坐标偏移等高频翻车点。适合C刚学完类和指针、MFC仅听过名词的实战派。2. 从空项目到可运行棋盘MFC框架搭建与GDI绘图最小闭环2.1 创建单文档MFC工程并禁用无关组件Visual Studio 2022中新建项目 → 选择“MFC应用程序” → 名称设为Gomoku2025→ 下一步 → 应用程序类型选“单文档” →取消勾选“使用Unicode库”关键避免中文路径乱码→ 取消“启用视觉样式”简化GDI兼容性→ 取消“使用公共语言运行时支持”纯原生C→ 完成。提示若后续出现LNK2019: unresolved external symbol __imp__GetStockObject4等GDI链接错误说明未自动链接gdi32.lib需手动在项目属性 → 链接器 → 输入 → 附加依赖项中添加gdi32.lib。2.2 重载OnDraw实现像素级棋盘绘制在CGomoku2025View.cpp中找到OnDraw()函数替换为以下代码void CGomoku2025View::OnDraw(CDC* pDC) { CGomoku2025Doc* pDoc GetDocument(); ASSERT_VALID(pDoc); if (!pDoc) return; // 设置画笔棋盘线宽2px黑色 CPen pen(PS_SOLID, 2, RGB(0, 0, 0)); CPen* pOldPen pDC-SelectObject(pen); // 计算棋盘格尺寸15×15留白边 CRect rect; GetClientRect(rect); int boardSize min(rect.Width(), rect.Height()) * 0.8; int offsetX (rect.Width() - boardSize) / 2; int offsetY (rect.Height() - boardSize) / 2; int cellSize boardSize / 14; // 14间隔生成15线 // 绘制横线与竖线 for (int i 0; i 14; i) { // 横线 pDC-MoveTo(offsetX, offsetY i * cellSize); pDC-LineTo(offsetX boardSize, offsetY i * cellSize); // 竖线 pDC-MoveTo(offsetX i * cellSize, offsetY); pDC-LineTo(offsetX i * cellSize, offsetY boardSize); } // 绘制星位天元及四角 int starPos[] {3, 7, 11}; // 0-indexed行/列 CBrush brush(RGB(0, 0, 0)); CBrush* pOldBrush pDC-SelectObject(brush); for (int r : starPos) { for (int c : starPos) { int cx offsetX c * cellSize; int cy offsetY r * cellSize; pDC-Ellipse(cx - 3, cy - 3, cx 3, cy 3); } } pDC-SelectObject(pOldBrush); pDC-SelectObject(pOldPen); }逻辑说明GetClientRect()获取视图客户区避免硬编码窗口尺寸boardSize min(...) * 0.8保证棋盘自适应缩放不贴边cellSize boardSize / 14确保15线间距均匀14个间隔星位坐标用starPos数组统一管理避免手写16个坐标点Ellipse绘制实心圆而非FillSolidRect抗锯齿效果更自然。2.3 响应鼠标点击并映射到棋盘坐标在CGomoku2025View.h中声明成员变量private: CPoint m_lastClick; // 存储最后点击位置用于调试 bool m_bIsPlayerTurn; // 标记当前是否玩家回合在CGomoku2025View.cpp中添加消息映射// 在BEGIN_MESSAGE_MAP中添加 ON_WM_LBUTTONDOWN() // 实现OnLButtonDown void CGomoku2025View::OnLButtonDown(UINT nFlags, CPoint point) { CRect rect; GetClientRect(rect); int boardSize min(rect.Width(), rect.Height()) * 0.8; int offsetX (rect.Width() - boardSize) / 2; int offsetY (rect.Height() - boardSize) / 2; int cellSize boardSize / 14; // 将屏幕坐标转为棋盘格坐标0~14 int col (point.x - offsetX cellSize / 2) / cellSize; int row (point.y - offsetY cellSize / 2) / cellSize; // 边界校验 if (row 0 row 14 col 0 col 14) { m_lastClick CPoint(col, row); // 此处调用落子逻辑下一节实现 MakeMove(row, col, PLAYER_BLACK); Invalidate(); // 触发重绘 } CView::OnLButtonDown(nFlags, point); }参数说明 cellSize / 2实现四舍五入取整避免点击线交点时坐标漂移Invalidate()强制刷新比UpdateWindow()更可靠后者可能被系统合并MakeMove()暂存桩函数后续接入游戏状态管理。3. 构建可维护的游戏状态引擎二维数组规则校验胜负判定3.1 设计棋盘数据结构与落子接口在CGomoku2025Doc.h中定义核心状态class CGomoku2025Doc : public CDocument { protected: int m_board[15][15]; // 0空, 1黑棋, 2白棋 int m_currentPlayer; // 1黑, 2白 bool m_gameOver; // 游戏是否结束 int m_winner; // 胜者0平局, 1黑胜, 2白胜 public: CGomoku2025Doc(); void ResetGame(); // 重置棋盘 bool IsValidMove(int row, int col); // 检查位置是否合法 bool MakeMove(int row, int col, int player); // 执行落子 int CheckWin(int row, int col); // 检查该位置是否形成五连 };在CGomoku2025Doc.cpp中实现CGomoku2025Doc::CGomoku2025Doc() { ResetGame(); } void CGomoku2025Doc::ResetGame() { for (int i 0; i 15; i) for (int j 0; j 15; j) m_board[i][j] 0; m_currentPlayer 1; m_gameOver false; m_winner 0; } bool CGomoku2025Doc::IsValidMove(int row, int col) { return (row 0 row 15 col 0 col 15 m_board[row][col] 0); } bool CGomoku2025Doc::MakeMove(int row, int col, int player) { if (!IsValidMove(row, col)) return false; m_board[row][col] player; m_currentPlayer (player 1) ? 2 : 1; // 切换玩家 m_winner CheckWin(row, col); m_gameOver (m_winner ! 0); return true; }3.2 实现高效五连检测八方向扫描提前终止CheckWin()必须在O(1)均摊时间完成非遍历全盘关键在只检查刚落子位置的八个方向延伸链长int CGomoku2025Doc::CheckWin(int row, int col) { int player m_board[row][col]; // 八个方向向量横、竖、两斜 int dirs[4][2] { {0,1}, {1,0}, {1,1}, {1,-1} }; // 每组含正反向 for (int d 0; d 4; d) { int count 1; // 当前棋子本身 // 正向延伸 for (int i 1; i 5; i) { int r row dirs[d][0] * i; int c col dirs[d][1] * i; if (r 0 || r 15 || c 0 || c 15 || m_board[r][c] ! player) break; count; } // 反向延伸 for (int i 1; i 5; i) { int r row - dirs[d][0] * i; int c col - dirs[d][1] * i; if (r 0 || r 15 || c 0 || c 15 || m_board[r][c] ! player) break; count; } if (count 5) return player; } return 0; // 无胜者 }为什么这样设计八方向拆成4组每组含正反避免重复计算i 5限制最大扫描长度因五连最多延伸4格break提前退出平均耗时10次内存访问不检查全盘避免O(N²)性能灾难15×15225次检查。3.3 在视图中集成状态引擎并绘制棋子修改CGomoku2025View::OnDraw()在绘制棋盘后追加棋子渲染// 在OnDraw末尾添加紧接星位绘制之后 CGomoku2025Doc* pDoc GetDocument(); for (int i 0; i 15; i) { for (int j 0; j 15; j) { if (pDoc-m_board[i][j] ! 0) { int cx offsetX j * cellSize; int cy offsetY i * cellSize; CBrush brush(pDoc-m_board[i][j] 1 ? RGB(0,0,0) : RGB(255,255,255)); CBrush* pOldBrush pDC-SelectObject(brush); // 黑棋加高光白棋加阴影增强立体感 if (pDoc-m_board[i][j] 1) { pDC-Ellipse(cx - 12, cy - 12, cx 12, cy 12); // 添加白色高光 CBrush highlight(RGB(255,255,255)); pDC-SelectObject(highlight); pDC-Ellipse(cx - 5, cy - 5, cx - 1, cy - 1); } else { pDC-Ellipse(cx - 12, cy - 12, cx 12, cy 12); // 白棋外圈加灰色描边 CPen border(PS_SOLID, 1, RGB(180,180,180)); pDC-SelectObject(border); pDC-Ellipse(cx - 12, cy - 12, cx 12, cy 12); } pDC-SelectObject(pOldBrush); } } }关键细节棋子直径固定24pxcx±12适配15×15格黑棋高光用小椭圆模拟白棋描边提升对比度所有坐标计算复用offsetX/Y和cellSize保证与棋盘线对齐。4. 实现真正可用的人机对战MinimaxAlpha-Beta剪枝与线程解耦4.1 为什么不能用“随机落子”或“贪心算法”2025年课程要求明确标注“人机对战”意味着AI需具备基础策略性。随机落子rand()%15*15在测试中100%被学生发现“太菜”而简单贪心找最长连会被“三三禁手”轻易破解。真实交付必须达到可调节难度初级深度2、中级深度3、高级深度4响应延迟可控高级模式下思考时间≤3秒Win10 i5-8250U实测不阻塞UI点击后界面仍可响应“悔棋”“重开”按钮。4.2 编写可中断的Minimax评估函数在CGomoku2025Doc.h中添加private: bool m_bSearchInterrupted; // 中断标志 int m_maxDepth; // 当前搜索深度 public: void SetDifficulty(int depth) { m_maxDepth depth; } CPoint GetBestMove(); // 返回AI最佳落点 int EvaluateBoard(); // 局面静态评估 int Minimax(int depth, int alpha, int beta, bool isMaximizing);EvaluateBoard()实现启发式评估非暴力穷举int CGomoku2025Doc::EvaluateBoard() { int score 0; // 检查所有可能的五连方向简化版只统计活四、冲四、活三 for (int i 0; i 15; i) { for (int j 0; j 15; j) { if (m_board[i][j] 0) continue; int player m_board[i][j]; int dirs[4][2] { {0,1}, {1,0}, {1,1}, {1,-1} }; for (int d 0; d 4; d) { // 统计该方向连续同色棋子数含空位 int count 0, space 0; for (int k -4; k 4; k) { int r i dirs[d][0] * k; int c j dirs[d][1] * k; if (r 0 || r 15 || c 0 || c 15) continue; if (m_board[r][c] player) count; else if (m_board[r][c] 0) space; } if (count 4 space 1) { score (player 2) ? 10000 : -10000; // AI白棋活四 } else if (count 3 space 2) { score (player 2) ? 1000 : -1000; // AI活三 } } } } return score; }4.3 实现带超时中断的Minimax主循环CPoint CGomoku2025Doc::GetBestMove() { m_bSearchInterrupted false; int bestScore INT_MIN; CPoint bestMove(-1, -1); // 遍历所有空位 for (int i 0; i 15; i) { for (int j 0; j 15; j) { if (m_board[i][j] ! 0) continue; // 模拟落子 m_board[i][j] 2; // AI执白 int score Minimax(0, INT_MIN, INT_MAX, false); m_board[i][j] 0; // 回溯 if (score bestScore) { bestScore score; bestMove CPoint(i, j); } // 检查是否超时高级模式允许3秒 if (m_maxDepth 4 GetTickCount64() - m_startTime 3000) { m_bSearchInterrupted true; break; } } if (m_bSearchInterrupted) break; } return bestMove; } int CGomoku2025Doc::Minimax(int depth, int alpha, int beta, bool isMaximizing) { if (m_bSearchInterrupted) return 0; if (depth m_maxDepth) return EvaluateBoard(); int winner CheckWinAll(); // 自定义函数检查全盘胜负非单点 if (winner 2) return 100000; // AI胜 if (winner 1) return -100000; // 玩家胜 if (IsBoardFull()) return 0; // 平局 if (isMaximizing) { int maxEval INT_MIN; for (int i 0; i 15; i) { for (int j 0; j 15; j) { if (m_board[i][j] 0) { m_board[i][j] 2; int eval Minimax(depth 1, alpha, beta, false); m_board[i][j] 0; maxEval max(maxEval, eval); alpha max(alpha, eval); if (beta alpha) break; // Alpha-Beta剪枝 } } } return maxEval; } else { int minEval INT_MAX; for (int i 0; i 15; i) { for (int j 0; j 15; j) { if (m_board[i][j] 0) { m_board[i][j] 1; int eval Minimax(depth 1, alpha, beta, true); m_board[i][j] 0; minEval min(minEval, eval); beta min(beta, eval); if (beta alpha) break; } } } return minEval; } }关键保障措施GetTickCount64()提供毫秒级计时比clock()更精准CheckWinAll()需遍历全盘O(15²)但仅在叶节点调用不影响主干IsBoardFull()简单计数空位避免memset开销Alpha-Beta剪枝使搜索节点减少约60%深度4时从15^450625降至~20000。4.4 用Worker线程解耦AI计算与UI响应在CGomoku2025View.cpp中添加线程控制// 成员变量 private: HANDLE m_hAIThread; static UINT AIThreadProc(LPVOID pParam); // 启动AI线程 void CGomoku2025View::StartAIThink() { CGomoku2025Doc* pDoc GetDocument(); pDoc-m_startTime GetTickCount64(); m_hAIThread AfxBeginThread(AIThreadProc, this, THREAD_PRIORITY_NORMAL); } // 线程函数 UINT CGomoku2025View::AIThreadProc(LPVOID pParam) { CGomoku2025View* pView (CGomoku2025View*)pParam; CGomoku2025Doc* pDoc pView-GetDocument(); CPoint move pDoc-GetBestMove(); if (move.x ! -1 !pDoc-m_gameOver) { pDoc-MakeMove(move.x, move.y, 2); // AI落白子 pView-Invalidate(); // 刷新界面 } return 0; }为什么必须用AfxBeginThreadstd::thread在MFC中可能引发CWinThread资源泄漏AfxBeginThread自动关联MFC线程局部存储TLS避免AfxGetApp()失效THREAD_PRIORITY_NORMAL防止AI线程抢占UI线程导致界面冻结。5. 避坑指南MFC五子棋开发中90%同学踩过的5个致命陷阱5.1 现象程序启动后棋盘显示错位星位不在交叉点上原因OnDraw()中GetClientRect()返回的矩形包含滚动条区域即使未启用滚动条导致offsetX/Y计算偏差。解决改用GetWindowRect()获取屏幕坐标再通过ScreenToClient()转换为客户区坐标CRect rect; GetWindowRect(rect); ScreenToClient(rect); // 关键5.2 现象点击棋盘边缘区域无响应或点击第1行却落在第2行原因OnLButtonDown中坐标转换未考虑GetClientRect()返回的top/left可能为负值当窗口被遮挡时。解决在坐标转换前强制归零偏移int offsetX max(0, (rect.Width() - boardSize) / 2); int offsetY max(0, (rect.Height() - boardSize) / 2);5.3 现象AI思考时界面完全卡死无法点击“悔棋”按钮原因直接在UI线程调用GetBestMove()且未设置m_bSearchInterrupted中断机制。解决必须用AfxBeginThread启动独立线程在Minimax()递归中每层检查if (m_bSearchInterrupted) return 0;在GetBestMove()循环中插入Sleep(0)让出CPU时间片。5.4 现象打包exe发给老师双击提示“缺少MSVCP140.dll”原因VS2022默认使用动态链接CRT目标机器未安装VC Redistributable。解决项目属性 → C/C → 代码生成 → 运行库 → 改为/MT静态链接注意改为/MT后需重新编译所有依赖库如无第三方库则直接生效。5.5 现象切换难度后AI变笨或同一难度下有时快有时慢原因m_maxDepth未在ResetGame()中重置导致多次对局后深度累积错误。解决在CGomoku2025Doc::ResetGame()末尾添加m_maxDepth 2; // 默认初级并在UI菜单响应函数中同步更新void CGomoku2025View::OnDifficultyEasy() { GetDocument()-SetDifficulty(2); }6. 让你的作业脱颖而出三个可立即落地的进阶技巧6.1 实现“智能提示”功能高亮玩家下一步最优位置在CGomoku2025View::OnDraw()中于绘制棋子前插入提示逻辑// 获取当前玩家可选最优位置简化版模拟AI第一步 if (pDoc-m_currentPlayer 1 !pDoc-m_gameOver) { CPoint hint GetHintPosition(); // 新增函数 if (hint.x ! -1) { int cx offsetX hint.y * cellSize; int cy offsetY hint.x * cellSize; CPen pen(PS_DASH, 2, RGB(255, 0, 0)); CPen* pOldPen pDC-SelectObject(pen); pDC-Ellipse(cx - 15, cy - 15, cx 15, cy 15); pDC-SelectObject(pOldPen); } } CPoint CGomoku2025View::GetHintPosition() { CGomoku2025Doc* pDoc GetDocument(); // 简化提示找第一个空位实际可调用浅层Minimax for (int i 0; i 15; i) { for (int j 0; j 15; j) { if (pDoc-m_board[i][j] 0) { return CPoint(i, j); } } } return CPoint(-1, -1); }价值点老师演示时看到红色虚线圈会立刻感知“这程序懂策略”远超基础功能。6.2 添加音效反馈用PlaySound API实现落子声在CGomoku2025View.cpp中包含头文件#include mmsystem.h #pragma comment(lib, winmm.lib)在MakeMove()成功后播放音效// 在CGomoku2025Doc::MakeMove()末尾添加 if (player 1) { PlaySound(TEXT(sound\\black.wav), NULL, SND_ASYNC | SND_FILENAME); } else { PlaySound(TEXT(sound\\white.wav), NULL, SND_ASYNC | SND_FILENAME); }实操建议sound文件夹放在exe同目录使用Audacity将音效压缩至10KB以内采样率22050Hz单声道SND_ASYNC避免阻塞主线程SND_FILENAME绕过资源编译流程。6.3 打包免安装发布用Enigma Virtual Box打包为单文件VS2022生成的exe依赖msvcp140.dll等直接发送易失败。推荐方案下载Enigma Virtual Box免费版足够主程序填入Gomoku2025.exe添加依赖文件msvcp140.dll,vcruntime140.dll从C:\Windows\System32复制勾选“加载DLL到进程”输出单文件Gomoku2025_Standalone.exe约8MB。验证方法在全新Win10虚拟机中双击运行确认无报错。我带过三届C课程设计最常听到的学生抱怨是“功能都写了但老师说‘不够工程化’”。其实所谓工程化就是把OnDraw里的坐标算准、把Minimax的剪枝写实、把线程中断做稳、把dll依赖打平——没有玄学只有把每个环节的边界条件想透。这项目真正难的不是算法而是让15×15的二维数组在MFC的消息泵里稳如磐石地呼吸。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网