项目仓库:https://gitcode.com/zyx282024000/yuxuanjiang
技术栈:HTML5 + CSS3 + JavaScript(纯前端,零依赖、零构建)
核心亮点:把 Python requests 流式请求完整转换为浏览器端 fetch + SSE 解析,并用系统提示词工程约束大模型输出结构化 JSON,让 AI 真正"指挥"一场文字版贪吃蛇对局。


一、写在前面:为什么会有这个项目

在人工智能快速渗透到日常开发的今天,越来越多的开发者开始尝试把大语言模型的能力嵌入到网页应用里。常见的做法不外乎两种:要么直接调用一个封装好的 SDK,要么照着官方文档写一个 fetch 请求。但真正把"流式输出"“结构化返回”"游戏实时联动"这三件事串起来做一遍,体会是完全不一样的。

"贪吃蛇文字版"这个项目的出发点很朴素。我们手头有一段运行良好的 Python 参考代码,它通过 requests 库以流式方式调用大模型接口,逐行读取 data: 前缀的数据块,并打印出 choices 字段。现在的问题在于:我们需要在纯浏览器环境里复刻这套能力,并且要让 AI 返回的不再是干巴巴的对话文本,而是可以被 HTML 直接解析、直接驱动游戏的状态数据。

换句话说,这个项目的目标可以拆成四句话:

  1. 做一个能玩、好看、有反馈的文字版贪吃蛇游戏;
  2. 把 Python 的流式大模型调用,逐字逐句地"翻译"成浏览器端的 JavaScript;
  3. 通过系统提示词工程,让 AI 输出结构化的 JSON,而不是自由文本;
  4. 让 AI 的返回值真正驱动游戏——它说往哪转,蛇就往哪转。

本文会沿着这四个目标,把我们踩过的坑、做过的取舍、验证过的方式,完整地记录下来。希望这篇博客能够成为一篇"看得懂、抄得走、改得动"的工程实践记录。


二、需求拆解:什么是"AI 驱动的贪吃蛇文字版"

很多人第一眼看到"贪吃蛇文字版"会以为这是一个用字符画出来的简易小游戏。确实,游戏的本体是用文字字符渲染的:蛇身是 ■,食物是 ●,蛇头是随方向变化的箭头字符(▲▼◀▶)。但这只是外壳,真正有意思的是"AI 驱动"这四个字。

我们把项目的形态定义成 AI 指挥官模式:AI 不仅仅是一个陪你聊天的机器人,它同时还是游戏的大脑。每一次对话,我们都会把当前游戏的状态快照(蛇头的坐标、蛇身的前几节、食物的位置、当前方向、得分、步数)序列化成 JSON,连同玩家的提问一起发给大模型。大模型根据这些信息,返回一个结构化的决策 JSON:

{
  "ai_message": "对玩家的简短中文回复",
  "direction": "up | down | left | right | null"
}

HTML 拿到这个 JSON 之后做两件事:把 ai_message 渲染成一条漂亮的聊天气泡;把 direction 应用成蛇的下一步移动方向。这样一来,玩家可以跟游戏对话:“前方有没有危险?”“帮我规划一条靠近食物的路线”“直接向右走!”,AI 会一边回答一边真实地操控游戏。

在此基础上,我们还提供了三种玩法形态:

模式说明
手动模式方向键 / WASD 直接控制,AI 不介入,传统贪吃蛇体验
单步咨询点击「AI 走一步」,AI 基于当前棋盘做一次方向决策
AI 托管勾选开关后每步由 AI 全权决策,相当于"围观 AI 自己打游戏"

三种模式共用同一套游戏引擎和同一套 AI 接口,只是触发时机不同。这种"一个引擎、多种玩法"的设计,让项目在不大幅增加复杂度的前提下,具备了很强的演示价值和可玩性。


三、技术选型:为什么坚持纯前端零依赖

在开始写代码之前,我们认真权衡过技术栈。最主流的方案当然是引入一个前端框架(Vue / React),再配一个 Node 中间层去转发大模型请求,规避浏览器的跨域限制。但最终我们选择了最朴素、也最硬核的方案:原生 HTML5 + CSS3 + JavaScript,不引入任何第三方库,不搭建任何后端服务,一个静态服务器就能跑起来。

