
1. 从黑框到可玩贪吃蛇小项目到底在练什么很多人学完 C 语言的for、while、结构体、指针之后会陷入一种“我好像会了又好像什么都不会”的状态。原因很简单语法是散的项目是整的。贪吃蛇这个项目之所以经典是因为它把三块最核心的能力串成了一条线——Win32 API 控制台绘图、链表管理蛇身、键盘输入响应。你写完之后收获的不是一个游戏而是一套“用 C 语言操作真实系统”的完整手感。先说清楚它是什么。这是一个运行在 Windows 控制台就是那个黑框里的贪吃蛇用 C 语言从零写不依赖任何图形库只调用 Windows 自带的 API。蛇身用链表存每吃一个食物就头插一个新节点地图和蛇用宽字符█、◎、★画出来键盘用GetAsyncKeyState实时读取方向键。适合谁适合刚学完 C 语言基础语法、想找一个能跑起来、能改、能炫的小项目练手的人。也适合想复习链表和指针的人——因为蛇的移动、增长、销毁全是链表操作。我试过把这个项目拆成三个文件Snake.h放声明和结构体Snake.c放实现test.c放入口。这样改起来不痛苦编译也不会一团乱。下面我会把每一步都写成可复制的代码你跟着敲最后能编译出一个真正能玩的版本。整个过程不需要额外装什么只要你有 Windows 和一个能编译 C 的环境VS、Dev-C、MinGW 都行。核心检索词先摆出来C语言贪吃蛇 Win32 API 控制台链表实现。你搜到的很多版本要么只给代码不讲原理要么用数组代替链表。这篇会把链表节点结构体、控制台光标与颜色配置、编译运行与按键测试的验证步骤全部交付让你独立完成可玩版本。在动手之前先理解一个关键点控制台不是画布它是一格一格的字符网格。你看到的“蛇在动”本质是不断擦掉旧位置、在新位置打印字符。所以光标定位函数是整个项目的命脉。理解了这一点后面的SetPos、DrawMap、SnakeMove就都顺了。2. TaoToken 前置给调试和代码补全配一个稳定入口写这种小项目最烦的不是逻辑而是环境。比如你敲到一半某个 API 的参数记不清了或者想快速验证一段链表遍历逻辑对不对。这时候如果有一个能直接对话、能补全、能解释报错的入口效率会高很多。TaoToken 就是干这个的——它提供统一的 API 入口让你在写代码时随时调用模型能力做辅助。先说清楚它是什么、能做什么。TaoToken 是一个大模型 API 聚合服务你可以把它理解成一个“统一的模型调用网关”。你不需要分别去注册一堆平台、记一堆 Key而是用一套 Base URL 和 Key就能调用不同模型。对于写 C 语言小项目的人来说它的价值在于遇到GetAsyncKeyState返回值看不懂、链表头插写反了、wprintf打印乱码可以直接把代码贴进去问比翻文档快。适合谁适合正在做项目、需要边写边查边验证的人。尤其是你写贪吃蛇这种涉及 Win32 API 和指针的项目报错信息往往很隐晦比如local proxy failed、reading choices这类有人帮你翻译一下能省很多时间。前置准备很简单三步第一步拿到 API Key。访问 API Keys 页面https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite登录后创建一个 Key复制保存。注意这个 Key 只显示一次丢了就重新建。第二步确认 Base URL。TaoToken 的 API 地址是https://taotoken.net/api注意这个地址不加 UTM 参数直接用于代码里的base_url配置。第三步选一个模型 ID。比如你想用 Claude 系列做代码解释或者用其他模型做补全在模型对话页面https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite能看到可用模型列表。记下你要用的 Model ID后面配置里要填。如果你打算长期写代码、跑 Agent 类任务可以了解一下 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite它更适合高频调用场景。但就贪吃蛇这个项目而言用按量调用就够了。这里要强调一点TaoToken 不是让你替代编译器也不是让你把整个项目丢给模型生成。它的定位是“辅助”——你写主逻辑遇到卡点用它查。比如链表节点的next指针什么时候置空、SetConsoleCursorPosition的COORD为什么 X 要乘 2这些细节问一下比瞎试快。配置的时候有个坑要注意Base URL 一定要写全包括https://不要只写域名。另外 Key 不要硬编码在提交到 Git 的代码里本地测试可以用环境变量或者单独的配置文件。下面第三节我会给出一份可复制的 JSON 配置片段你直接改 Key 和 Model ID 就能用。3. 可复制配置JSON、TOML 与 settings 片段这一节给你三份可直接复制的配置分别对应不同的使用方式。你不需要全用选一个顺手的就行。重点是路径和字段名要和原文一致不然会报401或者local proxy failed。先给一份通用的 JSON 配置适合大多数支持 OpenAI 兼容接口的客户端{ base_url: https://taotoken.net/api, api_key: sk-你的Key粘贴在这里, model: claude-3-5-sonnet, timeout: 60, max_tokens: 4096 }这份配置里base_url是固定的不要加 UTM 参数api_key换成你在 API Keys 页面创建的那个model换成你在模型对话页面看到的 Model ID。timeout和max_tokens按需调整写代码解释一般 60 秒够用。如果你用的是支持 TOML 的工具比如某些 CLI 客户端可以用这份[provider] name taotoken base_url https://taotoken.net/api api_key sk-你的Key粘贴在这里 [model] id claude-3-5-sonnet max_tokens 4096 temperature 0.3注意 TOML 里字符串要用双引号temperature写代码辅助建议低一点0.2 到 0.4 之间输出更稳定。如果你用的是 VS Code 的 settings.json 配置某个 AI 插件可以这样写{ aiAssistant.provider: openai-compatible, aiAssistant.baseUrl: https://taotoken.net/api, aiAssistant.apiKey: sk-你的Key粘贴在这里, aiAssistant.model: claude-3-5-sonnet, aiAssistant.enableCodeActions: true }这里字段名可能因插件不同而略有差异核心是三件套Base URL Key Model ID。只要这三个对了基本就能通。配置完之后怎么验证不要急着写贪吃蛇先发一个最小请求。用 curl 测试curl https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer sk-你的Key \ -d { model: claude-3-5-sonnet, messages: [{role: user, content: 用一句话解释C语言链表头插法}] }如果返回里有choices字段和正常文本说明配置通了。如果返回401检查 Key 有没有复制错、有没有多余空格。如果返回local proxy failed检查你的网络环境是否直连不要挂任何代理工具。如果返回reading choices相关错误多半是返回体格式和你客户端预期不一致换一个兼容模式试试。还有一个常见问题是 Model ID 写错。比如你写claude-3.5但实际 ID 是claude-3-5-sonnet就会报模型不存在。去模型对话页面确认一下准确的 ID。配置这件事一次弄好后面写代码就顺了。下面进入正题开始写贪吃蛇。4. 从零实现链表节点、控制台绘图与键盘响应这一节是重头戏我会把完整源码拆成可理解的块。你先建三个文件Snake.h、Snake.c、test.c。4.1 头文件与链表节点结构体定义先写Snake.h把所有声明、宏、结构体放进去#ifndef SNAKE_H #define SNAKE_H #include stdio.h #include locale.h #include Windows.h #include stdbool.h #include stdlib.h #include time.h #define KEYPRESS(vk) ((GetAsyncKeyState(vk) 1) ? 1 : 0) typedef struct SnakeNode { int x; int y; struct SnakeNode* next; } SnakeNode, *pSnakeNode; enum DIRECTION { DIR_UP 1, DIR_DOWN, DIR_LEFT, DIR_RIGHT }; enum GameState { SnakeAlive, SnakeHitWall, SnakeHitSelf, SnakeExited }; typedef struct Snake { pSnakeNode pSnake; pSnakeNode pFood; enum DIRECTION dir; enum GameState state; int foodScore; int score; int sleepTime; } Snake, *pSnake; void InitGame(pSnake ps); void SetPos(short x, short y); void PrintWelcomeScreen(); void DrawMap(); void InitSnake(pSnake ps); void InitFood(pSnake ps); void RunGame(pSnake ps); void SnakeMove(pSnake ps); int NextIsFood(pSnakeNode pn, pSnake ps); void EatFood(pSnakeNode pn, pSnake ps); void NoFood(pSnakeNode pn, pSnake ps); void CheckWallCollision(pSnake ps); void CheckSelfCollision(pSnake ps); void EndGame(pSnake ps); #endif这里的关键是SnakeNode结构体它存x、y坐标和next指针。蛇身就是一条单向链表蛇头是pSnake指向的节点。KEYPRESS宏把GetAsyncKeyState的返回值最低位取出来判断按键是否被按过。4.2 控制台光标定位与隐藏在Snake.c里先实现SetPosvoid SetPos(short x, short y) { HANDLE hOutput GetStdHandle(STD_OUTPUT_HANDLE); COORD pos { x, y }; SetConsoleCursorPosition(hOutput, pos); }这个函数是整个绘图的基础。COORD是 Win32 定义的结构体存 X 和 Y。注意控制台里 X 轴一个宽字符占两格所以后面蛇移动时 X 每次加减 2。隐藏光标void HideCursor() { HANDLE hOutput GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_CURSOR_INFO cursorInfo; GetConsoleCursorInfo(hOutput, cursorInfo); cursorInfo.bVisible FALSE; SetConsoleCursorInfo(hOutput, cursorInfo); }4.3 绘制地图与欢迎界面地图用宽字符█画边框。因为宽字符占两格上边界循环 50 次左右边界每行打两个void DrawMap() { int i 0; for (i 0; i 50; i) { wprintf(L%1c, L█); } SetPos(0, 29); for (i 0; i 50; i) { wprintf(L%1c, L█); } for (i 1; i 28; i) { SetPos(0, i); wprintf(L%1c, L█); SetPos(1, i); wprintf(L%1c, L█); } for (i 1; i 28; i) { SetPos(48, i); wprintf(L%1c, L█); SetPos(49, i); wprintf(L%1c, L█); } }欢迎界面void PrintWelcomeScreen() { SetPos(53, 14); wprintf(L欢迎来到贪吃蛇\n); SetPos(51, 20); system(pause); system(cls); SetPos(53, 14); wprintf(L规则简单介绍\n); SetPos(46, 15); wprintf(L(1)按 ↑ ↓ ← → 控制移动方向\n); SetPos(46, 16); wprintf(L(2)按 E 加速 按 Q 减速\n); SetPos(46, 17); wprintf(L(3)加速状态得分更高\n); SetPos(51, 20); system(pause); system(cls); }4.4 初始化蛇与食物蛇初始化用头插法开局 5 个节点向右走void InitSnake(pSnake ps) { int i 0; pSnakeNode pcur NULL; for (i 0; i 5; i) { pcur (pSnakeNode)malloc(sizeof(SnakeNode)); if (pcur NULL) { perror(malloc fail); return; } pcur-next NULL; pcur-x 20 2 * i; pcur-y 15; if (ps-pSnake NULL) { ps-pSnake pcur; } else { pcur-next ps-pSnake; ps-pSnake pcur; } } pcur ps-pSnake; while (pcur) { SetPos(pcur-x, pcur-y); wprintf(L%1c, L◎); pcur pcur-next; } ps-dir DIR_RIGHT; ps-score 0; ps-sleepTime 200; ps-state SnakeAlive; ps-foodScore 1; }食物初始化要随机且不落在蛇身上X 必须是偶数void InitFood(pSnake ps) { int x 0, y 0; again: do { x rand() % 46 2; y rand() % 27 1; } while (x % 2 ! 0); pSnakeNode pcur ps-pSnake; while (pcur) { if (x pcur-x y pcur-y) { goto again; } pcur pcur-next; } pSnakeNode pFood (pSnakeNode)malloc(sizeof(SnakeNode)); if (pFood NULL) { perror(food malloc fail); return; } pFood-x x; pFood-y y; pFood-next NULL; SetPos(x, y); wprintf(L%1c, L★); ps-pFood pFood; }4.5 蛇的移动、吃食物与碰撞检测移动逻辑先创建一个新节点根据方向算出下一个坐标然后判断是不是食物。void SnakeMove(pSnake ps) { pSnakeNode pNext (pSnakeNode)malloc(sizeof(SnakeNode)); if (pNext NULL) { perror(move malloc fail); return; } switch (ps-dir) { case DIR_UP: pNext-x ps-pSnake-x; pNext-y ps-pSnake-y - 1; break; case DIR_DOWN: pNext-x ps-pSnake-x; pNext-y ps-pSnake-y 1; break; case DIR_LEFT: pNext-x ps-pSnake-x - 2; pNext-y ps-pSnake-y; break; case DIR_RIGHT: pNext-x ps-pSnake-x 2; pNext-y ps-pSnake-y; break; } if (NextIsFood(pNext, ps)) { EatFood(pNext, ps); } else { NoFood(pNext, ps); } CheckWallCollision(ps); CheckSelfCollision(ps); }吃食物就是头插然后重新生成食物void EatFood(pSnakeNode pn, pSnake ps) { ps-pFood-next ps-pSnake; ps-pSnake ps-pFood; free(pn); pn NULL; pSnakeNode pcur ps-pSnake; while (pcur) { SetPos(pcur-x, pcur-y); wprintf(L%1c, L◎); pcur pcur-next; } ps-score ps-foodScore; InitFood(ps); }不吃食物就头插新节点删尾节点并用空格覆盖void NoFood(pSnakeNode pn, pSnake ps) { pn-next ps-pSnake; ps-pSnake pn; pSnakeNode pcur ps-pSnake; while (pcur-next-next ! NULL) { SetPos(pcur-x, pcur-y); wprintf(L%1c, L◎); pcur pcur-next; } SetPos(pcur-next-x, pcur-next-y); wprintf(L ); free(pcur-next); pcur-next NULL; }碰撞检测void CheckWallCollision(pSnake ps) { if (ps-pSnake-x 0 || ps-pSnake-x 48 || ps-pSnake-y 0 || ps-pSnake-y 29) { ps-state SnakeHitWall; } } void CheckSelfCollision(pSnake ps) { pSnakeNode pcur ps-pSnake-next; while (pcur) { if (pcur-x ps-pSnake-x pcur-y ps-pSnake-y) { ps-state SnakeHitSelf; break; } pcur pcur-next; } }4.6 主循环与键盘响应void RunGame(pSnake ps) { do { SetPos(72, 12); wprintf(L总分%2d\n, ps-score); SetPos(72, 13); wprintf(L当前食物分数%d\n, ps-foodScore); if (KEYPRESS(VK_UP) ps-dir ! DIR_DOWN) { ps-dir DIR_UP; } else if (KEYPRESS(VK_DOWN) ps-dir ! DIR_UP) { ps-dir DIR_DOWN; } else if (KEYPRESS(VK_LEFT) ps-dir ! DIR_RIGHT) { ps-dir DIR_LEFT; } else if (KEYPRESS(VK_RIGHT) ps-dir ! DIR_LEFT) { ps-dir DIR_RIGHT; } else if (KEYPRESS(VK_ESCAPE)) { ps-state SnakeExited; } else if (KEYPRESS(E)) { if (ps-sleepTime 50) { ps-sleepTime - 30; ps-foodScore 1; } } else if (KEYPRESS(Q)) { if (ps-score 0 ps-foodScore 0) { ps-sleepTime 30; ps-foodScore - 1; } } SnakeMove(ps); Sleep(ps-sleepTime); } while (ps-state SnakeAlive); }4.7 入口与游戏善后test.c#include Snake.h void SnakeTest() { char ch 0; do { system(cls); Snake snake { 0 }; InitGame(snake); RunGame(snake); EndGame(snake); SetPos(66, 10); printf(是否重新开始:是(Y)/否(N)-); while (1) { if (KEYPRESS(Y)) { ch Y; break; } else if (KEYPRESS(N)) { ch N; break; } } } while (ch Y); } int main() { setlocale(LC_ALL, ); srand((unsigned int)time(NULL)); SnakeTest(); return 0; }InitGame和EndGame在Snake.c里补全void InitGame(pSnake ps) { system(mode con cols100 lines30); system(title 贪吃蛇); HideCursor(); PrintWelcomeScreen(); DrawMap(); InitSnake(ps); InitFood(ps); } void EndGame(pSnake ps) { SetPos(72, 9); switch (ps-state) { case SnakeExited: printf(游戏已退出\n); break; case SnakeHitWall: printf(撞墙了游戏结束\n); break; case SnakeHitSelf: printf(撞到自己了游戏结束\n); break; } pSnakeNode pcur ps-pSnake; while (pcur) { pSnakeNode del pcur; pcur pcur-next; free(del); } }5. 编译运行与按键测试验证每一步是否真的通了代码写完了接下来是验证。这一步很多人会跳过结果运行起来一堆问题。我按顺序给你验证步骤。第一步编译。如果你用 gccMinGW命令是gcc test.c Snake.c -o snake.exe -lwinmm注意-lwinmm不是必须的但有些环境需要。如果你用 VS直接新建空项目把三个文件加进去编译即可。如果报wprintf未定义检查有没有#include locale.h和setlocale(LC_ALL, )。第二步运行。双击snake.exe应该看到欢迎界面按任意键继续然后出现地图和蛇。如果地图边框是乱码说明本地化没设置好检查setlocale那行。第三步按键测试。按方向键蛇应该转向。按 E 加速蛇变快分数增加。按 Q 减速。按 ESC 退出。如果按键没反应检查KEYPRESS宏有没有写对GetAsyncKeyState的返回值判断是不是用了 1。第四步吃食物测试。控制蛇去吃★吃到后蛇变长分数增加新食物出现。如果蛇吃到食物后没变长检查EatFood里的头插逻辑。第五步碰撞测试。故意撞墙应该提示“撞墙了”。故意让蛇头碰到自己身体应该提示“撞到自己了”。如果没提示检查CheckWallCollision和CheckSelfCollision的坐标判断。常见报错对照报错/现象原因解决401 UnauthorizedKey 错误或过期重新创建 Key检查有没有空格local proxy failed网络环境有代理干扰关闭代理工具直连reading choices相关返回格式不匹配检查客户端兼容模式地图乱码本地化未设置加setlocale(LC_ALL, )蛇移动但旧位置不擦除NoFood没覆盖空格检查wprintf(L )那行食物生成在蛇身上随机范围没排除蛇身检查InitFood的goto again按键无响应宏写错或句柄问题检查KEYPRESS定义编译报COORD未定义没包含Windows.h检查头文件如果你在配置 TaoToken 时遇到OAuth相关提示说明你用的客户端走了 OAuth 流程而不是 API Key换成 API Key 模式即可。如果出现auth.json相关错误检查你的配置文件路径是否正确字段名是不是api_key而不是apikey。验证通过后你可以试着改一些参数把sleepTime初始值改成 100蛇会更快把地图大小改大需要同步改边界判断把蛇初始长度改成 10看看链表头插有没有问题。这些改动能帮你真正理解代码。6. 继续深入把项目变成你自己的到这里一个可玩的贪吃蛇已经跑起来了。但我想说的是这个项目的价值不在于“完成”而在于“可改”。你可以基于它做很多延伸。比如把链表换成双向链表这样删除尾节点不用遍历。或者加一个“穿墙”模式撞墙后从另一边出来。再或者加一个排行榜把最高分写到文件里。这些改动都不难但能让你对 C 语言和 Win32 API 的理解更深一层。如果你在改的过程中遇到问题比如链表操作写反了、API 参数记不清了可以用 TaoToken 的模型对话快速问一下。它的接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite里面有完整的接口说明。需要长期写代码的话Coding Plan 更适合高频场景。最后给你一个实用技巧写这种控制台项目调试时不要只靠printf。可以用SetPos把中间变量打到屏幕角落比如蛇头坐标、当前方向、链表长度。这样运行起来一眼就能看出哪里不对。我踩过的坑是一开始没隐藏光标光标在地图上乱闪后来加了HideCursor才清爽。代码写完之后建议你手动敲一遍不要直接复制。敲的过程中你会遇到各种小错误比如分号漏了、括号不匹配、wprintf写成printf。这些错误才是真正让你进步的地方。等你把整个项目跑通再回头看链表和 Win32 API会发现它们不再抽象而是你亲手用过的工具。