简介:这份资源面向C语言初学者与图形编程入门者,提供一套基于EasyX图形库实现的五子棋完整工程,帮助读者在巩固C语言基础的同时,理解图形界面开发、游戏状态管理与简单AI决策的实现思路。压缩包共145个文件,约57.36MB,包含6个cpp源文件与5个头文件承载游戏逻辑与界面绘制,另有jpg图片、mp3与wav音频等素材,以及obj、pdb、exe等编译产物和工程配置,目录结构清晰,便于按模块阅读与二次修改。已有1688人学习下载,适合作为课程设计或练手项目参考。读者可从中获得棋盘绘制、落子交互、人人对战与人机对战流程的完整代码框架,并借此了解Minimax或Alpha-Beta剪枝在五子棋AI中的基本用法,以及网络对战部分的设计思路,为后续扩展联机功能与音效模块打下基础。
1. 五子棋配 EasyX:一个能跑起来的 C 语言图形化练手项目
很多人学完 C 语言指针和数组就卡住了,控制台里敲数字下棋,棋盘靠printf拼字符,落子后满屏刷新,体验一言难尽。这份「C 语言五子棋 EasyX」资源解决的正是这个断层:它把五子棋的胜负判定、落子逻辑和 EasyX 图形库绑在一起,让你在 Windows 窗口里用鼠标点格子下棋,而不是对着黑框敲坐标。适合两类人——刚学完 C 语言基础、想找个完整项目把语法串起来的初学者,以及想快速验证 EasyX 绘图接口、需要一个可运行 Demo 的开发者。它不涉及复杂算法,核心是二维数组、鼠标消息和绘图 API 的组合,拿来练手刚好。
2. 拆开这份源码:棋盘、落子与胜负判定怎么落地
拿到一份 C 语言五子棋源码,别急着编译,先搞清楚它的数据结构和主循环长什么样。这类项目的骨架高度相似:一个二维数组存棋盘状态,一个initgraph开窗口,一个while循环里处理鼠标消息、重绘棋盘。看懂这三块,后面改参数、加功能才有下手的地方。
2.1 棋盘用二维数组存,0/1/2 三态最省心
棋盘本质是一个 15×15 的整型数组,常见约定是 0 表示空位、1 表示黑子、2 表示白子。这个约定比用字符'X'/'O'更省内存,也方便做数值判断。数组下标直接对应格子坐标,落子就是board[row][col] = currentPlayer,判断该位置是否可下就是board[row][col] == 0。
#define BOARD_SIZE 15 // 15 路棋盘,标准五子棋规格 #define EMPTY 0 #define BLACK 1 #define WHITE 2 int board[BOARD_SIZE][BOARD_SIZE] = {0}; // 全局初始化,全空 int currentPlayer = BLACK; // 黑棋先行这里有个容易忽略的点:数组是全局的,初始化为全 0 正好对应 EMPTY,不需要额外写初始化循环。currentPlayer用宏值而不是布尔量,是为了后续扩展悔棋、AI 对战时能直接拿它做分支判断。棋盘尺寸改成 19 路也只需动BOARD_SIZE,但绘图部分的格子间距要同步算,这个后面踩坑章节会细说。
2.2 EasyX 绘图三步走:开窗口、画网格、贴棋子
EasyX 的绘图流程很固定:initgraph创建窗口,setlinecolor/setfillcolor设颜色,line/circle/fillcircle画图形。五子棋的棋盘就是一组横竖线,棋子是实心圆。关键在于把数组下标换算成屏幕像素坐标,公式是x = margin + col * cellSize,其中margin是棋盘左上角留白,cellSize是格子边长。
#include <graphics.h> #define MARGIN 40 // 棋盘四周留白像素 #define CELL 35 // 每个格子边长像素 #define RADIUS 14 // 棋子半径,略小于格子一半 void drawBoard() { setlinecolor(BLACK); for (int i = 0; i < BOARD_SIZE; i++) { // 横线:y 固定,x 从 MARGIN 到 MARGIN+(SIZE-1)*CELL line(MARGIN, MARGIN + i * CELL, MARGIN + (BOARD_SIZE - 1) * CELL, MARGIN + i * CELL); // 竖线:x 固定,y 同理 line(MARGIN + i * CELL, MARGIN, MARGIN + i * CELL, MARGIN + (BOARD_SIZE - 1) * CELL); } }MARGIN和CELL决定了整个棋盘的视觉比例,RADIUS要小于CELL/2,否则相邻棋子会粘连。画完网格后,遍历board数组,遇到非空位就在对应像素坐标画实心圆,黑子用setfillcolor(BLACK),白子用setfillcolor(WHITE)并补一圈黑边,否则白子在白底上看不见。这个「白子描边」是新手最容易漏的细节。
2.3 鼠标消息转格子坐标:整除取整是核心
EasyX 用ExMessage结构体接收鼠标事件,peekmessage非阻塞取消息,适合放在主循环里。拿到鼠标坐标后,要反算它落在哪个格子:col = (msg.x - MARGIN + CELL/2) / CELL,加CELL/2是为了让点击落在格子中心附近也算命中,而不是必须精确点在交叉点上。
ExMessage msg; while (true) { if (peekmessage(&msg, EM_MOUSE)) { if (msg.message == WM_LBUTTONDOWN) { // 反算行列,加 CELL/2 做四舍五入容差 int col = (msg.x - MARGIN + CELL / 2) / CELL; int row = (msg.y - MARGIN + CELL / 2) / CELL; // 边界检查:越界或已有棋子直接忽略 if (row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE && board[row][col] == EMPTY) { board[row][col] = currentPlayer; drawPiece(row, col, currentPlayer); if (checkWin(row, col, currentPlayer)) { // 弹出胜负提示,具体实现见 2.4 } currentPlayer = (currentPlayer == BLACK) ? WHITE : BLACK; } } } }这段代码里peekmessage的第二个参数EM_MOUSE限定只取鼠标消息,避免键盘消息干扰。边界检查必须放在赋值之前,否则点到棋盘外会写越界,程序直接崩。drawPiece是自定义函数,内部把行列换算回像素坐标再fillcircle,和drawBoard的换算逻辑保持一致,建议抽成一个toPixel函数复用,减少两处公式不同步的风险。
2.4 胜负判定:以落子点为中心四方向扫描
五子棋判定不需要扫全盘,只需从刚落的子出发,沿横、竖、两条对角线四个方向数连续同色棋子,任一方向达到 5 个即胜。每个方向要往正反两侧各数一次,合起来算总数。
int countDirection(int row, int col, int dr, int dc, int player) { int count = 0; int r = row + dr, c = col + dc; while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && board[r][c] == player) { count++; r += dr; c += dc; } return count; } int checkWin(int row, int col, int player) { // 四个方向:横、竖、主对角、副对角 int dirs[4][2] = {{0,1}, {1,0}, {1,1}, {1,-1}}; for (int i = 0; i < 4; i++) { int total = 1 + countDirection(row, col, dirs[i][0], dirs[i][1], player) + countDirection(row, col, -dirs[i][0], -dirs[i][1], player); if (total >= 5) return 1; } return 0; }dirs数组把四个方向编码成增量对,正反两侧用取负实现,代码量比写八次循环少一半。total从 1 起算是因为落子点本身算一个。这里用>= 5而不是== 5,是为了兼容长连情况——标准规则里长连是否算胜有争议,但练手项目用>=不会漏判,想严格按禁手规则做再另说。
3. 从源码到窗口:编译环境与运行链路
源码能看懂是一回事,能不能在自己机器上跑起来是另一回事。EasyX 只支持 Windows 平台和特定编译器组合,环境配错会出现一堆链接错误,这是这类项目最高频的翻车点。
3.1 编译器选型:Visual Studio 是省事路线
EasyX 官方对 Visual Studio 的支持最完整,装完 VS 后下载 EasyX 安装包,它会自动把graphics.h和easyx.lib塞进 VS 的 include 和 lib 目录,新建空项目直接#include <graphics.h>就能用。如果用的是 MinGW 或 Code::Blocks,需要手动配置库路径,且部分 EasyX 版本对 MinGW 支持不完整,容易出现undefined reference链接错误。我的建议是:练手阶段别在编译器上折腾,直接用 VS 社区版,把精力留给代码本身。
配置步骤大致是:装 VS 时勾选「使用 C++ 的桌面开发」工作负载;下载对应 VS 版本的 EasyX 库并运行安装程序;新建空项目,添加.c源文件;项目属性里确认字符集设为「使用多字节字符集」,因为 EasyX 的部分字符串接口对 Unicode 支持有差异,多字节字符集能少踩一类编译错误。
3.2 主循环结构:消息处理与重绘的节奏
EasyX 程序的主循环有两种写法:getmessage阻塞式和peekmessage非阻塞式。五子棋这种「等用户点击」的场景,两种都能用,但peekmessage更灵活,方便后续加动画或计时。循环体里通常先处理消息,再按需重绘。注意不要每帧全量重绘棋盘——网格线是静态的,只需在开局画一次,落子时只画那颗新棋子,这样窗口不会闪烁。
int main() { initgraph(640, 640); // 窗口尺寸要能容纳棋盘+留白 drawBoard(); // 静态网格只画一次 ExMessage msg; while (true) { if (peekmessage(&msg, EM_MOUSE) && msg.message == WM_LBUTTONDOWN) { handleClick(msg.x, msg.y); // 落子+判定+切换玩家 } // 如需退出,监听 WM_CLOSE 或按键消息 } closegraph(); return 0; }窗口尺寸 640×640 对应 15 路棋盘、MARGIN=40、CELL=35时,棋盘实际占用40*2 + 14*35 = 570像素,留有余量。如果改棋盘尺寸或格子大小,窗口宽高要重新算,否则棋盘会被裁掉一部分。closegraph在死循环里其实执行不到,正规做法是监听关闭消息后break出循环,这里为了突出主结构做了简化,实际项目里要补上退出逻辑。
3.3 编译报错速查:三类高频错误对照
| 报错现象 | 常见原因 | 处理方式 |
|---|---|---|
graphics.h: No such file | EasyX 未安装或 include 路径未配置 | 重跑 EasyX 安装程序,确认 VS 版本匹配 |
undefined reference to initgraph | 链接阶段找不到 easyx 库 | 检查库目录配置,MinGW 用户改用支持版本 |
| 窗口一闪而过 | initgraph后没有消息循环或立即closegraph | 确认主循环存在且未被提前退出 |
这三类占了新手报错的绝大多数。第一类多半是装错 VS 版本对应的 EasyX 包;第二类在 MinGW 环境下尤其常见,本质是库文件格式不匹配;第三类往往是复制代码时漏了while循环。遇到报错先对照这张表,比盲目搜索快得多。
4. 避坑与排查:五个真实翻车现场
代码跑起来只是开始,改着改着就会遇到各种「明明逻辑对但显示不对」的问题。下面这几条是我拆这类项目时反复遇到的,按现象、原因、解决三段记录,照着排查能省不少时间。
4.1 棋子画偏半格:坐标换算公式不统一
现象:点击交叉点,棋子却画在格子中间或偏移固定距离。原因:绘图时用MARGIN + col * CELL,反算时却用了(x - MARGIN) / CELL没加容差,或者两处MARGIN取值不一致。解决:把「行列转像素」和「像素转行列」各封装成一个函数,全项目只调用这两个函数,杜绝公式散落多处。换算时统一加CELL/2做四舍五入,保证点击容差对称。
4.2 白子看不见:填充色与背景色撞了
现象:黑子正常显示,白子落下去像没落一样。原因:setfillcolor(WHITE)画在白底窗口上,没有描边。解决:画白子时先setlinecolor(BLACK)再fillcircle,EasyX 的fillcircle会同时用当前线色描边;或者先画一个稍大的黑圆再叠一个白圆。这个细节在深色主题下反过来——黑子要描白边,思路一样。
4.3 连点两下同一格:状态没锁住
现象:快速双击同一位置,出现两颗棋子叠在一起或玩家切换错乱。原因:落子后没有立即把board对应位置置为非空,或者消息队列里积压了两次点击。解决:落子赋值和drawPiece之间不要插入任何可能重入的逻辑,赋值必须第一时间完成;必要时在胜负判定后加一个gameOver标志位,为真时直接忽略后续点击。
4.4 改棋盘尺寸后界面错乱:常量没联动
现象:把BOARD_SIZE从 15 改成 19,网格画到窗口外面去了。原因:窗口尺寸、MARGIN、CELL是写死的,没有随棋盘路数联动。解决:要么保持 15 路不动,要么把窗口宽高改成MARGIN*2 + (BOARD_SIZE-1)*CELL的表达式,让编译器帮你算。改尺寸时同步检查RADIUS是否还小于CELL/2。
4.5 胜负判定漏判斜线:方向数组写错
现象:横竖能判赢,斜着连五却不触发。原因:方向数组里副对角线写成{1,1}和{1,-1}时,正反两侧取负后方向重复或遗漏。解决:四个方向固定为{0,1}、{1,0}、{1,1}、{1,-1},正反两侧分别用dr,dc和-dr,-dc,写完后手动在纸上画一遍四个方向的扫描路径验证。这类错误编译器不报,只能靠测试用例覆盖。
5. 进阶改造:让这个 Demo 更像一个完整作品
基础版跑通后,最值得做的三件事是加悔棋、加简单 AI、加胜负提示界面。悔棋用栈存每一步的(row, col, player),撤销时把board对应位置清回 EMPTY 并重绘该区域;简单 AI 可以从「遍历所有空位、对每个位置试下己方棋子并调用checkWin」做起,能挡住对手的四连就算及格,再往上才考虑评分函数。这里给一个悔棋栈的最小实现思路:
typedef struct { int row, col, player; } Move; Move history[BOARD_SIZE * BOARD_SIZE]; int moveCount = 0; // 落子时压栈 history[moveCount++] = (Move){row, col, currentPlayer}; // 悔棋时出栈并清位 if (moveCount > 0) { Move last = history[--moveCount]; board[last.row][last.col] = EMPTY; // 重绘该格子:先用背景色覆盖,再补画网格线 redrawCell(last.row, last.col); currentPlayer = last.player; // 玩家回退到落子前 }redrawCell是难点:EasyX 没有「只重绘局部」的现成接口,常见做法是用背景色fillrectangle覆盖该格子区域,再把穿过该格子的横竖线补画回去。如果嫌麻烦,直接全量重绘棋盘加所有棋子也行,15 路棋盘重绘开销很小,肉眼无感。AI 部分建议先做成「人机对战开关」,用键盘消息切换模式,别一上来就写复杂评估函数,容易劝退。
验证改造是否成功,我习惯用三个固定用例:横线连五、斜线连五、以及「四连被堵后无法成五」的负例。每改一次判定或落子逻辑,这三个用例都手动走一遍。从那以后我每次动胜负判定相关代码,都强制先把这三个局面摆出来跑一遍再继续写别的——五子棋的判定逻辑看着简单,方向数组和边界条件凑在一起,玄学 bug 特别多,靠肉眼 review 不如靠固定用例兜底。希望这份拆解帮到你,把这份源码真正跑成自己手里的东西。
本文还有配套的精品资源,点击获取