尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

EasyX五子棋C语言课程设计:从零实现图形界面与胜负判断

EasyX五子棋C语言课程设计:从零实现图形界面与胜负判断 简介这份资源面向C语言初学者与课程设计需求者提供利用EasyX图形库实现五子棋程序的完整工程。EasyX基于Windows API简化了窗口创建、图形绘制与鼠标事件处理适合用来练习变量、控制结构、函数、二维数组等C语言核心知识。压缩包共50个文件约8.81MB包含cpp源码、vcxproj与sln工程文件、exe可执行程序、jpg背景图以及pdb、ilk、tlog等调试与编译中间文件可直接在Visual Studio中打开运行。目前已有3191人学习下载。通过该工程读者能掌握棋盘绘制、落子判断、五子连线胜负检测、鼠标交互与游戏状态切换等实现思路并参考错误处理与模块化函数设计快速完成一份可演示、可提交的课程设计作品。1. 用 EasyX 写五子棋为什么它是 C 语言课程设计里最稳的选题很多人做 C 语言课程设计第一反应是写个学生成绩管理系统菜单加文件读写交上去能过但答辩时老师问一句「图形界面呢」就哑了。五子棋不一样它天然带界面、带交互、带算法而 EasyX 这个图形库恰好把 Windows 下的绘图和鼠标事件封装成了十几个函数C 语言入门水平就能调起来。你不需要懂 MFC不需要配 Qt装完库直接initgraph开窗口getmouse拿点击坐标剩下的就是画棋盘、落子、判胜负。这篇笔记讲的就是从零把五子棋跑起来包括棋盘坐标怎么算、鼠标怎么映射、胜负怎么判、以及那些第一次写必然翻车的地方。适合正在做课程设计、刚学完数组和函数、想找一个能写进简历的小项目的人。EasyX 的核心价值在于它把「图形」这件事的门槛压到了 C 语言语法范围内。你写circle(x, y, r)就是画圆写setfillcolor(BLACK)就是设填充色没有对象、没有信号槽、没有事件循环的抽象。五子棋的棋盘是 15×15 的交叉点每个点用数组存状态鼠标点击后换算成行列下标判断该位置是否为空然后落子并检查八个方向有没有连成五子。整个程序的结构就是初始化图形窗口、绘制棋盘、循环等待鼠标、处理落子、判断胜负、重绘。这套流程用到的 C 语言知识点覆盖了数组、函数、循环、条件判断、结构体正好是课程设计要考察的范围。热搜里常出现「c语言做游戏代码可复制」「c语言代码爱心动态」这类词说明很多人找的就是能直接跑起来的完整代码。五子棋比爱心动画多了一层逻辑但代码量并不夸张核心逻辑两百行以内能写完。下面从环境配置开始一步步把可运行的程序搭出来。2. 环境配置与最小可运行窗口把 EasyX 装进 Visual Studio2.1 EasyX 的安装与版本选择EasyX 只支持 Visual Studio 系列编译器这一点必须先说清楚。如果你用的是 VS Code 加 MinGWEasyX 用不了因为它的库文件是针对 MSVC 编译的。热搜里「vscode配置c语言环境」很火但做 EasyX 项目时我一般会直接切到 Visual Studio省掉一堆链接错误。社区版 Visual Studio 2019 或 2022 都可以安装时勾选「使用 C 的桌面开发」工作负载确保有 MSVC 编译器和 Windows SDK。EasyX 官网提供安装包运行后会检测你机器上的 VS 版本自动把头文件graphics.h、easyx.h和对应的.lib文件放到 VS 的 include 和 lib 目录。安装完成后新建一个空项目源文件后缀用.cpp而不是.c因为 EasyX 的头文件里有 C 的引用用.c编译会报错。这一点是新手最容易踩的坑明明代码没问题一编译就是几百行错误原因就是文件扩展名。装好之后写一个最小测试#include graphics.h #include conio.h int main() { // 创建 640x480 的窗口 initgraph(640, 480); // 设置背景色为白色 setbkcolor(WHITE); // 用背景色清屏 cleardevice(); // 画一个红色圆圈 setlinecolor(RED); circle(320, 240, 100); // 等待按键防止窗口一闪而过 _getch(); // 关闭图形窗口 closegraph(); return 0; }initgraph的两个参数是窗口宽高单位像素。setbkcolor设背景色后必须调cleardevice才会生效只设不刷是看不到变化的。circle的坐标是圆心_getch来自conio.h作用是暂停程序让窗口停留。如果这段代码能弹出一个白底红圈的窗口说明环境没问题。2.2 坐标系与棋盘尺寸的确定EasyX 的坐标原点是窗口左上角x 向右增大y 向下增大。五子棋棋盘是 15 条横线和 15 条竖线交叉形成 15×15 个落子点。如果每个格子间距设为 40 像素棋盘从边距 30 像素开始画那么第 i 行第 j 列的交叉点坐标是// 棋盘边距和格子间距 const int MARGIN 30; const int GRID 40; const int ROWS 15; const int COLS 15; // 计算第 row 行第 col 列交叉点的屏幕坐标 int boardX(int col) { return MARGIN col * GRID; } int boardY(int row) { return MARGIN row * GRID; }窗口宽度至少是MARGIN * 2 GRID * (COLS - 1)算下来是 30×2 40×14 620高度同理。我一般开 640×640 的窗口右边留一点空间显示当前轮到谁下。这个映射关系是整个程序的基础鼠标坐标转棋盘下标、棋盘下标转屏幕坐标都靠这两个函数。参数改 GRID 就能改棋盘疏密改 MARGIN 就能调边距但要注意窗口尺寸跟着变否则棋盘会被裁掉。棋盘画线用line函数循环 15 次画横线和竖线。为了美观可以在四个星位天元、小目位置画小圆点。这些绘制代码放在一个drawBoard函数里每次重绘时调用。3. 棋盘绘制与鼠标落子从点击坐标到数组下标3.1 用双重循环画棋盘和星位绘制棋盘的逻辑很直接横线从boardX(0)到boardX(14)纵坐标是boardY(i)竖线从boardY(0)到boardY(14)横坐标是boardX(j)。星位在五子棋里通常是 (3,3)、(3,11)、(11,3)、(11,11) 和 (7,7) 五个点画成实心小圆。void drawBoard() { setlinecolor(BLACK); setlinestyle(PS_SOLID, 1); // 画 15 条横线 for (int i 0; i ROWS; i) { line(boardX(0), boardY(i), boardX(COLS - 1), boardY(i)); } // 画 15 条竖线 for (int j 0; j COLS; j) { line(boardX(j), boardY(0), boardX(j), boardY(ROWS - 1)); } // 画五个星位 int stars[5][2] {{3,3},{3,11},{11,3},{11,11},{7,7}}; setfillcolor(BLACK); for (int k 0; k 5; k) { int cx boardX(stars[k][1]); int cy boardY(stars[k][0]); solidcircle(cx, cy, 4); } }solidcircle画实心圆半径 4 像素足够。setlinestyle的第一个参数是线型PS_SOLID是实线第二个参数是线宽。棋盘线宽用 1 就行太粗会显得拥挤。这段代码每次窗口重绘都要调所以不要在里面做耗时操作。3.2 鼠标消息获取与坐标换算EasyX 获取鼠标用getmouse它返回一个MOUSEMSG结构体里面有uMsg消息类型、x和y坐标。我们只关心左键按下即WM_LBUTTONDOWN。拿到坐标后要判断它离哪个交叉点最近并且距离不能超过格子间距的一半否则视为无效点击。// 棋盘状态0 空1 黑子2 白子 int board[ROWS][COLS] {0}; // 将屏幕坐标转换为棋盘行列无效返回 false bool screenToBoard(int mx, int my, int *row, int *col) { // 找最近的列 int c (mx - MARGIN GRID / 2) / GRID; int r (my - MARGIN GRID / 2) / GRID; // 越界检查 if (r 0 || r ROWS || c 0 || c COLS) return false; // 距离检查点击位置离交叉点不能太远 int dx mx - boardX(c); int dy my - boardY(r); if (dx * dx dy * dy (GRID / 2) * (GRID / 2)) return false; *row r; *col c; return true; }这里用了一个四舍五入的技巧(mx - MARGIN GRID/2) / GRID在整数除法下等价于找最近的格子。距离检查用平方比较避免开根号。board数组是全局的初始全零。落子时先判断board[r][c] 0是空位才允许下然后根据当前执子方设为 1 或 2并在对应屏幕位置画实心圆。画棋子用solidcircle黑子填充黑色白子填充白色并加黑色边框否则白子在白底上看不见。半径取GRID / 2 - 2留一点缝隙。落子后切换执子方然后调checkWin判断胜负。3.3 主循环的结构主循环是一个while循环里面不断调getmouse拿消息判断左键按下就处理落子。为了不让 CPU 跑满可以在循环末尾加一个Sleep(10)。窗口关闭的消息是WM_CLOSE检测到就break退出。int main() { initgraph(640, 640); setbkcolor(WHITE); cleardevice(); drawBoard(); int currentPlayer 1; // 1 黑2 白 bool gameOver false; while (!gameOver) { MOUSEMSG msg getmouse(); if (msg.uMsg WM_LBUTTONDOWN) { int r, c; if (screenToBoard(msg.x, msg.y, r, c) board[r][c] 0) { board[r][c] currentPlayer; drawStone(r, c, currentPlayer); if (checkWin(r, c, currentPlayer)) { // 显示胜利信息 gameOver true; } else { currentPlayer 3 - currentPlayer; // 切换 1-2 } } } Sleep(10); } _getch(); closegraph(); return 0; }3 - currentPlayer是切换 1 和 2 的常用写法比 if-else 简洁。drawStone函数根据玩家画不同颜色的圆。这个主循环结构清晰后面加悔棋、加 AI 都在这个框架里扩展。4. 胜负判断与常见翻车点八个方向扫描与边界处理4.1 四方向扫描的胜负算法五子棋判胜负只需要检查刚落下的那颗子。以它为中心沿四个轴线方向横、竖、左斜、右斜分别向两边数同色棋子如果某个方向总数达到或超过 5就赢了。注意是四个方向而不是八个因为每个轴线包含正反两个方向。// 方向向量横、竖、主对角线、副对角线 int dirs[4][2] {{0,1},{1,0},{1,1},{1,-1}}; bool checkWin(int row, int col, int player) { for (int d 0; d 4; d) { int count 1; // 包含当前这颗 // 正方向 for (int step 1; step 5; step) { int r row dirs[d][0] * step; int c col dirs[d][1] * step; if (r 0 || r ROWS || c 0 || c COLS) break; if (board[r][c] ! player) break; count; } // 反方向 for (int step 1; step 5; step) { int r row - dirs[d][0] * step; int c col - dirs[d][1] * step; if (r 0 || r ROWS || c 0 || c COLS) break; if (board[r][c] ! player) break; count; } if (count 5) return true; } return false; }dirs数组存了四个方向的单位向量。正方向和反方向分别累加遇到边界或不同色就停。step 5是因为五子连珠最多只需要看四个方向各四步加上中心正好五颗。这个算法的时间复杂度是常数级每落一子调用一次完全不影响性能。4.2 避坑EasyX 五子棋开发中最容易翻车的五个问题现象一编译报错「无法打开源文件 graphics.h」。原因通常是 EasyX 没装成功或者装到了另一个 VS 版本目录下。解决方法是重新运行 EasyX 安装程序确认它检测到的 VS 路径和你实际用的编译器一致。如果用的是 VS 2022 但 EasyX 只装了 2019 的库手动把graphics.h和easyx.h复制到 2022 的 include 目录.lib文件复制到 lib 目录。现象二程序运行后窗口一闪就没了。原因是main函数执行完直接返回图形窗口跟着关闭。解决是在closegraph之前加_getch()或system(pause)。我一般用_getch因为它不依赖系统命令也不会在窗口里打印「请按任意键继续」这种破坏画面的话。现象三鼠标点击位置和棋子落点对不上。血泪经验多半是窗口有标题栏和边框getmouse返回的坐标是客户区坐标但如果你手动改过窗口样式或者用了initgraph的扩展参数坐标系可能偏移。解决是统一用initgraph(width, height)的默认样式不要加EX_SHOWCONSOLE之类的标志然后在screenToBoard里打印几个测试点的坐标验证映射。现象四白子画出来看不见。白子填充白色背景也是白色当然看不见。解决是画白子时先setfillcolor(WHITE)再fillcircle然后setlinecolor(BLACK)再circle描边。或者干脆把背景设成浅灰色白子就显出来了。现象五连下两个子后程序卡死。检查getmouse是不是在while里没有Sleep导致消息队列被抽干后 CPU 空转。另一个可能是checkWin里的边界判断写反了比如r 0写成了r 0导致数组越界访问程序行为不可预测。用调试器在checkWin入口打断点看row和col的值是否在 0 到 14 之间。注意EasyX 的getmouse是阻塞式还是非阻塞式取决于调用方式在循环里连续调用时它会立即返回当前消息队列的状态没有消息时uMsg为 0。不要用while(getmouse().uMsg ! WM_LBUTTONDOWN)这种写法会死循环。5. 进阶技巧用双缓冲消除闪烁并加一个简单悔棋5.1 双缓冲绘图解决画面闪烁当棋盘上棋子越来越多每次落子后如果直接重绘整个窗口会看到明显的闪烁。原因是 EasyX 默认直接画在屏幕上绘制过程中屏幕被反复刷新。解决办法是用双缓冲先在内存里画好一整幅图再一次性贴到屏幕上。EasyX 提供了BeginBatchDraw和FlushBatchDraw两个函数来实现批量绘制。// 在 initgraph 之后调用开启批量绘制 BeginBatchDraw(); // 每次需要重绘时 cleardevice(); drawBoard(); // 重绘所有已落棋子 for (int i 0; i ROWS; i) for (int j 0; j COLS; j) if (board[i][j] ! 0) drawStone(i, j, board[i][j]); // 把内存中的画面一次性刷到屏幕 FlushBatchDraw();BeginBatchDraw之后所有绘图操作都先存到内存缓冲区FlushBatchDraw才真正显示。这样落子时不会看到棋盘被擦除再重画的过程。记得在程序退出前调EndBatchDraw否则可能残留绘图状态。这个技巧在热搜「c语言做游戏代码可复制」里很少有人提但实际做课程设计时有没有双缓冲直接决定演示效果好不好。5.2 悔棋功能的实现与状态回滚悔棋的本质是撤销上一步落子。用一个栈记录每一步的(row, col, player)悔棋时弹出栈顶把board[row][col]置回 0然后重绘整个棋盘。栈可以用数组加一个top指针实现不需要链表。struct Move { int row, col, player; }; Move history[ROWS * COLS]; int moveCount 0; // 落子时记录 history[moveCount].row r; history[moveCount].col c; history[moveCount].player currentPlayer; moveCount; // 悔棋 void undo() { if (moveCount 0) return; moveCount--; board[history[moveCount].row][history[moveCount].col] 0; // 切换回被撤销的玩家 currentPlayer history[moveCount].player; // 重绘 cleardevice(); drawBoard(); for (int i 0; i ROWS; i) for (int j 0; j COLS; j) if (board[i][j] ! 0) drawStone(i, j, board[i][j]); FlushBatchDraw(); }悔棋的触发可以绑一个键盘按键比如_kbhit()检测到z键就调undo。注意_kbhit和_getch都来自conio.h在 EasyX 窗口里也能用。history数组大小是 15×15225足够存一整局。这个功能在答辩时很加分老师看到你有状态管理意识说明不是抄的代码。5.3 验证程序正确性的几个测试用例写完代码后不要随便下几颗子就交差用几个固定场景验证胜负判断。测试用例一黑棋在 (7,3) 到 (7,7) 连成五子检查checkWin(7,5,1)返回 true。测试用例二白棋在 (3,3)、(4,4)、(5,5)、(6,6)、(7,7) 主对角线连五检查checkWin(5,5,2)返回 true。测试用例三黑棋在 (0,0) 落子检查边界方向不会越界。测试用例四棋盘下满 225 手后没有胜者程序应该提示平局而不是卡死。我一般会在main里加一个隐藏的测试模式用#ifdef TEST包起来编译时定义TEST宏就自动跑这些用例并打印结果。这样改代码后能快速回归不用手动点鼠标。这个习惯是从写单片机 C 语言时养成的热搜里「单片机c语言没有堆栈吗为什么」那种底层思维放到上层应用开发一样管用——先验证核心逻辑再调界面。最后说一个我自己的教训第一次做五子棋时我把棋盘状态和屏幕绘制混在一起落子函数里既改数组又画图结果悔棋时只擦了屏幕没改数组导致棋子「复活」。后来把逻辑和绘制彻底分开board数组是唯一真相屏幕只是它的投影所有操作先改数组再重绘问题就没了。这个习惯希望帮到你。本文还有配套的精品资源点击获取
返回列表