这么做有四个理由:

第一,学习价值最大化。这个项目的灵魂是"转换"——把 Python 的流式请求翻译成 JavaScript。如果依赖了 axios、openai SDK 之类的库,流式解析的细节就被库封装掉了,读者就看不到 data: 前缀、[DONE] 结束符、delta.content 增量这些真正的协议细节。用原生 fetch + ReadableStream,这些细节会原原本本地暴露在我们面前。

第二,部署成本为零。项目的产物就是一堆静态文件:一个 HTML、一份 CSS、四个 JS 文件,外加一个 README。托管到任何静态服务器(GitHub Pages、GitCode Pages、nginx)都能直接工作,不需要考虑 Node 进程、环境变量、进程守护这些问题。

第三,打开即用。得益于 GitCode 的对话接口默认没有对我们测试场景开启强制的同源校验,浏览器直连在实测中顺利通过(这一点的详细验证过程见后文"测试与验证"章节)。即使遇到跨域限制,修复方案也非常明确:加一个轻量反向代理,或者把文件放到同域服务下即可。

第四,符合"文字版"的极客气质。归根结底这是一个字符艺术风格的项目,用最接近底层的方式去实现它,本身就是在向这份极客气质致敬。全屏的等宽字体、霓虹色的棋盘格、快速闪烁的流式文本,再加上终端风格的配色,整个产品的气质是统一的。


四、系统架构设计

整个项目的架构可以分成三个清晰的层次:游戏引擎层、AI 通信层、UI 表现层。三者之间通过约定好的数据契约解耦,谁都不直接依赖谁的内部实现。

┌────────────────────────────────────────────────────┐
│                    UI 表现层 (main.js)              │
│   游戏循环调度  ·  事件绑定  ·  消息渲染  ·  模式管理  │
└───────────────┬─────────────────────┬──────────────┘
                │ 游戏状态             │ 对话/指令
┌───────────────▼──────────┐   ┌──────▼─────────────────┐
│      游戏引擎层(snake.js) │   │      AI 通信层(ai.js)   │
│   24×24 文字棋盘          │   │   fetch + SSE 流式解析   │
│   蛇身/食物/碰撞/得分      │   │   JSON 容错提取          │
└───────────┬──────────────┘   └──────┬─────────────────┘
            │                         │
            └─────────┬───────────────┘
                      ▼
         数据契约(config.js 定义)
     系统提示词(约束 JSON 格式) + 游戏状态快照 JSON
                      ▼
             大模型 API (deepseek-ai/DeepSeek-V4-Flash)

三个文件各司其职,职责边界非常干净:

  • js/config.js:唯一需要改配置的地方。所有 API 参数、棋盘尺寸、速度、系统提示词都在这里集中管理,做到了"改一处、全局生效"。
  • js/snake.js:封装了一个 SnakeGame 类,不依赖任何 DOM,纯粹处理游戏逻辑。这意味着它可以在 Node 环境里直接跑单元测试,我们把"禁止原地掉头"“撞墙判定”"成长逻辑"都验证过。
  • js/ai.js:这是整个项目的技术心脏,完成了从 Python 到 JavaScript 的流式转换,还包含一个 parseAiJson 容错解析函数。
  • js/main.js:胶水层,把上面三个模块串起来,负责游戏循环、事件绑定、聊天渲染、AI 托管调度。

分层带来的直接好处是:如果以后想换一个游戏(比如把贪吃蛇换成本子的迷宫),只要重写 snake.js 并保持 getSnapshot() 和 setDirection() 这两个对外接口不变,AI 这层可以完全不动。这就是契约驱动设计的价值。


五、核心一:文字版贪吃蛇的游戏逻辑实现

游戏逻辑是整个项目的地基。虽然贪吃蛇是"Hello World 级别"的游戏,但要做对细节,依然有不少讲究。

5.1 数据结构

蛇的数据结构选择了最简单的数组 + 叉乘方向向量:

