简介:这是一份面向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 下跑出一个完整可交互的桌面级五子棋:深蓝渐变星空作底,鼠标悬停格子高亮、点击落子带粒子扩散动画、胜负判定后弹出半透明提示框、支持 Ctrl+Z 悔棋、空格键切换先手、ESC 退出,甚至每步落子都触发.wav音效(项目自带click.wav和win.wav)。它不是教学 Demo,是能塞进课程设计答辩 PPT、能发给室友联机对战、能让你在简历“项目经验”栏里写“独立实现图形化双人博弈系统”的真实工程。适合刚学完指针和二维数组的大二学生快速上手图形编程,也适合想用最小技术栈验证游戏状态机设计的老手——毕竟,它没用任何引擎、没封装类、没抽象接口,所有逻辑都摊开在main.c里,连星空背景都是用putimage()+getimage()手搓的位图循环。
2. 技术栈与工程结构:为什么是 EasyX 而不是 OpenGL 或 SDL?
EasyX 是 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 白点,随机坐标,alpha=30(半透明) 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 圆,固定坐标(查表),alpha=180 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 中绑定 Ctrl+Z 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而非>0: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) { /* 安全访问 */ }
现象 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。
解决:在编译命令中添加-lwinmm(GCC/MinGW)或项目属性中添加winmm.lib(Visual 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 策略选择:为什么用“威胁检测 + 随机落子”而非 Minimax?
Minimax 算法对五子棋来说是杀鸡用牛刀。标准 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 href="https://download.csdn.net/download/gouxiaodan/10008733" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>