简介基于VC的在线联机五子棋游戏设计与实现源码包面向C学习者、课程设计或毕业设计需要联网博弈项目的人群。资源包含完整工程文件与可执行程序支持双人对战与人机对战两种模式双人模式由黑白双方鼠标交替落子先连成五子者获胜并可返回菜单人机模式中人类执黑先行AI自动应对覆盖了棋盘状态管理、落子判断、胜负检测、AI搜索等核心模块。全包共26个文件以7个cpp源文件、5个h头文件为主体附带设计报告docx、编译依赖文件depend、工程配置cbp与可直接运行的exe压缩包仅1.7MB结构紧凑。已有336人学习使用。通过源码可了解VC下GDI绘制、鼠标交互、AI决策流程及联机对战逻辑设计报告辅助理解整体框架适合作为课设参考或进一步扩展开发的基础。1. 从 zip 到能联机对战VC 五子棋到底难在哪从网上某个 VC 资源网站下载一份《基于VC的在线联机五子棋游戏设计与实现.zip》绝大多数人的第一反应是解压、打开工程、编译然后在一台电脑上跑两个实例结果要么连接不上要么连上了落子乱跳最后把责任推给“源码有问题”。这类项目的难点从来不在五子棋规则而在“能连上、能对局、不崩”。整套系统本质是三件事一个 MFC 对话框界面一个 C/S 两层的 Socket 通信一套 15 路棋盘上的落子与判胜逻辑。适合正在做课程设计、毕业设计的人也适合想用一个小项目把 C 网络编程和 Windows 界面编程串起来练一遍的从业者。读完这篇文章你会得到一份可以直接照着搭的架构、一套能抄的通信协议代码以及联调两天才能遇到的坑。2. 先拆三层架构再写代码C/S 框架、消息协议与工程骨架2.1 为什么课程设计几乎都用 C/S而不是点对点五子棋在线联机最常见的两种网络模型是一台服务器、一台客户端C/S或者两个客户端直接点对点P2P。你从 VC 资源网站下载的例程绝大多数都是前者。原因很实际C/S 模型天然适合“一局游戏只有一个主持人”的语义服务器负责创建房间、等待连接、决定先手、广播结果客户端只负责表现和落子请求。P2P 虽然代码量更少但必须先解决“谁先开监听、谁去连接”的协商问题一旦有一方处于内网UDP 打洞或端口映射会直接把新手劝退。从模块划分上看我一般会把整个工程切为三块模块职责依赖界面模块MFC 对话框、棋盘绘制、鼠标落子、状态栏提示仅依赖逻辑模块逻辑模块棋盘状态、落子合法性、胜负判定不依赖界面与网络通信模块Socket 监听、连接、收发消息、收发缓冲仅依赖协议头文件这个划分决定了你能不能在一个晚上把程序调通。如果把“收到消息后直接改界面”“鼠标点击后直接发包”写在一起初期跑得爽后期加一个悔棋功能就会把所有代码翻一遍。先写协议再写逻辑最后把界面和网络接到逻辑上这是最省事的路。2.2 通信协议先行定长 4 字节的消息结构在线联机五子棋最核心的通信内容是谁落子、落在哪里、谁赢了、谁悔棋。为了避免粘包和半包带来的解析地狱我强烈建议直接把消息设计成定长结构体。网络数据本质是字节流不带边界定长意味着收到 4 个字节就是一条完整消息收到 7 个字节就是一条消息加半条消息逻辑上非常清晰。// NetProto.h —— 客户端和服务端共用的协议头文件 #pragma pack(push, 1) // 强制 1 字节对齐保证结构体大小固定为 4 enum MsgType { MSG_HELLO 1, // 客户端连上后主动打招呼 MSG_START 2, // 服务端通知客户端对局开始extra1 表示服务端先手黑棋 MSG_MOVE 3, // 落子x、y 为 0~14 的棋盘坐标 MSG_WIN 4, // 某方获胜extra1 表示黑胜extra2 表示白胜 MSG_BYE 5, // 主动退出或掉线通知 }; struct NetMsg { BYTE type; // 消息类型见 MsgType BYTE x; // 横坐标0~14 BYTE y; // 纵坐标0~14 BYTE extra; // 附加参数先手标记 / 胜负标记 / 保留 }; #pragma pack(pop)这个结构体的核心参数有三个type决定消息语义x和y是落子坐标extra是万金油字段。比如MSG_START里用extra1表示服务器执黑先行extra2表示客户端执黑先行MSG_WIN里用它表示哪一方获胜。#pragma pack(push, 1)是为了让编译器按 1 字节对齐保证sizeof(NetMsg)一定是 4不会因为默认 4 字节对齐而变成 8否则两端对同一段字节流的解析就会错位。这一点在 VC6.0 老工程里尤其容易踩新工程用 VS2017 的默认值也未必是 1。有了这个 4 字节消息整个通信模型的复杂度就降低了一大截不需要解析变长字符串不需要约定结束符只需要处理“收到 4 字节整数倍数据”这一种情况。字节序方面局域网内两端都是 x86 小端直接用结构体指针读内存即可如果以后要跨平台再对端口用htons/ntohs转换棋盘坐标不受影响。2.3 工程骨架两个工程还是一个工程环境怎么搭拿到一份五子棋源码先看它是 VC6.0 工程还是 VS2017 工程。老例程通常包含Server.dsp和Client.dsp两个工程新一点的会是一个解决方案里两个.vcxproj。如果只有一份代码我建议照这个方式拆开一个对话框程序通过命令行参数决定自己是服务器还是客户端或者干脆复制成两个独立工程。一个工程两个模式的缺点是全局变量互相污染调试时切来切去容易精神分裂。环境搭建上直接面对两个高频问题一是老 VC6 工程在 VS2017 上打开后提示“需要 vc 2013 安装”或转换向导二是编译报错cannot convert from const char [N] to CString。前者是运行库版本问题后者是字符集问题——新工程默认 Unicode老代码里AfxMessageBox(hello)会编译失败。我的做法是属性页里把字符集显式改成“使用多字节字符集”或者全代码用TCHAR/_T()包字符串。对课程设计来说改成多字节字符集最快代码里可以直接写char、sprintf不用到处加_T。然后是 Winsock 初始化每个进程只需要做一次放在对话框OnInitDialog里即可// ServerDlg.cpp / ClientDlg.cpp 的 OnInitDialog 中 WSADATA wsaData; int nResult WSAStartup(MAKEWORD(2, 2), wsaData); if (nResult ! 0) { AfxMessageBox(_T(Winsock 初始化失败)); return FALSE; }MAKEWORD(2, 2)表示请求 2.2 版本的 Windows Socket 实现返回值非 0 时不要继续往下走直接提示后退出。很多人忽略这一步导致后面socket()返回INVALID_SOCKET又找不到原因。理论上 MFC 在CWinApp::InitInstance里也可能隐式初始化但显式写一次是最稳妥的新手调错时也能明确知道是哪一步挂了。3. 棋盘逻辑先单机跑通数据落子、四向判胜与 MFC 绘图3.1 棋盘数据结构与落子函数联机五子棋的棋盘通常有两种选择15 路或 19 路。课程设计里最常见的是 15 路对应标准五子棋规则。数据结构不需要任何花哨设计一个全局二维数组就够了// ChessLogic.h const int BOARD_SIZE 15; // m_board[i][j]0空1黑棋2白棋 extern int m_board[BOARD_SIZE][BOARD_SIZE]; // 落子函数成功返回 true失败返回 false 并给出原因 bool PlacePiece(int x, int y, int player) { if (x 0 || x BOARD_SIZE || y 0 || y BOARD_SIZE) return false; // 越界 if (m_board[x][y] ! 0) return false; // 已有棋子 if (player ! 1 player ! 2) return false; // 非法玩家标识 m_board[x][y] player; return true; }这个函数的三个参数很直白x、y是棋盘坐标player是执子方。关键设计点是“把合法性判断集中在一个函数里”。有些同学在鼠标点击事件里写一遍判断在收到网络消息里又写一遍判断两次判断逻辑稍有不一致就会出现“本地看到棋子落下、对面没收到”或“对面发来一个越界坐标导致数组越界崩溃”。只留一个入口所有落子都必须经过PlacePiece这是最省心的做法。坐标系的约定在这里就要定死我习惯用board[x][y]x对应列从左到右y对应行从上到下。这样和后面画棋盘时的dc.Ellipse(ORIGIN_X x * CELL, ...)天然对应不用在绘图函数里再做一次 x/y 交换。如果你看到某份代码里是board[y][x]也别惊讶跟着它的绘图函数走就行但自己的代码里注意不要再混用。3.2 胜负判定四方向双倍计数五子棋判胜逻辑是所有源码里最容易写错的部分。错误典型有两种只扫了一个方向就返回或者从棋盘左上角到右下角全盘扫描导致最后一手判断重复。正确做法是只对最后一手落子的位置沿四个方向分别向两边延伸统计同色棋子数量任何方向连成 5 子即胜。bool CheckWin(int x, int y) { int player m_board[x][y]; if (player 0) return false; // 四个方向向量水平、垂直、主对角线、副对角线 int dir[4][2] { {1,0}, {0,1}, {1,1}, {1,-1} }; for (int i 0; i 4; i) { int count 1; // 当前棋子自身 // 正方向延伸 for (int step 1; step 5; step) { int nx x dir[i][0] * step; int ny y dir[i][1] * step; if (nx 0 || nx BOARD_SIZE || ny 0 || ny BOARD_SIZE) break; if (m_board[nx][ny] ! player) break; count; } // 反方向延伸 for (int step 1; step 5; step) { int nx x - dir[i][0] * step; int ny y - dir[i][1] * step; if (nx 0 || nx BOARD_SIZE || ny 0 || ny BOARD_SIZE) break; if (m_board[nx][ny] ! player) break; count; } if (count 5) return true; } return false; }这段代码里最需要注意的是反方向扫循环。因为最后一手棋可能落在连珠正中间比如棋盘上已经有(3,5)到(7,5)五颗黑子最后一手是(5,5)如果只朝一个方向数只能数到 3 个必须反方向合起来才能判定胜利。count从 1 开始是因为当前棋子已经算一颗。step 5是优化项五子棋只需要统计到 5 颗即可多了没必要扫完整条线。边界条件是另一个高频翻车点当落子在棋盘边缘正方向或反方向会很快越界所以每个step都要先判断nx、ny是否合法。如果漏掉这个判断数组越界可能不会立刻崩溃但会在某次特定落子时读到脏数据导致误判。我在调试阶段踩过一次坐标 (0,0) 落子后正向扫描没问题反向扫描直接读到了board[-1][0]win 的条件莫名成立花了半小时才定位到缺了越界判断。3.3 MFC 绘图与鼠标落子15 条线加双缓冲界面部分用 MFC 对话框实现时核心只有两个函数OnPaint画棋盘和棋子OnLButtonDown把鼠标坐标换算成格子坐标并调用落子逻辑。棋盘区域我一般用CStatic控件或直接在对话框上画关键是设定三个常量原点坐标ORIGIN_X、ORIGIN_Y和格子边长CELL。const int ORIGIN_X 30; // 棋盘左边距 const int ORIGIN_Y 30; // 棋盘上边距 const int CELL 30; // 每个格子边长 void CChessDlg::OnPaint() { CPaintDC dc(this); // 画 15x15 网格 for (int i 0; i BOARD_SIZE; i) { dc.MoveTo(ORIGIN_X, ORIGIN_Y i * CELL); dc.LineTo(ORIGIN_X (BOARD_SIZE - 1) * CELL, ORIGIN_Y i * CELL); dc.MoveTo(ORIGIN_X i * CELL, ORIGIN_Y); dc.LineTo(ORIGIN_X i * CELL, ORIGIN_Y (BOARD_SIZE - 1) * CELL); } // 画已经落下的棋子 CBrush brush; for (int x 0; x BOARD_SIZE; x) { for (int y 0; y BOARD_SIZE; y) { if (m_board[x][y] 0) continue; int cx ORIGIN_X x * CELL; int cy ORIGIN_Y y * CELL; if (m_board[x][y] 1) brush.CreateSolidBrush(RGB(0, 0, 0)); // 黑棋 else brush.CreateSolidBrush(RGB(255, 255, 255)); // 白棋 dc.SelectObject(brush); dc.Ellipse(cx - CELL / 2 1, cy - CELL / 2 1, cx CELL / 2 - 1, cy CELL / 2 - 1); brush.DeleteObject(); } } }绘制时注意Ellipse的四个参数要留出 1 到 2 像素的间隙否则相邻两格子的棋子边缘会连在一起。鼠标点击的换算公式是x (point.x - ORIGIN_X CELL / 2) / CELL加CELL / 2是为了让点击在格子边缘时也能就近吸附到最近的交叉点。void CChessDlg::OnLButtonDown(UINT nFlags, CPoint point) { int x (point.x - ORIGIN_X CELL / 2) / CELL; int y (point.y - ORIGIN_Y CELL / 2) / CELL; if (x 0 || x BOARD_SIZE || y 0 || y BOARD_SIZE) return; // 点击棋盘外 // 合法且轮到我方时才落子网络部分稍后补充 if (PlacePiece(x, y, m_myPlayer)) { Invalidate(FALSE); // 只重绘客户区不擦除背景减少闪烁 // 这里后续会调用 SendMsg(MSG_MOVE, x, y, 0) } }Invalidate(FALSE)是低成本的防闪烁手段FALSE表示不先擦除背景。对课程设计来说这比OnEraseBkgnd返回TRUE再加内存 DC 的做法简单得多效果也够用。等单机逻辑全部验证通过再把这个函数里的注释替换成真正的发包调用。4. Socket 联机从连接到对局先手约定、收包缓存与断线处理4.1 服务端CAsyncSocket 与 OnAccept/OnReceive网络部分有两种主流写法直接用 WinSock API 的socket()bind()listen()accept()或者用 MFC 封装的CAsyncSocket。我见过大量课程设计代码用阻塞 socket 加recv()死等这有一个致命问题对话框 UI 线程会被recv()卡死对方不掉线你就永远等不到消息窗口拖动都变得迟滞。传统解法是开一个工作线程做recv然后用PostMessage通知主线程但既然是 VC/MFC 项目更优雅的做法是用CAsyncSocket它在后台通过窗口消息机制回调OnReceive不阻塞 UI 线程。// 服务端监听 socket 派生类 class CListenSocket : public CAsyncSocket { public: virtual void OnAccept(int nErrorCode) override; }; void CListenSocket::OnAccept(int nErrorCode) { if (nErrorCode ! 0) return; CAsyncSocket* pClient new CAsyncSocket(); if (Accept(*pClient)) { // 把新连接交给对话框管理用成员变量保存 theApp.m_pMainDlg-OnNewConnection(pClient); } else { delete pClient; } }调用流程是先在对话框OnInitDialog里Create(5600)监听端口 5600然后Listen()当有客户端connect进来框架自动调用OnAccept此时调用Accept取回一个已连接的 socket。注意Accept传入的必须是一个新的CAsyncSocket对象不能在原监听 socket 上直接收发数据。theApp那行是示意实际代码里你可以用一个全局指针或者把对话框指针传进监听类只要保证OnAccept里能访问到主对话框即可。发送消息的代码我封装成一个通用函数所有消息类型都走同一条路bool SendMsg(CAsyncSocket sock, const NetMsg msg) { int nSent sock.Send(msg, sizeof(NetMsg)); return nSent sizeof(NetMsg); }CAsyncSocket::Send与阻塞 socket 的send一样不能保证一次把 4 个字节全部发出极端情况下可能只发出去 1 字节。对课程设计这种局域网环境4 字节消息一次发完的概率极高但严谨起见你应该写一个循环把没发完的剩余字节继续发。这个小细节不会影响演示但面试官追问时能答上来会加分。4.2 客户端连接、握手与收到 START 才能落子客户端同样派生自CAsyncSocket重写OnReceive和OnClose。连接动作放在对话框的“连接”按钮事件里void CClientDlg::OnBnClickedConnect() { // 假设界面上有一个 IP 编辑框 m_strIP 和端口编辑框 m_nPort UpdateData(TRUE); m_sockClient.Create(); // 客户端 socket 不需要绑定端口 if (!m_sockClient.Connect(m_strIP, m_nPort)) { AfxMessageBox(_T(连接失败请确认服务端已启动)); } }Connect是异步的返回值只代表“连接发起成功”不代表“已经连上”。真正的连接结果由OnConnect回调返回但课程设计里通常没人深究这一步只要服务端已启动Connect几乎都成功。连接上之后客户端要立刻发送一条MSG_HELLO服务端收到HELLO后回复MSG_START客户端收到START才真正进入对局状态。这套握手机制防止了“界面已经是棋盘但对端什么时候准备好都不知道”的灰色状态。4.3 先手约定与轮次切换避免两个人同时落子联机五子棋能不能玩很大程度上取决于“先手逻辑”有没有设计清楚。我看过的失败代码里最常见的错误是让“连接发起方”先手或者干脆双方都能随时落子结果两个人都能下棋盘上出现黑白交叉的非法局面。正确做法是先手完全由服务端决定并通过MSG_START消息中的extra字段告知客户端。// 服务端收到 MSG_HELLO 后 NetMsg msg; msg.type MSG_START; msg.x 0; msg.y 0; msg.extra 1; // 1服务端执黑先行 SendMsg(m_sockClient, msg); // 客户端收到 MSG_START 后 if (msg.extra 1) { m_myPlayer 2; // 服务端黑棋客户端白棋 m_myTurn false; // 对方先走 } else { m_myPlayer 1; m_myTurn true; }轮次判断直接在鼠标落子函数里加一道闸不是自己的回合点击棋盘没反应状态栏显示“等待对方落子”。收到MSG_MOVE后先判断消息里的坐标是否是空位再调用PlacePiece落子然后m_myTurn true并刷新界面。这里有个事情必须想清楚MSG_MOVE到达本方时说明对方已经完成落子接下来就该你走。这段逻辑写在OnReceive里不要写在OnLButtonDown里。4.4 收包缓存与半包/粘包处理这是网络部分最核心的一段代码。前面用定长 4 字节结构体已经避免了“按行读”“按结束符拆”的复杂性但recv/OnReceive仍然不保证一次正好收到 4 字节。你需要一个字节缓存区把收到的新数据追加到尾巴然后从头扫描有没有完整的NetMsg。// 每个 socket 连接维护一个缓存区 BYTE m_recvBuf[1024]; // 缓存区 int m_recvLen 0; // 当前缓存里有效字节数 void CClientSocket::OnReceive(int nErrorCode) { if (nErrorCode ! 0) return; // 1. 先把网络数据读进临时缓冲区 BYTE temp[256]; int nRead Receive(temp, sizeof(temp)); if (nRead 0) { OnClose(0); // 收到 0 字节说明对端关闭 return; } if (nRead SOCKET_ERROR) { int err GetLastError(); if (err ! WSAEWOULDBLOCK) OnClose(0); return; } // 2. 追加到缓存尾部注意不要越界 if (m_recvLen nRead sizeof(m_recvBuf)) { // 实际工程中应扩容课程设计里清空重来即可 m_recvLen 0; return; } memcpy(m_recvBuf m_recvLen, temp, nRead); m_recvLen nRead; // 3. 从缓存中解析完整消息 int offset 0; while (m_recvLen - offset (int)sizeof(NetMsg)) { NetMsg* pMsg (NetMsg*)(m_recvBuf offset); // 在这里根据 pMsg-type 分发落子、开始、胜负、退出 ProcessMsg(*pMsg); offset sizeof(NetMsg); } // 4. 把剩余不完整的字节移到头部下次继续 if (offset 0) { memmove(m_recvBuf, m_recvBuf offset, m_recvLen - offset); m_recvLen - offset; } }这段代码有三个关键点。第一读完temp后要立即memcpy追加到缓存不能在temp上直接解析因为temp是临时数组下次Receive会覆盖它。第二while循环处理粘包对端连续发来两个MSG_MOVE一次OnReceive可能同时收到 8 字节循环能依次拆出两条。第三memmove处理半包如果收到 5 字节前 4 字节解析完剩下 1 字节就是下一条消息的开头要挪到数组头部等待下次Receive再补 3 字节凑齐。这里不能用memcpy因为源和目的地址可能重叠memmove才能保证正确。5. 联机五子棋常见问题排查5 个高频坑与定位顺序5.1 现象棋盘上突然多出若干颗乱掉的棋子或坐标错乱原因半包与粘包没有处理。很多人直接在OnReceive里拿到多少字节就按照NetMsg*强转解析如果这次只收到 2 字节结构体里的x、y就会是上次缓存里的残留数据如果一次收到 8 字节就只解析了第一条第二条被覆盖掉。解决用第 4.4 节的缓存追加 循环解析方案把所有解析逻辑收敛到ProcessMsg。现象一旦出现先在每次Receive后把收到的字节数、解析出的消息类型打印到调试窗口或写入日志文件盯两局就能确认是否粘包。5.2 现象服务端关闭程序后客户端窗口假死或卡住原因如果客户端用的是阻塞recv()它会一直等待数据服务端 socket 关闭后recv()返回 0 或SOCKET_ERROR但阻塞线程如果没处理退出信号就会空转或死循环。即便用CAsyncSocket如果OnClose里没有关闭socket句柄并释放资源也会出现“连接已断开但程序毫无反应”。解决统一在OnClose里做四件事调用Close()关闭 socket、把指向该 socket 的指针置空、更新界面状态栏为“对方已断开”、弹一次AfxMessageBox提示。另外在发送MSG_BYE之后发送方延迟 1 秒再关闭 socket避免消息还在系统缓冲区里就被 RST 包打断。5.3 现象两个实例连上了但只有一方能落子另一方点击无反应原因先手状态没有正确传递或者某一方m_myTurn初始值就是false也没有在收到MSG_START时更新。常见于复制粘贴代码时漏掉了MSG_START的消息分发分支。解决按 4.3 节的方式把“谁先手”固化到MSG_START的extra字段中。在服务端和客户端各写一条日志记录“发送 START(extra1)”和“收到 START(extra1)”两行对照日志定位是谁没收到消息还是在ProcessMsg里漏写了分发。这属于逻辑 bug 而不是网络问题用日志比断点好使。5.4 现象程序在自己机器上能跑复制到教室电脑上报 0xc000007b 或缺 dll原因缺少对应的 VC 运行库。用 VS2017 编译的工程在目标机器上需要安装 vc 2015-2022 运行库VC6.0 老程序需要 vc 2005/2008 运行库。教室电脑常年不更新很容易中招。解决两个方案选一个就行。方案一是把工程属性里的“运行库”从“动态库 (/MD)”改成“静态库 (/MT)”这样 MFC 和 CRT 全部静态链进 exe单文件复制即可运行方案二是在发布包里附带对应版本的 vc 运行库安装包。对课程设计答辩来说方案一最省事但 exe 体积会从几百 KB 涨到几 MB属正常现象。5.5 现象OnReceive只被调用一次之后再收不到消息原因OnReceive是基于窗口消息机制的如果你在OnReceive里做了耗时操作比如Sleep、弹MessageBox、或者执行了一个大循环窗口消息泵被阻塞后续的通知消息根本进不来。另一个常见原因是 socket 对象被提前销毁回调时访问到野指针。解决OnReceive里只做“读数据 存缓存 解析 更新简单状态”所有界面刷新用Invalidate触发重绘不要弹框。如果要弹“你赢了”这样的提示用PostMessage发自定义消息给主窗口等OnReceive返回后主窗口再弹框。我在最初版本里直接在OnReceive里调了AfxMessageBox结果第二个人赢的提示弹完后第三手落子就再也不触发了折腾了一个晚上才反应过来是消息泵卡死。6. 验证三连与进阶方向从回环测试到 AI 补全拿到代码之后不要急着联机先按三个层次做验证每一层都能暴露不同类别的问题。第一步是单机逻辑验证。在OnInitDialog里加一个调试开关#define LOCAL_TEST 1让鼠标落子时不经过网络直接调用PlacePiece和CheckWin。把第 3 章的判胜函数故意摆出各种形状测试横五、竖五、左斜五、右斜五、边缘五子。这一步能隔离掉全部棋盘逻辑 bug。第二步是回环测试。在同一台电脑上运行两个实例服务端监听 5600客户端连接127.0.0.1。回环测试通过说明协议设计、消息解析、先手切换逻辑都没有大问题。如果连回环都不通把第 4 章的收发日志打开逐条核对MSG_HELLO、MSG_START、MSG_MOVE的顺序和内容。第三步是跨机器测试。关掉 Windows 防火墙或者手动添加 5600 端口的放行规则两台电脑连同一个局域网。跨机器失败的第一嫌疑是防火墙第二嫌疑是 IP 填错用服务端 IPipconfig查到的 IPv4 地址不要填虚拟机地址。如果服务端能OnAccept但客户端收不到数据大概率还是防火墙拦截了入站数据。以上都跑通之后这个项目已经具备完整对局能力。再往上走我建议按这个顺序补功能悔棋协议MSG_UNDO 应答、双方各限一次、认输按钮MSG_LOSE、30 秒无操作超时判负SetTimerOnTimer、断线重连客户端记录棋盘现场连上后服务端回放。AI 方面可以做一个最简单的“打分表”算法对每个空位扫描横竖斜四个方向的活二、活三、冲四累加一个分值分值最高的位置就是下一步这个难度不高但效果明显适合毕业设计拿来当亮点。这个项目我做下来最大的体会是网络联机五子棋最难的不是五子棋而是“双方状态一致”。协议定长、先手归服务端、收包进缓存这三个习惯帮我避开了至少十个晚上的返工。希望帮到你。本文还有配套的精品资源点击获取