this.snake = [
  { x: 12, y: 12 },   // 蛇头,索引 0
  { x: 11, y: 12 },
  { x: 10, y: 12 },
];
this.dir = { x: 1, y: 0 };      // 当前方向(右)
this.nextDir = { x: 1, y: 0 };  // 下一帧方向(缓冲)

之所以要引入 nextDir 而不是直接改 dir,是为了解决电子游戏里经典的"一帧双操作"问题:如果玩家在 220 毫秒的间隔里快速按了两次方向键(比如先按上再按左),两次事件都发生在同一帧 tick 之前,那么第二次转向必须基于"上一帧的方向"做合法性判断,而不是基于已经被第一次按键改掉的方向。nextDir 作为一个一次性缓冲,保证每帧只消费一个方向变更。

5.2 禁止原地掉头

贪吃蛇的核心约束是"不能掉头撞自己"。实现它的方式是对方向向量做点积判断:

if (nd.x === -this.dir.x && nd.y === -this.dir.y) return; // 反向,忽略

这一步映射到 AI 场景也非常关键。AI 返回 direction 之前,系统提示词会明确告诉它"当前向右时不能返回 left",但 AI 毕竟不是程序,偶尔会犯错。所以游戏引擎层面必须再做一次兜底校验,把非法方向直接过滤掉。这就是"信任但要验证"原则在游戏里的体现。

5.3 边界与自撞判定

一步移动的逻辑顺序是:先算新蛇头坐标 → 撞墙判定 → 撞身判定 → 入头 → 判定吃食物 → 决定是否缩尾。其中有一个容易踩坑的细节:吃到食物时蛇不会缩尾(长度 +1),所以撞身判定要排除掉"即将被移走的尾巴格子"——这一格下一刻就不存在了,撞上去不算死。

const eating = this.food && nh.x === this.food.x && nh.y === this.food.y;
const body = eating ? this.snake : this.snake.slice(0, -1);
if (body.some((s) => s.x === nh.x && s.y === nh.y)) { /* 死亡 */ }

5.4 文字渲染

渲染层没有用 Canvas,而是用 CSS Grid 搭了一个 24×24 的字符网格,每个格子是一个 div。每帧渲染时,遍历所有格子,根据"蛇身映射表"和食物坐标决定该格子显示哪个字符、什么颜色:

  • 空格子显示微弱的 ·,形成点阵背景;
  • 蛇身显示青色 ■,带霓虹发光阴影;
  • 蛇头显示方向箭头字符,且额外加亮;
  • 食物显示红色 ●,做呼吸缩放动画(pulse)。

用 CSS Grid 而不是 Canvas,还有一个隐藏的好处:棋盘可以借助 CSS 变量 --cols 轻松改尺寸,后续想扩展到 30×30 或 16×16 都只需改配置,而且单元格天然支持文字,做"文字版"的主题非常自然。


六、核心二:Python → JavaScript 流式请求转换详解

这一节是全篇文章的技术重心。我们逐行对照参考代码,说明每一步在浏览器世界里对应的实现。

6.1 参考代码做了什么

先看 Python 参考代码的核心链路:

response = requests.post(API_URL, headers=headers, json=payload, stream=True)  # 发起流式请求
for line in response.iter_lines():                                            # 逐行迭代响应体
    if not line.startswith(b"data:"):                                          # 只处理 data: 行
        continue
    if line.strip() == b"data:[DONE]":                                         # 流结束标记
        return
    yield json.loads(line.decode("utf-8").lstrip("data:").rstrip("/n"))        # 逐块产出 JSON

这段代码的本质是:向服务端发起一个 POST 请求,服务端以 SSE(Server-Sent Events) 风格推送数据——每一条消息以 data: 开头,最后以 data: [DONE] 收尾。调用方拿到的是一个"生成器",可以一边接收一边处理,实现真正的打字机效果。

6.2 浏览器端的三个对应物

浏览器端没有 requests,没有 iter_lines(),但有三个天然的对应物:

PythonJavaScript说明
requests.post(..., stream=True)fetch(url, { method: "POST", body })fetch 天生返回可流式读取的 Response
response.iter_lines()response.body.getReader() + TextDecoder手动把字节流解码并切行
yield 生成器回调函数 onDelta / onDone逐块产出改为逐块回调

