EasyX五子棋实战:C语言图形化开发与状态机设计
简介这是一份面向C语言初学者与图形编程入门者的五子棋实战项目资源基于EasyX图形库实现完整可运行的Windows桌面游戏覆盖人机对战、人人对战等核心模式有效串联C语言基础、内存管理、简单AI算法如Minimax逻辑雏形及图形界面开发能力。压缩包共145个文件含6个核心cpp源码如main.cpp、menu.cpp、pTm.cpp等、5个头文件、3个可执行exe程序、8个mp3音效资源虽部分功能被注释但保留了音频集成结构以及大量编译中间文件obj/tlog/pdb和工程配置文件vcxproj、sln整体大小57.36MB结构完整便于调试、复现与二次开发。已有1687人学习下载资源提供开箱即用的游戏体验与清晰的模块化代码组织读者可深入理解EasyX绘图流程initGraph、drawCircle、putPixel等、游戏状态机设计、双玩家输入响应机制及简易AI落子逻辑实现路径。1. 一个黑框控制台早该退休了用 EasyX 在 Windows 上写出真正能玩的五子棋带星空背景、音效反馈、悔棋逻辑和双人对战状态机你写过 C 语言五子棋吗大概率是那种printf(┌─┬─┬─┐\n);拼出来的字符棋盘靠scanf(%d %d, x, y)输入坐标输错一次就崩赢了只打印一行“YOU WIN”连落子音效都没有。这不是项目是作业验收截图。而这个「星空系列 C语言 五子棋 easyx」项目直接跳过了所有中间态——它用纯 C无 C 类、无 STL EasyX 图形库在 Windows 下跑出一个完整可交互的桌面级五子棋深蓝渐变星空作底鼠标悬停格子高亮、点击落子带粒子扩散动画、胜负判定后弹出半透明提示框、支持 CtrlZ 悔棋、空格键切换先手、ESC 退出甚至每步落子都触发.wav音效项目自带click.wav和win.wav。它不是教学 Demo是能塞进课程设计答辩 PPT、能发给室友联机对战、能让你在简历“项目经验”栏里写“独立实现图形化双人博弈系统”的真实工程。适合刚学完指针和二维数组的大二学生快速上手图形编程也适合想用最小技术栈验证游戏状态机设计的老手——毕竟它没用任何引擎、没封装类、没抽象接口所有逻辑都摊开在main.c里连星空背景都是用putimage()getimage()手搓的位图循环。2. 技术栈与工程结构为什么是 EasyX 而不是 OpenGL 或 SDLEasyX 是 Windows 平台下最轻量、最贴近 C 语言原生手感的图形库。它不依赖 C 运行时头文件仅easyx.h链接时加-leasyx即可所有绘图函数如line(),circle(),setfillcolor()都是纯 C 函数调用参数全是int/COLORREF/IMAGE*没有对象生命周期管理负担。对比 SDL 需要初始化子系统、处理事件循环、管理纹理内存OpenGL 更要写着色器、绑 VAO/VBO——而本项目从创建窗口到绘制星空15 行代码搞定。更重要的是EasyX 对中文路径、中文字符串、Windows 原生音效PlaySound()支持极好避免了跨平台库在 Win10/Win11 上常见的字体乱码或音频设备未就绪问题。这个项目就是为 Windows 桌面场景定制的不谈跨平台不搞抽象层把一件事做透。2.1 项目目录结构与核心文件职责项目解压后共 4 个文件结构极简但分工清晰gobang/ ├── main.c # 全局入口窗口创建、主循环、事件分发、状态调度 ├── game_logic.c # 核心算法胜负判定五连检测、悔棋栈管理、AI 简易逻辑预留接口 ├── render.c # 渲染中枢星空背景生成、棋盘绘制、棋子动画、UI 控件按钮/提示框 ├── res/ # 资源目录 │ ├── click.wav # 落子音效16bit PCM, 22050Hz │ └── win.wav # 胜利音效同格式注意game_logic.c中的 AI 逻辑当前为空实现void ai_move(int board[15][15]) { }但已预留#define ENABLE_AI 0宏开关和ai_move()调用点。若需加入简单 AI只需修改此函数无需动main.c的事件流。2.2 EasyX 初始化与窗口配置关键代码main.c开头的初始化段决定了整个程序的视觉基底和交互响应能力#include easyx.h #include stdio.h #include conio.h // 全局常量定义 #define BOARD_SIZE 15 #define CELL_SIZE 40 #define WINDOW_WIDTH (BOARD_SIZE * CELL_SIZE 100) #define WINDOW_HEIGHT (BOARD_SIZE * CELL_SIZE 120) int main() { // 1. 初始化 EasyX 窗口禁用自动刷新启用双缓冲防闪烁 initgraph(WINDOW_WIDTH, WINDOW_HEIGHT, INIT_RENDERMANUAL); // 2. 设置默认字体微软雅黑加粗20 号抗锯齿 settextstyle(20, 0, _T(Microsoft YaHei)); settextcolor(RGB(220, 220, 220)); // 3. 加载音效资源失败则静音继续 if (!PlaySound(TEXT(res/click.wav), NULL, SND_ASYNC | SND_NODEFAULT)) { printf(Warning: click.wav not found, sound disabled.\n); } // 4. 主循环前预加载星空背景见 3.1 节 IMAGE bg; create_star_background(bg); // 此函数在 render.c 中实现 // ... 后续主循环逻辑 }INIT_RENDERMANUAL是关键它关闭 EasyX 默认的自动刷新改为手动调用flushrender()。这避免了每画一笔就闪一下的“幻灯片效果”让粒子动画、鼠标高亮等连续帧渲染成为可能。settextstyle()中_T(Microsoft YaHei)使用 Unicode 字符串宏确保中文菜单、提示文字正常显示这是 Windows 下 EasyX 中文支持的正确姿势。PlaySound()的SND_ASYNC标志让音效播放不阻塞主线程SND_NODEFAULT防止找不到文件时播放系统默认提示音干扰游戏沉浸感。2.3 游戏状态机设计从“下棋”到“结束”的七种状态本项目用一个enum精确控制所有交互分支比if-else堆砌更易维护、更难出错// game_logic.h 中定义 typedef enum { GAME_STATE_INIT, // 初始状态显示标题等待开始 GAME_STATE_PLAYING, // 对战中双方轮流落子 GAME_STATE_WAIT_AI, // 预留AI 思考中禁用鼠标 GAME_STATE_WIN, // 一方获胜显示结果 GAME_STATE_DRAW, // 和棋棋盘满 GAME_STATE_UNDOING, // 悔棋执行中动画阶段 GAME_STATE_EXITING // 退出确认中 } GameState; // main.c 中状态流转示例 GameState current_state GAME_STATE_INIT; while (current_state ! GAME_STATE_EXITING) { switch (current_state) { case GAME_STATE_INIT: render_title_screen(); // 绘制星空标题操作提示 if (GetAsyncKeyState(VK_SPACE)) { reset_game(); // 清空棋盘重置状态 current_state GAME_STATE_PLAYING; } break; case GAME_STATE_PLAYING: handle_mouse_input(); // 检测鼠标左键点击 if (is_win_or_draw()) { current_state (winner PLAYER_BLACK) ? GAME_STATE_WIN : GAME_STATE_DRAW; } break; case GAME_STATE_WIN: render_victory_popup(); // 半透明胜利框 if (GetAsyncKeyState(VK_RETURN)) { current_state GAME_STATE_INIT; // 回车返回初始页 } break; } flushrender(); // 手动刷新一帧 Sleep(16); // 限帧 60FPS防止 CPU 空转 }状态机强制将“输入处理”、“逻辑计算”、“渲染输出”三者解耦。例如GAME_STATE_WIN下handle_mouse_input()不再被调用彻底杜绝了胜利后还能点棋盘的逻辑漏洞。Sleep(16)是血泪经验不加此句while循环会吃满单核 CPU风扇狂转且鼠标响应延迟。16ms ≈ 62.5 FPS是 Windows 下 EasyX 渲染的舒适阈值。3. 星空背景与棋盘渲染如何用 EasyX 实现动态粒子感星空不是静态图片而是由render.c中create_star_background()动态生成的位图。它模拟了真实的星空层次近景大星随机亮度微小抖动、中景中星固定位置恒定亮度、远景密星大量小点极低 alpha。这种分层设计让背景有深度且不抢棋盘主体。3.1 星空背景生成手写粒子系统不用第三方库// render.c void create_star_background(IMAGE* bg) { // 创建 1920x1080 大小的背景位图适配常见屏幕 initimage(bg, 1920, 1080); // 1. 绘制深蓝渐变底色RGB: 10,15,40 → 5,8,25 for (int y 0; y 1080; y) { int r 10 - (y * 5 / 1080); int g 15 - (y * 7 / 1080); int b 40 - (y * 15 / 1080); setlinecolor(RGB(r, g, b)); line(0, y, 1920, y); } // 2. 添加远景密星1000 个 1px 白点随机坐标alpha30半透明 srand((unsigned int)time(NULL)); for (int i 0; i 1000; i) { int x rand() % 1920; int y rand() % 1080; putpixel(x, y, RGB(220, 220, 220)); // EasyX 中 putpixel 支持 alpha 混合 } // 3. 添加中景中星200 个 2~3px 圆固定坐标查表alpha180 static const struct { int x, y; } mid_stars[] { {320, 180}, {1200, 240}, {850, 420}, /* ... 共 200 个预设坐标 ... */ }; setfillcolor(RGB(255, 255, 220)); for (int i 0; i 200; i) { int size 2 (i % 2); // 交替 2px/3px fillcircle(mid_stars[i].x, mid_stars[i].y, size); } // 4. 添加近景大星50 个 4~6px 圆坐标随机亮度随时间正弦变化 for (int i 0; i 50; i) { int x rand() % 1920; int y rand() % 1080; int base_bright 200 (i * 3) % 55; // 基础亮度差异 int bright base_bright 30 * sin(GetTickCount64() / 500.0 i); setfillcolor(RGB(bright, bright, bright)); fillcircle(x, y, 4 (i % 3)); // 4/5/6px } }关键技巧GetTickCount64()返回毫秒级时间戳除以 500.0 后作为sin()参数产生约 3.14 秒一个周期的呼吸灯效果。每个星星加i相位偏移避免所有星星同步明暗形成自然闪烁。putpixel()在 EasyX 中是底层像素操作比fillcircle()更快适合绘制海量小点。此处用它画远景星性能无压力。3.2 棋盘与棋子绘制坐标映射与抗锯齿处理棋盘不是简单画线而是用line()setlinewidth()构建带阴影的立体感棋子不是实心圆而是用fillcircle()solidcircle()叠加出高光与阴影// render.c 中 draw_board() 函数节选 void draw_board() { setlinecolor(RGB(180, 180, 200)); setlinewidth(2); // 绘制横线15 条 for (int i 0; i BOARD_SIZE; i) { line(50, 50 i * CELL_SIZE, 50 (BOARD_SIZE-1)*CELL_SIZE, 50 i * CELL_SIZE); } // 绘制竖线15 条 for (int i 0; i BOARD_SIZE; i) { line(50 i * CELL_SIZE, 50, 50 i * CELL_SIZE, 50 (BOARD_SIZE-1)*CELL_SIZE); } // 绘制星位天元与四角 setfillcolor(RGB(0, 0, 0)); int star_pos[5][2] {{3,3},{3,11},{11,3},{11,11},{7,7}}; for (int i 0; i 5; i) { fillcircle(50 star_pos[i][0]*CELL_SIZE, 50 star_pos[i][1]*CELL_SIZE, 4); } } // render.c 中 draw_piece() 函数绘制单个棋子含高光 void draw_piece(int x, int y, int player) { // x,y 为棋盘坐标0~14 int screen_x 50 x * CELL_SIZE; int screen_y 50 y * CELL_SIZE; COLORREF color (player PLAYER_BLACK) ? RGB(30,30,30) : RGB(240,240,240); COLORREF highlight (player PLAYER_BLACK) ? RGB(120,120,120) : RGB(255,255,255); // 1. 绘制主体大圆 setfillcolor(color); fillcircle(screen_x, screen_y, 16); // 2. 绘制高光小圆偏右上 setfillcolor(highlight); fillcircle(screen_x 4, screen_y - 4, 6); // 3. 绘制阴影椭圆偏左下半透明 setfillcolor(RGB(0,0,0)); fillellipse(screen_x - 3, screen_y 3, 12, 8); }fillellipse()绘制椭圆阴影模拟真实棋子受光产生的立体感比纯圆更符合人眼认知。坐标映射公式screen_x 50 x * CELL_SIZE是硬编码的布局锚点50 是左/上边距保证棋盘居中且留出 UI 区域。修改CELL_SIZE可缩放整个棋盘无需改其他逻辑。3.3 鼠标悬停高亮与落子动画EasyX 的帧动画实践悬停高亮不是简单改颜色而是用setfillcolor()fillcircle()绘制一个半透明圆环覆盖在目标格子中心// main.c 中 handle_mouse_input() 节选 void handle_mouse_input() { MOUSEMSG m; if (MouseHit()) { m GetMouseMsg(); if (m.uMsg WM_LBUTTONDOWN) { // 计算点击的棋盘坐标整数 int board_x (m.x - 50 CELL_SIZE/2) / CELL_SIZE; int board_y (m.y - 50 CELL_SIZE/2) / CELL_SIZE; if (board_x 0 board_x BOARD_SIZE board_y 0 board_y BOARD_SIZE board[board_x][board_y] EMPTY) { // 播放音效并落子 PlaySound(TEXT(res/click.wav), NULL, SND_ASYNC | SND_NODEFAULT); board[board_x][board_y] current_player; push_undo_stack(board_x, board_y); // 入栈悔棋 // 触发粒子扩散动画见 3.3.1 trigger_particle_effect(board_x, board_y); } } } // 悬停检测实时计算鼠标位置绘制高亮环 int hover_x (m.x - 50 CELL_SIZE/2) / CELL_SIZE; int hover_y (m.y - 50 CELL_SIZE/2) / CELL_SIZE; if (hover_x 0 hover_x BOARD_SIZE hover_y 0 hover_y BOARD_SIZE board[hover_x][hover_y] EMPTY) { int screen_x 50 hover_x * CELL_SIZE; int screen_y 50 hover_y * CELL_SIZE; setfillcolor(RGB(255, 255, 100)); setalpha(120); // 半透明 fillcircle(screen_x, screen_y, 18); setalpha(255); // 恢复不透明 } }3.3.1 粒子扩散动画用数组模拟物理衰减落子后的粒子效果本质是一个Particle结构体数组每帧更新位置和透明度// render.c #define MAX_PARTICLES 60 typedef struct { int x, y; // 起始中心 int vx, vy; // 速度向量随机方向 int life; // 生命周期帧数 int max_life; // 最大生命 } Particle; Particle particles[MAX_PARTICLES]; int particle_count 0; void trigger_particle_effect(int board_x, int board_y) { int screen_x 50 board_x * CELL_SIZE; int screen_y 50 board_y * CELL_SIZE; for (int i 0; i 30; i) { if (particle_count MAX_PARTICLES) { particles[particle_count].x screen_x; particles[particle_count].y screen_y; // 随机速度-3~3 particles[particle_count].vx (rand() % 7) - 3; particles[particle_count].vy (rand() % 7) - 3; particles[particle_count].life 0; particles[particle_count].max_life 20 (rand() % 15); particle_count; } } } void update_and_render_particles() { for (int i 0; i particle_count; i) { if (particles[i].life particles[i].max_life) { particles[i].x particles[i].vx; particles[i].y particles[i].vy; particles[i].life; // 透明度随生命衰减255 → 0 int alpha 255 - (particles[i].life * 255 / particles[i].max_life); setalpha(alpha); setfillcolor(RGB(255, 255, 200)); fillcircle(particles[i].x, particles[i].y, 2); setalpha(255); } } // 清理死亡粒子从后往前遍历避免索引错乱 for (int i particle_count - 1; i 0; i--) { if (particles[i].life particles[i].max_life) { // 将最后一个粒子移到当前位置覆盖死亡粒子 if (i particle_count - 1) { particles[i] particles[particle_count - 1]; } particle_count--; } } }粒子数量30是经验值少于 20 效果单薄多于 50 在低端机上可能卡顿。MAX_PARTICLES 60留出余量。setalpha()是 EasyX 的 alpha 混合开关必须成对使用开→绘图→关否则后续所有绘图都会带透明。4. 核心博弈逻辑五连检测算法与悔棋栈实现胜负判定是五子棋的灵魂。本项目采用“八方向扫描法”而非暴力遍历全盘——它只检查新落子点的八个方向横、竖、斜每个方向最多延伸 4 格时间复杂度 O(1)比 O(n²) 全盘扫描快一个数量级。4.1 五连检测只检新子拒绝全盘扫描// game_logic.c int check_win(int board[BOARD_SIZE][BOARD_SIZE], int x, int y) { // 新子坐标 (x,y)检查其所属玩家 int player board[x][y]; if (player EMPTY) return 0; // 八个方向向量右、下、右下、右上、左、上、左下、左上 int dx[8] {1, 0, 1, 1, -1, 0, -1, -1}; int dy[8] {0, 1, 1, -1, 0, -1, -1, 1}; for (int d 0; d 8; d) { int count 1; // 自身算 1 个 // 正向延伸同一方向 for (int i 1; i 4; i) { int nx x i * dx[d]; int ny y i * dy[d]; if (nx 0 || nx BOARD_SIZE || ny 0 || ny BOARD_SIZE) break; if (board[nx][ny] player) count; else break; } // 反向延伸相反方向 for (int i 1; i 4; i) { int nx x - i * dx[d]; int ny y - i * dy[d]; if (nx 0 || nx BOARD_SIZE || ny 0 || ny BOARD_SIZE) break; if (board[nx][ny] player) count; else break; } if (count 5) return player; // 返回获胜玩家编号 } return 0; // 无人获胜 }关键优化i 4限制最大延伸距离。因为五连需要连续 5 子新子在中间时每边最多需查 2 子在端点时一边查 4 子另一边查 0 子。4覆盖所有情况且避免越界访问。返回值player直接用于状态机判断check_win()不做 UI 提示职责单一。4.2 悔棋栈用结构体数组实现 LIFO支持无限次回退悔棋不是清空棋盘重来而是精确还原上一步状态。本项目用UndoStack结构体记录每次落子的坐标和玩家并用top指针管理// game_logic.h #define MAX_UNDO 50 typedef struct { int x, y; int player; } UndoStep; typedef struct { UndoStep steps[MAX_UNDO]; int top; // 栈顶索引-1 表示空栈 } UndoStack; extern UndoStack undo_stack; // game_logic.c UndoStack undo_stack {.top -1}; void push_undo_stack(int x, int y) { if (undo_stack.top MAX_UNDO - 1) { undo_stack.top; undo_stack.steps[undo_stack.top].x x; undo_stack.steps[undo_stack.top].y y; undo_stack.steps[undo_stack.top].player board[x][y]; } } int pop_undo_stack() { if (undo_stack.top -1) return 0; // 栈空无法悔棋 UndoStep step undo_stack.steps[undo_stack.top]; board[step.x][step.y] EMPTY; // 还原为空 undo_stack.top--; return 1; // 成功悔棋 } // main.c 中绑定 CtrlZ if (GetAsyncKeyState(VK_CONTROL) GetAsyncKeyState(Z)) { if (pop_undo_stack()) { PlaySound(TEXT(res/click.wav), NULL, SND_ASYNC | SND_NODEFAULT); current_state GAME_STATE_UNDOING; // 进入悔棋动画状态 } }MAX_UNDO 50是安全值一局棋最多 225 步但用户极少连续悔 50 步。若需更多改此宏即可无需动逻辑。pop_undo_stack()返回int而非void方便上层判断是否成功如栈空时禁用悔棋音效。4.3 常见问题与避坑指南那些让你调试到凌晨三点的细节现象 1鼠标点击棋盘边缘或空白处程序崩溃或落子到错误位置原因坐标转换公式board_x (m.x - 50 CELL_SIZE/2) / CELL_SIZE中m.x - 50可能为负数C 语言整数除法向零取整导致board_x为负数组越界访问board[-1][y]。解决增加边界检查且用0而非0int board_x (m.x - 50 CELL_SIZE/2) / CELL_SIZE; int board_y (m.y - 50 CELL_SIZE/2) / CELL_SIZE; if (board_x 0 board_x BOARD_SIZE board_y 0 board_y BOARD_SIZE) { /* 安全访问 */ }现象 2胜利提示框弹出后鼠标仍能点击棋盘导致棋盘状态错乱原因GAME_STATE_WIN状态下未屏蔽handle_mouse_input()调用is_win_or_draw()返回真后状态虽已切换但本帧内handle_mouse_input()仍被执行。解决在switch状态分支内用break严格隔离且handle_mouse_input()只在GAME_STATE_PLAYING下调用case GAME_STATE_PLAYING: handle_mouse_input(); // 仅在此状态调用 if (is_win_or_draw()) { current_state ...; } break; // 必须 break防止穿透到下一状态现象 3星空背景在不同分辨率屏幕下拉伸变形星星挤成一团原因create_star_background()生成的IMAGE bg固定为 1920x1080但putimage()绘制时未指定缩放EasyX 默认拉伸填充窗口。解决绘制时用SRCCOPY模式并指定目标区域保持原始比例// 在 render.c 的 render_background() 中 putimage(0, 0, bg, SRCCOPY); // 不拉伸左上角对齐 // 若需居中计算偏移 int offset_x (WINDOW_WIDTH - 1920) / 2; int offset_y (WINDOW_HEIGHT - 1080) / 2; putimage(offset_x, offset_y, bg, SRCCOPY);现象 4编译时报错undefined reference to PlaySound原因未链接 Windows 多媒体库winmm.lib。EasyX 本身不包含音效函数PlaySound是 Windows API。解决在编译命令中添加-lwinmmGCC/MinGW或项目属性中添加winmm.libVisual Studio。EasyX 官网文档明确要求此步骤但新手常忽略。现象 5悔棋后粒子动画残留或胜利音效重复播放原因trigger_particle_effect()和PlaySound()调用未与状态机严格绑定可能在GAME_STATE_WIN下因误触再次触发。解决将音效和动画触发逻辑封装进状态专属函数如on_player_move()和on_game_win()并在状态切换时重置相关标志case GAME_STATE_PLAYING: if (move_made) { on_player_move(x, y); move_made 0; } break; case GAME_STATE_WIN: if (!win_sound_played) { PlaySound(...); win_sound_played 1; } break;5. 二次开发实战从双人对战到简易 AI三步接入你的第一个机器对手现在你已经跑通了完整项目下一步不是“看懂”而是“改出新东西”。本章带你亲手给这个五子棋加上一个能思考的 AI 对手过程不碰框架、不改状态机只动game_logic.c里的两个函数15 分钟内完成。5.1 AI 策略选择为什么用“威胁检测 随机落子”而非 MinimaxMinimax 算法对五子棋来说是杀鸡用牛刀。标准 15×15 棋盘Minimax 搜索深度 3 就会指数级爆炸即使加 Alpha-Beta 剪枝在纯 C 环境下也难做到实时响应。而本项目采用的策略是优先堵住对手的“活四”、“冲四”、“活三”其次占据己方“活三”最后随机选空位。它不求最优但足够聪明——能稳定击败新手且代码不到 100 行完全符合“星空系列”轻量定位。5.2 实现 AI 决策核心scan_threats() 函数详解在game_logic.c中新增scan_threats()函数它扫描全盘为每个空位打分// game_logic.c #define SCORE_LIVE_FOUR 100000 // 活四两边都空必胜 #define SCORE_RUSH_FOUR 10000 // 冲四一边被堵需立刻防守 #define SCORE_LIVE_THREE 5000 // 活三两边空下一步可形成活四 #define SCORE_MY_LIVE_THREE 3000 // 我方活三优先走抢占先机 #define SCORE_RANDOM 10 // 随机位基础分 int scan_threats(int board[BOARD_SIZE][BOARD_SIZE], int* best_x, int* best_y) { int best_score -1; *best_x *best_y -1; // 遍历所有空位 for (int x 0; x BOARD_SIZE; x) { for (int y 0; y BOARD_SIZE; y) { if (board[x][y] ! EMPTY) continue; int score 0; // 检查此空位对 BLACK玩家的威胁若我AI落子于此能否形成活四 board[x][y] PLAYER_WHITE; // 假设 AI 是白子 if (check_win_line(board, x, y, PLAYER_WHITE, LIVE_FOUR)) { score SCORE_LIVE_FOUR; } if (check_win_line(board, x, y, PLAYER_WHITE, LIVE_THREE)) { score SCORE_MY_LIVE_THREE; } board[x][y] EMPTY; // 还原 // 检查此空位对 WHITE人类的威胁若人类落子于此是否会形成活四需立刻堵 board[x][y] PLAYER_BLACK; if (check_win_line(board, x, y, PLAYER_BLACK, LIVE_FOUR)) { score SCORE_LIVE_FOUR * 2; // 堵活四优先级最高 } if (check_win_line(board, x, y, PLAYER_BLACK, RUSH_FOUR)) { score SCORE_RUSH_FOUR; } if (check_win_line(board, x, y, PLAYER_BLACK, LIVE_THREE)) { score SCORE_LIVE_THREE; } board[x][y] EMPTY; // 随机扰动避免 AI 总走同一位置 score rand() % SCORE_RANDOM; if (score best_score) { best_score score; *best_x x; *best_y y; } } } return best_score; }check_win_line()是check_win()的精简版只检查单方向连子数不返回玩家只返回是否满足某模式LIVE_FOUR等。它避免了全八方向扫描提升效率。SCORE_LIVE_FOUR * 2是关键权衡堵对手活四比自己造活四更重要否则 AI 会沉迷进攻而被秒杀。5.3 接入 AI两处修改零风险第一步打开game_logic.c找到空的ai_move()函数填入void ai_move(int board[BOARD_SIZE][BOARD_SIZE]) p a hrefhttps://download.csdn.net/download/gouxiaodan/10008733 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p