C++五子棋源码解析:人机/人人双模式与AI博弈实现
简介一套用C实现的五子棋对战程序源码包同时支持人机与人人两种模式适合C初学者和游戏开发入门者学习类的设计、AI决策算法以及图形界面交互可作为第一个完整的游戏练手项目。压缩包共收录7个文件整体仅106KB内含3个可直接运行的可执行文件、3个源程序文件覆盖鼠标版、键盘版及不同对战模式和1个说明文本结构紧凑便于边读源码边对照运行。目前已有180人学习下载。通过这套代码可以观察棋盘与棋子类如何抽象理解基于规则或搜索算法的人机落子逻辑并掌握用户输入校验和界面刷新的基本流程不同版本代码均可直接编译运行也适合作为课程设计或业余编程的起步模板帮助动手能力较弱的读者快速上手。1. C五子棋人机/人人两种模式都齐的小而美游戏源码用 C 写一个带图形界面的五子棋最有趣的从来不是画棋盘而是那个跟你下棋的电脑——而这份演示源码把两者的实现都备齐了纯 C 编写同时支持人机对战和本地双人同屏对战开局菜单里选模式电脑方默认执白。不依赖任何第三方游戏引擎渲染用系统自带的 Win32/GDI 完成拿去编译的门槛很低。对刚学完 C 的开发者这是从写算法题过渡到写完整小游戏最顺的一步对研究简单博弈 AI 的人评估函数和搜索部分也可以逐行读。整个工程把渲染、规则、AI 分在三层跑通之后再改成自己的版本并不难。2. 技术栈与工程结构为什么这种架构能撑住人机/人人两种模式2.1 渲染方案为什么我推荐 Win32/GDI 而不是控制台说实话五子棋写控制台版是完全可行的画个 15 行 15 列的字符棋盘P1/P2 轮流输入坐标配上清屏逻辑就能玩。但那种版本离“游戏”太远没有鼠标没有图形反馈更别提人机对战时观感上的紧张感。用大型游戏框架则是杀鸡用牛刀一个五子棋项目堆上完整的引擎依赖新手光配置构建链就能劝退一多半。这份源码选的是 Win32 加 GDI。Win32 是 Windows 系统的原生窗口 APIGDI 负责在窗口上画线、画圆、贴图都属于操作系统自带的能力编译器装好就能链接不需要单独安装第三方库。很多教学版喜欢用 EasyX 图形库画棋盘优点是代码短但它要求使用者额外安装库文件、配置 include 路径稍有版本出入就报一堆找不到头文件的错。相比之下 Win32 的样板代码确实多一些可只要把窗口过程搞清楚后面的逻辑反而是最稳定的部分。我用 GDI 顺手的地方棋盘网格用 MoveToEx/LineTo棋子用 Ellipse 画实心圆木纹背景用 FillRect 填充整套绘制代码不超过一百行。渲染这件事一旦和游戏逻辑分开后面要换主题皮肤或者改成贴图版本都不需要动棋子判定。如果是在 Linux/macOS 上开发这份源码的窗口代码没法直接编译需要改到 Qt 或 SDL那是移植工程而不是读源码的范畴不在这次拆解范围内。2.2 工程结构与文件职责这个版本没有把代码堆在单个 main.cpp 里而是拆成了几个文件每个文件的职责很清楚适合边读边查。文件主要职责common.h棋盘尺寸、格子像素、方向数组和 GameState 结构体main.cppWinMain 入口、窗口类注册、消息循环board.cpp棋盘和棋子的绘制以及鼠标坐标到格子的换算game.cpp落子、胜负判定、平局、重新开局等规则逻辑ai.cpp人机模式下电脑的落子决策从接口上看main.cpp 只负责把窗口立起来所有鼠标键盘事件都回调到 WndProcboard.cpp 对外暴露 DrawBoard 和 PointToGridgame.cpp 暴露 ApplyMove、CheckWinai.cpp 暴露 GetBestMove。谁依赖谁都一目了然渲染层不知道 AI 怎么算分AI 也不关心棋子用什么颜色画。对应到磁盘上大概是这样的摆放gomoku/ ├─ common.h ├─ main.cpp ├─ board.cpp ├─ game.cpp ├─ ai.cpp └─ build.batbuild.bat 里是一条 g 编译命令把上面四个源文件一起编成 gomoku.exe。有人习惯把所有代码塞进一个 cpp方便是方便但改 AI 时容易误伤渲染拆开文件之后比如把 GetBestMove 从贪心换成搜索只需要动 ai.cpp其他文件一行都不用改。这个工程能同时支持人机和人人靠的是 GameState 里一个 humanVsAI 开关落子前先判断当前模式如果是人机且当前轮到电脑就调 ai.cpp 里计算出的落点再走一遍 ApplyMove如果是人人则完全跳过 AI直接等待下一个鼠标点击。两条游戏流程在同一套落子逻辑上汇合所以两种模式不会出现玩法不一致的情况。读源码时我建议按 common.h → game.cpp → board.cpp → ai.cpp 的顺序来。先知道棋盘长什么样再看规则怎么改状态然后看界面怎么把状态画出来最后才去看 AI 怎么利用这些状态做决策。反过来先啃 AI 的话遇到棋盘数组、方向数组、坐标换算这些前置概念会比较吃力。2.3 核心数据模型先用二维数组AI 优化以后再说数据结构放在 common.h 里是全项目最值得先读的文件后面所有函数都在操作它。// common.h #pragma once constexpr int BOARD_SIZE 15; // 棋盘 15x15 constexpr int CELL_SIZE 36; // 每个格子的像素宽度 constexpr int OFFSET_X 30, OFFSET_Y 30; // 棋盘左上角相对窗口的坐标 enum class Piece : char { Empty 0, Black 1, White 2 }; struct GameState { Piece grid[BOARD_SIZE][BOARD_SIZE]; // 棋盘状态 bool turnIsBlack true; // true 表示轮到黑棋 bool gameOver false; // 对局是否结束 bool humanVsAI false; // 人机模式开关 Piece aiColor Piece::White; // 人机模式下 AI 执白 int moveCount 0; // 已落子数用于平局判断 };选二维数组而不是一维有两个原因。第一是直白CheckWin 时按 grid[row][col] 访问和你脑海里画出来的棋盘是一一对应的不用理解索引换算。第二是 15x15 的格子总数也就 225 个性能在这个量级上完全没有压力。真要优化可以维护一个 history 数组记录落子顺序再维护一个 empty 栈存空位让 AI 遍历只走空位——但那是以后的事先把逻辑写对。坐标换算也依赖这组常量第 r 行第 c 列的交叉点像素坐标是 (OFFSET_X cCELL_SIZE, OFFSET_Y rCELL_SIZE)。窗口尺寸是 600x640正好放下 15 列格子加左右留白下面再留一点空间显示当前状态。这个对应关系在 board.cpp 里会反复用到建议先把这一行写在纸上。3. 核心模块拆解从鼠标点击到棋局胜负的完整链路整个程序的行为可以总结成一条链路鼠标按下 → 坐标换算 → 合法性检查 → 更新棋盘 → 重绘 → 胜负判定 → 轮到下一个人或 AI。下面按这条链路把每个环节的代码拆开讲。3.1 窗口入口与消息循环先把地基铺好五子棋的窗口部分不复杂WinMain 只做三件事注册窗口类、创建窗口、进入消息循环。// main.cpp int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrev, LPSTR lpCmdLine, int nCmdShow) { WNDCLASS wc {}; wc.lpfnWndProc WndProc; wc.hInstance hInst; wc.hCursor LoadCursor(NULL, IDC_ARROW); wc.hbrBackground (HBRUSH)(COLOR_WINDOW 1); wc.lpszClassName TEXT(GomokuWindow); RegisterClass(wc); HWND hwnd CreateWindow(wc.lpszClassName, TEXT(五子棋 - 人机/人人对战), WS_OVERLAPPEDWINDOW, 100, 100, 600, 640, NULL, NULL, hInst, NULL); ShowWindow(hwnd, nCmdShow); MSG msg; while (GetMessage(msg, NULL, 0, 0)) { TranslateMessage(msg); DispatchMessage(msg); } return 0; }窗口尺寸 600x640我算了一下15 个格子每个 36 像素14 个间距是 504 像素加 OFFSET 各 30总共 564 像素宽窗口留出 600 足够高度多出来的部分留给菜单和底边状态栏。所有事件都从 DispatchMessage 送进 WndProc鼠标点击、菜单选择、重绘请求实际上都在 WndProc 的 switch-case 里处理。这里容易忽略的是 GetMessage 返回值的判断。标准写法是 while (GetMessage(...)) 而不是 while (GetMessage(...) 0)因为退出消息 WM_QUIT 返回 0写成大于 0 会导致窗口关闭后循环不退出。菜单功能是挂在窗口菜单栏上的点“人人对战”或“人机对战”时通过 WM_COMMAND 消息切换 GameState.humanVsAI同时清空棋盘重新开局。3.2 双缓冲渲染别让棋子一闪一闪棋盘绘制集中在 board.cpp 的 DrawBoard用双缓冲避免闪烁。// board.cpp void DrawBoard(HWND hwnd, const GameState state) { PAINTSTRUCT ps; HDC hdc BeginPaint(hwnd, ps); HDC memDC CreateCompatibleDC(hdc); HBITMAP bmp CreateCompatibleBitmap(hdc, 600, 620); HGDIOBJ old SelectObject(memDC, bmp); RECT rc {0, 0, 600, 620}; FillRect(memDC, rc, (HBRUSH)GetStockObject(WHITE_BRUSH)); for (int i 0; i BOARD_SIZE; i) { MoveToEx(memDC, OFFSET_X, OFFSET_Y i * CELL_SIZE, NULL); LineTo(memDC, OFFSET_X (BOARD_SIZE - 1) * CELL_SIZE, OFFSET_Y i * CELL_SIZE); MoveToEx(memDC, OFFSET_X i * CELL_SIZE, OFFSET_Y, NULL); LineTo(memDC, OFFSET_X i * CELL_SIZE, OFFSET_Y (BOARD_SIZE - 1) * CELL_SIZE); } for (int r 0; r BOARD_SIZE; r) for (int c 0; c BOARD_SIZE; c) if (state.grid[r][c] ! Piece::Empty) DrawPiece(memDC, r, c, state.grid[r][c]); BitBlt(hdc, 0, 0, 600, 620, memDC, 0, 0, SRCCOPY); SelectObject(memDC, old); DeleteObject(bmp); DeleteDC(memDC); EndPaint(hwnd, ps); }没有双缓冲时每次 WM_PAINT 都在窗口 DC 上直接画线画圆窗口来回拖动能看到明显的“先白屏再逐步画出来”的闪烁感原因是绘制过程暴露在眼前。这里先建一个内存 DC所有线和棋子都画到内存位图上最后用 BitBlt 一次性整块贴上去用户看到的始终是完整画面。CreateCompatibleDC 和 CreateCompatibleBitmap 是固定搭配前者建画布后者建一张与窗口兼容的位图SelectObject 把位图选进 DC 才能在它上面画。用完后 DeleteObject/DeleteDC 必须成对调用否则每次重绘都泄漏一点 GDI 资源窗口跑久会越画越卡。WM_PAINT 里只做重绘任何落子、翻盘、清空棋盘的动作都走 InvalidateRect(hwnd, NULL, TRUE) 申请重绘而不是直接调 DrawBoard。这样绘制逻辑只有一条路出错时好排查。DrawPiece 画单颗棋子时用两个圆形叠出立体感先画深色大圆再画浅色内圆黑棋白棋只换颜色代码量很小但观感好很多。3.3 落子输入鼠标坐标到格子的换算鼠标按下消息在 WndProc 里接关键是把像素坐标换算成 row/col。// board.cpp bool PointToGrid(int mouseX, int mouseY, int row, int col) { if (mouseX OFFSET_X - CELL_SIZE / 2 || mouseX OFFSET_X (BOARD_SIZE - 1) * CELL_SIZE CELL_SIZE / 2) return false; if (mouseY OFFSET_Y - CELL_SIZE / 2 || mouseY OFFSET_Y (BOARD_SIZE - 1) * CELL_SIZE CELL_SIZE / 2) return false; col (mouseX - OFFSET_X CELL_SIZE / 2) / CELL_SIZE; row (mouseY - OFFSET_Y CELL_SIZE / 2) / CELL_SIZE; return true; }如果只写 (mouseX - OFFSET_X) / CELL_SIZE鼠标落在格线左边 1 像素时除出来是前一个格子落在右边又向右偏整个棋盘点击都会错位半格。加上 CELL_SIZE/2 再整除等于把每个格子划分成“就近吸附”点在哪条线附近就归到哪条线。这个加半格的写法是我用过多种换算里最不容易翻车的。粗筛条件是把棋盘范围向外延半个格子防止点在窗口空白处时算出 -1 或 15 这类越界下标。合法性检查在 game.cpp 的 ApplyMove 入口统一做gameOver 不能落子对应格子必须为 Empty人机模式下如果轮到 AI 则跳过鼠标响应。落子成功后 moveCount 加一把当前棋子写进 grid调用 InvalidateRect 重绘最后调 CheckWin 判断是否五连。3.4 胜负判定四方向五连检测每次落子后只需要从刚落点向四个方向数连续同色棋子数到 5 就赢。// game.cpp bool CheckWin(const GameState st, int row, int col, Piece piece) { const int dirs[4][2] {{1, 0}, {0, 1}, {1, 1}, {1, -1}}; for (const auto d : dirs) { int count 1; for (int step 1; ; step) { int nr row d[0] * step, nc col d[1] * step; if (nr 0 || nr BOARD_SIZE || nc 0 || nc BOARD_SIZE) break; if (st.grid[nr][nc] ! piece) break; count; } for (int step 1; ; step) { int nr row - d[0] * step, nc col - d[1] * step; if (nr 0 || nr BOARD_SIZE || nc 0 || nc BOARD_SIZE) break; if (st.grid[nr][nc] ! piece) break; count; } if (count 5) return true; } return false; }方向数组只存了四个正向下、右、右下、左下反方向通过减号实现。这样水平方向的正反向合起来是一条完整横线斜向同理。四个方向只要有一个连出五次立刻返回 true。局部检测的好处是每次落子只扫两条线共四个方向最多几十次访问不用全盘扫 225 个格子五子棋盘小全盘扫也没压力但养成局部检测的习惯后换到 19 路围棋棋盘时逻辑也站得住。胜负判定之后还有一层moveCount 到 225 且没人五连就是平局。很多人写五子棋只写“有人赢”漏了这条分支结果对局走到最后一步直接卡死或无响应。game.cpp 里平局走和胜利相同的提示流程只是提示文案不同。4. 人机AI模块从评估函数到搜索的完整博弈实现人机部分才是这份源码里真正值钱的地方。一个能拦住你的 AI核心不是“下得快”而是“知道哪里危险”。整个 AI 代码跑在 ai.cpp 里按数据流可以拆成评估、打分、选择三步。4.1 棋型评估给每个空位算威胁值先把“把一枚棋子放到 (row, col) 之后这个点在四个方向能连出多少子”算出来。朴素版本长这样// ai.cpp - 简化评估 int EvaluatePoint(const GameState st, int row, int col, Piece me) { int score 0; const int dirs[4][2] {{1, 0}, {0, 1}, {1, 1}, {1, -1}}; for (const auto d : dirs) { int cnt 1; for (int s 1; s 5; s) { int nr row d[0] * s, nc col d[1] * s; if (nr 0 || nr BOARD_SIZE || nc 0 || nc BOARD_SIZE) break; if (st.grid[nr][nc] ! me) break; cnt; } for (int s 1; s 5; s) { int nr row - d[0] * s, nc col - d[1] * s; if (nr 0 || nr BOARD_SIZE || nc 0 || nc BOARD_SIZE) break; if (st.grid[nr][nc] ! me) break; cnt; } score cnt * cnt; } return score; }cnt*cnt 是一种粗糙的打分连三得 9 分连四得 16 分差距已经拉开。但问题很明显它分不清“活三”两端都空和“眠三”一端被堵——同样连三个子活三下一步能变活四威胁完全不同。所以正式一点的评估要数完连续段后再看两端的状态然后套一张经验分表棋型分数说明五连100000先手方必赢权重必须压过一切活四50000下一步无论怎么堵都能成五冲四5000只有一端能再落子成五活三5000两端都有机会发展成四眠三500一端被堵发展空间减半活二500基础进攻起点眠二100基本没有威胁分数设计有两条原则一是连子越多分数指数级上升五连的 100000 要保证在任何情况下都高于所有普通棋型之和二是“活”和“眠”拉开一个数量级让 AI 宁可去堵一个活三也不去管一个眠四。这张表看着简单实际调参时你会发现 AI 风格全由这几个数字决定防守系数调高就保守进攻分调高就激进。4.2 进攻与防守的平衡一个空位算两笔账落到落子决策上不能只算“我在这落子能得多少分”还要算“对手在这落子能得多少分”。一个位置对我方和敌方往往同样重要。// ai.cpp - 综合评估某个空位 int ScoreForMove(const GameState st, int row, int col, Piece ai, Piece human) { int attack EvaluatePoint(st, row, col, ai); // 我放这里进攻价值 int defend EvaluatePoint(st, row, col, human); // 对手放这里防守价值 return attack (int)(defend * 1.2); // 防守稍稍加权 }AI 遍历 225 个格子对每个空格算一次 ScoreForMove取分数最高的位置落子这就是一层博弈搜索也是最经典的“一眼看一步”。defend 乘 1.2 的作用是让 AI 在进攻和防守差不多时更倾向补防。我拆项目时试过把它改成 0.8AI 立刻变得头铁经常只顾自己连子而被对手偷家。如果不加防守分这代码在对方摆出活三时基本失灵因为 AI 眼中自己那些无关紧要的一子也许比堵那个活三分数更高。开局时全盘都是空位评估分全是 0AI 会随便挑一个角落落子显得很蠢。常见的处理是给中心点 (7,7) 一个很高的开局权重或者维护一张“开局前几手固定落子”的表。这份源码里用的是前者改动就一行效果立竿见影。4.3 深度扩展从贪心到两层搜索与剪枝要提升 AI 强度就得让电脑考虑“我走完这步对手会怎么应对”。这就是 minimax 的用武之地当前层取自己最大收益下一层取对手最小收益双方轮流换边。// ai.cpp - 两层搜索骨架 constexpr int INF 1e9; int Minimax(GameState st, int depth, int alpha, int beta, Piece cur) { if (depth 0 || st.gameOver) return EvaluateGlobal(st, st.aiColor); // 全局局面分按 AI 视角计 int best (cur st.aiColor) ? -INF : INF; for (int r 0; r BOARD_SIZE; r) for (int c 0; c BOARD_SIZE; c) if (st.grid[r][c] Piece::Empty) { ApplyMove(st, r, c, cur); int val Minimax(st, depth - 1, alpha, beta, (cur Piece::Black) ? Piece::White : Piece::Black); UndoMove(st); if (cur st.aiColor) { best max(best, val); alpha max(alpha, val); } else { best min(best, val); beta min(beta, val); } if (beta alpha) break; } return best; }ApplyMove 和 UndoMove 依赖 game.cpp 里的历史栈这个栈在第 6 章改造悔棋时会直接复用。alpha-beta 剪枝的意义是当对方层已经找到一个足够坏的分支当前层后续分支就不用再看了实测能把两层搜索的耗时压到原来的三分之一。默认版本用 4.2 的贪心方案响应是毫秒级把顶层 GetBestMove 换成“先对空位按 ScoreForMove 排序取前 10 个候选点进 Minimax深度设 2”AI 强度立刻上一个台阶普通 PC 上依然秒出。深度设 3 时全盘遍历会比较吃力建议配合候选点裁剪再用。5. 编译运行与避坑排查让源码在你的机器上真正跑起来代码读完了下一步就是让 exe 弹出来。这部分我踩过不少坑把最顺的两种构建方式和最容易翻车的五个问题一起说清楚。5.1 编译方式与运行步骤IDE 和命令行都试过有 IDE 的情况最简单新建一个空项目把五个文件加进去项目属性不用额外调整直接生成解决方案然后运行。窗口程序不像控制台会先弹出黑框棋盘窗口直接就是主界面。如果看到链接错误多半是字符集或者入口函数问题见下面的坑一。没装 IDE 时用 MinGW 的 g 一条命令也能编。源码目录下有个 build.bat内容等价于g -o gomoku.exe main.cpp board.cpp game.cpp ai.cpp -lgdi32 -luser32 -mwindows参数说明-lgdi32 把 GDI 绘图相关的函数链进来-luser32 提供窗口和消息函数-mwindows 表示这是一个 GUI 程序链接后不附带控制台窗口。如果编译时报错找不到入口 WinMain检查 main.cpp 是否把函数名写错如果报错说 TCHAR 未定义回看第 2 章把 TEXT() 宏用对。注意源码里大量使用 TEXT() 宏走的是窄字符。一旦你手动把字符串改成 L... 宽字符就必须把 WinMain 改成 wWinMain 并且 g 加 -municode两者配套使用混配几乎必然 LNK2019。在 IDE 里项目属性-字符集选“使用 Unicode 字符集”会自动切换入口新手最容易在这里卡住。运行起来后菜单里选 1 是人人对战选 2 是人机对战人机模式下默认 AI 执白、玩家执黑先行。命令行窗口不会出现所有交互都在棋盘窗口上完成。5.2 五个常见问题排查都是实际会遇到的情况坑一编译报 LNK2019无法解析的外部符号。现象编译阶段通过链接阶段报 unresolved external symbol指向 WinMain 或 WndProc。原因字符集设置和源码写法不一致比如源码用宽字符入口IDE 却配置成窄字符也可能是 WinMain 的签名写错。解决先把源码字符串统一成同一种风格再按 5.1 里的搭配改 IDE 设置或编译参数。坑二窗口黑屏棋盘区域是白色或花屏。现象窗口能打开但棋盘区域不显示拖动窗口后出现残缺。原因WM_PAINT 里没写 BeginPaint/EndPaint或者绘制代码写在 WM_CREATE 里窗口显示时内容丢失。解决所有绘制统一移到 DrawBoard由 WM_PAINT 触发落子后用 InvalidateRect 而不是直接调用绘制函数。坑三鼠标点交叉点落子却偏了一格。现象点击某个格点棋子落到了相邻位置越靠棋盘边缘越明显。原因坐标换算没做就近吸附整数除法直接截断小数导致区间偏移。解决按 3.3 的写法先减 OFFSET再加 CELL_SIZE/2最后除 CELL_SIZE并做好边界粗筛。坑四AI 开局乱下第一手经常落在角落。现象人机模式下电脑前几手像随机下棋不占中心。原因空棋盘上所有位置评估分都是 0AI 取到哪个都一样等于随机命中。解决给 (7,7) 中心一个固定开局加分或者前 N 手直接按预设开局表落子之后再启用评估。坑五人人对战下满 225 手程序无反应。现象棋盘满了没有任何结束提示游戏还能继续点。原因只判断了五连赢漏了平局分支。解决ApplyMove 里在 CheckWin 都不通过的情况下判断 moveCount 是否达到 225达到则提示平局并重置对局。6. 二次开发把这份源码改成你想要的样子到这里项目能跑、能看懂接下来是动手改。五子棋这个规模非常适合做改造练习不会大到无从下手又能覆盖“配置、玩法、AI”三个层次。6.1 三个立刻能上手的扩展点最省力的改动是把 common.h 里的常量改了BOARD_SIZE 改成 19 就是十九路棋盘把胜利条件抽出来换成 6 就是六子棋。注意 CheckWin 和 AI 评估里硬编码的 5 也要一并替换我习惯把这些常量收进 GameState 或单独配置结构体避免改一处漏一处。其次是给棋子加贴图把 DrawPiece 从画圆换成贴位图视觉升级很快。最后是调整 AI 难度把 4.2 里防守系数从 1.2 调成 1.5或者把 4.3 的搜索深度从 2 加到 3都是难度档位的天然旋钮。6.2 一个完整改造案例加入悔棋悔棋是五子棋最常用的功能实现思路是拿一个数组记录每步落子位置撤销时从数组尾往回退。// game.cpp struct MoveRecord { int row, col; Piece piece; }; std::vectorMoveRecord history; bool UndoLastMove(GameState st, int steps) { if ((int)history.size() steps) return false; for (int i 0; i steps; i) { MoveRecord m history.back(); history.pop_back(); st.grid[m.row][m.col] Piece::Empty; } st.moveCount - steps; st.gameOver false; InvalidateRect(g_hwnd, NULL, TRUE); // g_hwnd 是 main.cpp 暴露的全局窗口句柄 return true; }history 就是 4.3 节 Minimax 里 UndoMove 的底层数据悔棋和 AI 撤销共用一套栈。ApplyMove 里每成功落子一次就把该步 push 进 history。人机模式下玩家悔棋要回退两步也就是玩家一步加 AI 一步所以调用时 steps 传 2并且要确保栈里有 2 条记录才能执行。入口可以做成菜单项也可以做成键盘快捷键按一下 Z 触发一次。6.3 改完怎么验证改造之后的测试清单可以固定下来边界格点点击、四方向五连判定、平局触发、人机模式下 AI 是否能在玩家连四时封堵、悔棋之后棋盘状态是否完全还原。把这五条跑通改动基本就稳了。从那以后我每次拿到别人的游戏源码第一件事不是看渲染而是把“改状态 → 重绘 → 判胜负”这条链路先在纸上捋一遍再动代码拆这个五子棋也是一样历史栈和恢复现场的思路放到任意小游戏源码里都通用。希望帮到你。本文还有配套的精品资源点击获取