核心代码如下(节选自 js/ai.js):

const resp = await fetch(APP_CONFIG.API_URL, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${APP_CONFIG.API_KEY}`,
  },
  body: JSON.stringify(payload),
});

const reader = resp.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = "";
let full = "";

while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  buffer += decoder.decode(value, { stream: true });
  const lines = buffer.split("\n");
  buffer = lines.pop(); // 末尾可能是不完整的一行,留到下一轮处理

  for (const line of lines) {
    const trimmed = line.trim();
    if (!trimmed.startsWith("data:")) continue;      // 对应 python: not line.startswith(b"data:")
    const data = trimmed.slice(5).trim();            // 去掉 "data:" 前缀
    if (data === "[DONE]") continue;                 // 对应 python: data:[DONE] 结束标记
    const json = JSON.parse(data);                   // 对应 python: json.loads(...)
    const delta = json.choices?.[0]?.delta?.content; // 对应 python: chunk["choices"]
    if (delta) {
      full += delta;
      if (onDelta) onDelta(delta, full);             // 对应 python: yield
    }
  }
}

有几个工程细节值得单独拎出来讲:

细节一:TextDecoder 的 { stream: true } 参数。 网络分块不稳定,一个 UTF-8 字符可能被拆到两个 chunk 里。如果不加这个参数,半个字符会导致解码乱码。加上 { stream: true } 后,解码器会保留不完整的字符尾部,等下一批字节到达后合并解码,这就保证了中文流式输出不会出现"半个字"。

细节二:半行缓冲。 我们按 \n 切分每一段接收到的文本,但最后一段很可能是不完整的(服务端下一批字节才会把它补齐)。所以 lines.pop() 把最后一段塞回 buffer,留到下一轮循环再处理。这跟 Python 里 iter_lines() 帮我们隐藏的逻辑是完全一致的,只是现在我们自己接管了。

细节三:SSE 数据里并不只有 JSON。 有的服务端会推送行内注释(以 : 开头)、心跳包(空行)、或者 [DONE] 这样的控制标记。所以解析时要做两重防护:先过滤掉不是 data: 开头的行,再用 try/catch 包裹 JSON.parse,解析失败的行静默跳过,绝不中断整个流。

细节四:错误处理不能停。 HTTP 非 200 时,我们读取 resp.text() 拿错误详情并抛出;网络中断时,reader.read() 会 reject,由外层 try/catch 兜住。所有错误都会通过 onError 回调反映到 UI 上——聊天区会出现一条红色错误气泡,AI 状态栏显示"出错了",而且 aiBusy 标志会被复位,保证失败后系统依旧可以继续使用。

6.3 为什么流式比一次性请求更"对味"

很多人会问:直接 await resp.json() 拿整个结果不是更简单吗?确实更简单,但丢失了两个关键体验:

  1. 首字节延迟(TTFB)的感知。大模型的思考时间不短,一次性请求的等待期用户盯着空白界面毫无反馈;流式请求则能在一个 token 生成后立刻显示,虽然总量不变,主观等待时间会短很多。
  2. "正在打字"的拟人感。逐字出现的光标闪烁,配合打字机动画,用户能直观感知"AI 正在工作"。这在聊天场景里是重要的信任信号,也符合本项目"文字版"的复古气质。

所以结论是:流式不是炫技,而是这个产品形态的必备能力。


七、核心三:系统提示词工程——让 AI 学会"说话算话"

拿到流式能力之后,下一个难题是:如何让 AI 稳定地输出 JSON? 这一节我们聊聊提示词工程。

7.1 目标格式定义

为了让 HTML 能够直接解析,我们把 AI 的输出格式定义为一个严格的 JSON 对象:

{
  "ai_message": "对玩家的简短中文回复",
  "direction": "up | down | left | right | null"
}

字段含义:

  • ai_message:给玩家看的中文回复,可以是策略说明、路线规划、或者闲聊;
  • direction:蛇的下一步方向。合法值只有四个方向和 null(null 表示保持当前方向)。

7.2 提示词里的"规则注入"

系统提示词(AI_SYSTEM_PROMPT)是这个产品真正的大脑,我们在里面写清楚了四类信息:

第一类:角色定义。 “你是贪吃蛇文字版游戏中的 AI 指挥官,你会收到游戏状态 JSON 和玩家的提问。”

第二类:环境事实。 “棋盘 24×24,坐标 x∈[0,23],y∈[0,23],撞墙或撞到自己会导致死亡。蛇头是 snake 数组第一项。”

第三类:硬性约束。 “禁止原地掉头,例如当前向右时不能返回 left。优先让蛇靠近食物,同时留出安全空间,避免把路走死。如果当前方向安全且合理,direction 返回 null。”

第四类:输出纪律。 “你必须只输出一个 JSON 对象,不要输出任何解释文字、不要使用 markdown 代码块。” 然后给出两个正反示例。

为什么专门强调"不要 markdown 代码块"?因为很多模型默认会用 json 把 JSON 包起来,这会让前端的 JSON.parse 直接抛错。纪律声明 + 示例,双管齐下,是实测中把"包裹率"压到最低的有效手段。

更进一步,我们在 js/ai.js 里还实现了解析兜底:不管 AI 有没有守纪律,parseAiJson() 都会执行"截取第一个 { 到最后一个 } 之间的内容再 JSON.parse"的容错策略。哪怕 AI 输出了一大段废话夹着 JSON,也能精准提取。这就是工程上常说的:不要把正确性寄托在合作方的自觉上。

7.3 为什么让 AI 返回"决策"而不是"让 AI 玩"

还有一个设计取舍值得记录。我们最开始想过"让 AI 直接背板"——把整盘棋交给 AI 处理,每步都让 AI 自己决定。但很快就放弃了。原因有三个:

  1. Token 成本:step 数一多,上下文会越来越长,每步都全量重发状态很贵;
  2. 延迟不可控:AI 决策的 TTFB 在 1~5 秒量级,与游戏 220ms 的 tick 节奏严重不匹配,会非常卡顿;
  3. 玩法定位:这个项目的定位是"人机协作"——玩家可以说、可以指挥、可以干预,而不是彻底让位。AI 只负责出主意,人是最终决策者,这更符合我们想表达的"AI 辅助创作"理念。

最终采用的方案是"对话 + 可选指令":direction 是空值合法、存在可选。聊天为主、指令为辅,两者都通过同一个 JSON 传递,HTML 各取所需。


八、核心四:UI 表现层的设计细节

如果说 AI 层负责"聪明",那 UI 层负责"好看 + 好用"。这一节讲几个不写代码不知道的设计细节。

8.1 布局:双栏工作台

页面采用左右双栏:左侧是游戏区(含工具栏、棋盘、Game Over 遮罩),右侧是对话区(含聊天记录、输入框、提示文案)。在移动端(宽度 <900px)自动降为单栏纵向排列,棋盘缩小字体保证不溢出。布局上刻意保持"游戏 + 对话"同屏,让"AI 说话 → 蛇真的动了"的因果链一屏可见。

8.2 视觉:霓虹终端风

配色上用了深空蓝底色 + 青绿霓虹 + 红色食物的经典终端配色:

  • 背景两层径向渐变光晕,分别投射在左上(蓝)和右下(青),避免纯黑背景的压抑感;
  • 面板使用半透明毛玻璃(backdrop-filter: blur)+ 细边框,营造层次;
  • 所有文字使用等宽字体栈(JetBrains Mono / Fira Code / Consolas),与"文字版"的主题咬合;
  • 蛇身青绿发光、蛇头亮白、食物红色呼吸,网格点阵有微弱的棋盘纹理。

8.3 交互:打字机 + 光标闪烁

AI 流式回复期间,消息气泡会加一个 cursor 类,使末尾显示一个每秒闪烁的 ▋;流结束后光标消失。这个 3 行 CSS 就能实现的小交互,对"AI 正在实时输出"的感知提升非常明显。

8.4 状态机:清楚的游戏状态

running(运行中)/ paused(暂停)/ over(已结束)三个状态,配合按钮的 disabled 联动,确保任何时刻用户都不可能做出非法操作——比如游戏结束后再点"暂停"是无意义的,按钮直接禁用。Game Over 时弹出遮罩显示最终得分,并有"再来一局"按钮一键复位。

8.5 消息类型体系

聊天区的消息用类名区分样式:user(用户,蓝)、ai(AI,绿框)、system(系统提示,居中灰)、direction(AI 决策指令,蓝色标签)、error(错误红)。每种类型各有各的背景色、边框色和圆角,一眼可辨。这是"让 JSON 更好地显示在 HTML 里"的直译:不只是把文本怼进页面上,而是每一类信息都有专属的视觉语言。


九、完整测试与验证:用 Playwright 跑真实验证

工程写完不算完,验证才是闭环。我们没有停留在"能打开不报错",而是用 Playwright 无头浏览器 + 本地静态服务器做了一套端到端冒烟测试,并且调用了真实的大模型接口。

测试脚本的要点:

  1. 收集页面 console 的 error 事件和 pageerror(JS 异常);
  2. 断言页面标题、棋盘格子数量(576 个)、初始分数;
  3. 点击"开始",等待 1.2 秒,断言步数确实在增加;
  4. 模拟键盘事件(ArrowUp / ArrowDown),再点"暂停",断言步数停止增长;
  5. 点击「AI 走一步」,触发真实的 askAI 流式请求,监听 .msg.ai 消息的出现;
  6. 全程截图留档,供人工审查界面。

一组真实运行的输出摘要:

[1] title: 贪吃蛇文字版 · AI 驱动 | cells: 576 | score: 0
[2] steps after start: 5
[4] pause check: before=8 after=8
[6] ai messages total in chat: 3 | aiStatus: 就绪
[7] last ai text: {"ai_message": "前方是墙,不能继续右移;我选择向下转向,既安全又靠近食物。", "direction": "down"}
[console errors]: (none)

这次验证收获了非常关键的信息:

  • API Key、地址、模型参数全部生效,真实流式回复到达浏览器,说明整条链路是通的;
  • AI 确实返回了合法的 JSON,direction 为 down,与"前方是墙"的判断自洽——说明提示词工程的约束生效了;
  • 无任何 console 报错,说明 reader.read() 循环、buffer 切行、parseAiJson 容错都运行正常;
  • 附带的人工截图审查确认:双栏布局无错位、文字无溢出、元素无重叠,棋盘字符与聊天气泡渲染正确。

这一步让我们有底气说:这不是一个"看起来能跑"的 demo,而是一个"被验证过真能跑"的小产品。


十、遇到的坑与解法:一份避坑清单

项目全程踩过不少坑,这里挑五个有代表性的记下来:

坑 1:一帧双方向按键导致"瞬移掉头"

现象:快速连按两个方向键,蛇会原地反向,撞上自己。

原因:每次按键直接改写 dir,第二次按键判断合法性时用错了基准。

解法:引入 nextDir 缓冲,每帧 tick 开始时才把 nextDir 赋给 dir,一帧只消费一次转向。

坑 2:多字节字符被拆包导致乱码

现象:流式中文输出偶尔出现"�"。

原因:一个中文 UTF-8 字符占 3 个字节,网络 chunk 可能把它劈开。

解法:TextDecoder("utf-8", { stream: true }),让解码器记住半个字符,等下一批字节合并。

坑 3:AI 输出 markdown 包裹的 JSON

现象:JSON.parse 抛 SyntaxError。

原因:模型默认习惯 ``````json ```包裹。

解法:双管齐下——提示词里强制声明"不要使用 markdown 代码块",同时 parseAiJson() 里截取首个 { 与末个 } 再解析。

坑 4:SSE 流中含有非 JSON 行

现象:解析到某一行直接崩。

原因:流里混有注释行、心跳包、[DONE]。

解法:startsWith("data:") 先过滤,try/catch 再兜底,解析失败的行跳过不中断。

坑 5:AI 托管模式疯狂并发请求

现象:AI 回复还没回来,下一轮 tick 又发一个请求,队列堆积。

原因:tick 循环里无条件发请求。

解法:加 aiBusy 忙标志,请求进行中不再发新请求;回复落地后自然进入下一轮决策,实现"AI 步速自适应"。


十一、如何上手体验与二次开发

11.1 本机运行

cd yuxuanjiang
python3 -m http.server 8000
# 或 npx serve . / php -S localhost:8000

浏览器打开 http://localhost:8000 即可。

11.2 修改 AI 配置

所有配置集中在 js/config.js 的 APP_CONFIG:

window.APP_CONFIG = {
  API_URL: "https://api-ai.gitcode.com/v1/chat/completions",
  API_KEY: "你的API Key",
  MODEL: "deepseek-ai/DeepSeek-V4-Flash",
  STREAM: true,
  MAX_TOKENS: 2048,
  TEMPERATURE: 0.6,
  TOP_P: 0.95,
  FREQUENCY_PENALTY: 0,
  THINKING_BUDGET: 2048,
  BOARD_COLS: 24,
  BOARD_ROWS: 24,
  TICK_MS: 220,
};

想换模型、想把蛇跑得更快、想换棋盘尺寸,都是改这里。

11.3 改提示词就能换玩法

这个项目最有趣的一点是玩法由提示词驱动。把 AI_SYSTEM_PROMPT 的主题从"贪吃蛇指挥官"改成"迷宫寻路者",再相应调整状态快照和 direction 的语义,就能复用到一大批"AI 决策游戏"。这也是我们把系统提示词单独抽成常量的原因——提示词就是可配置的代码。

11.4 测试命令

node --check js/config.js && node --check js/snake.js \
  && node --check js/ai.js && node --check js/main.js

十二、项目结构速览

.
├── index.html          # 页面骨架(棋盘 + 聊天双栏)
├── css/
│   └── style.css       # 霓虹终端风样式(含响应式)
├── js/
│   ├── config.js       # ★ AI 配置 + 系统提示词(JSON 格式约束)
│   ├── snake.js        # 贪吃蛇游戏逻辑(纯文字渲染)
│   ├── ai.js           # ★ Python→JS 转换:fetch + SSE 流式解析 + JSON 容错
│   └── main.js         # UI 绑定 / 游戏循环 / AI 调度
└── README.md           # 项目说明文档

共 8 个文件、约 1000 行,一个小而完整的工程样本。


十三、总结与展望

这篇文章从一个简单的需求(把 Python 流式调用转换成 JavaScript)出发,做了一个不小的工程:一个可玩、可看、可对话、可被 AI 指挥的文字版贪吃蛇。

回看整个项目,有三条方法论层面的收获值得带走:

  1. 打好数据契约,分层就自然成立。游戏引擎和 AI 之间只通过 getSnapshot() 和 setDirection() 两个接口通信,其余都互不感知。"契约先行"让每一层都能独立测试、独立替换。

  2. 协议细节亲手走一遍,收益远超想象。如果不亲自实现 SSE 解析,就不会理解 data: 前缀、[DONE] 结束符、半行缓冲这些概念;而一旦理解了,以后接任何流式接口都不会再发怵。

  3. 不要把正确性寄托在 AI 的自觉上。提示词约束了 90%,容错解析兜底了剩下的 10%。真实世界的工程,永远需要这条双保险。

放眼未来,这个项目还有不少值得玩的方向:

  • 增加"AI 回忆":把历史决策回合编码进提示词,让 AI 记住自己走过的路线,策略更连贯;
  • 引入多玩法共用一个大脑:迷宫、扫雷、21 点,同一个"状态 JSON + JSON 返回"契约,理论上可以复制粘贴出整个 AI 决策游戏家族;
  • 加上简单的教学引导:把"AI 走一步"背后发生了什么做成逐步可视化的教学动画,让初学者看到"文本怎么变成方向"。

最后,借用这个项目的一句话收尾:当大模型不再是聊天框里的玩具,而是游戏棋盘上真实的手,技术就有了温度。 希望这篇博客对正在做类似尝试的你有所帮助,也欢迎到仓库里把代码 Clone 下来,亲手按一下那个绿色的「AI 走一步」按钮。

祝你写得开心,玩得开心。


本文首发于 GitCode,项目地址:https://gitcode.com/zyx282024000/yuxuanjiang在这里插入图片描述
在这里插入图片描述

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