码道 | 趣味猜数字网页游戏开发全记录:从 1~100 的随机数到一个被 AI 点亮的猜谜世界

项目实例图:
在这里插入图片描述
使用工具:
在这里插入图片描述

摘要

猜数字是一个几乎人人都会玩的经典小游戏:系统在心里藏一个数字,玩家一次次地猜,系统一次次地给出"大了"或"小了"的反馈,直到命中答案。这个游戏规则简单到可以用一句话讲完,却蕴含着二分查找、信息论、概率与策略优化这些深刻的思想。本文完整记录了一个趣味猜数字网页游戏从立项、设计、开发到测试、上线的全过程。项目采用 HTML5 + CSS3 + JavaScript 构建前端游戏界面,使用 Python 标准库实现轻量后端,并创造性地接入大模型 API,通过精心设计的系统提示词让模型以严格的 JSON 结构化格式返回提示信息,从而让前端获得强大而稳定的 AI 陪玩体验。文中将详细介绍系统提示词的设计思路、JSON 数据契约的定义、前后端交互协议、路径穿越等安全防护,以及基于 Playwright 的真实浏览器自动化测试与十万次级的逻辑模拟验证。文章既是本次开发的一手记录,也是一份可供复用的"从零搭建带 AI 能力的小型 Web 项目"的工程实践清单。

目录

  • 一、引言:为什么我要做一个小小猜数字游戏
  • 二、项目背景与目标定位
  • 三、需求分析:把一句需求拆成可以交付的规格
  • 四、技术选型:三个原则决定的方案
  • 五、整体架构:一次请求的全过程
  • 六、HTML5 页面结构:从一张白纸到信息层级
  • 七、CSS3 视觉设计:让"猜数字"也有高级感
  • 八、JavaScript 游戏核心逻辑:随机数、判定与状态机
  • 九、AI 提示系统:系统提示词与 JSON 结构化输出
  • 十、Python 后端:标准库实现的轻量服务
  • 十一、密钥管理:把安全隐患挡在仓库之外
  • 十二、开发中踩过的坑与解决方案
  • 十三、测试与验证:不是跑通就行,而是可证伪
  • 十四、运行与部署指南
  • 十五、项目总结与未来展望
  • 结语

一、引言:为什么我要做一个小小猜数字游戏

在第一次接触编程的时候,很多人写过的第一个"有智力"的程序就是猜数字。它不像 Hello World 那样只是把字符串打印到屏幕上,而是包含了随机数生成、用户输入、条件判断、循环、变量维护这样一整套最基本却也最核心的编程概念。一个猜数字程序写得好不好,几乎可以衡量一个人对编程基础概念的掌握程度。

但这一次,我不打算停留在控制台里打印文字的程度。我想要一个真正可以打开浏览器就玩的、界面美观的、拥有完整交互体验的网页游戏。不仅如此,我还想给这个经典游戏加上一点不一样的东西——AI。当玩家猜了很多次都找不到头绪的时候,可以让一个 AI 助手根据玩家此前的猜测记录,给出有针对性的提示、策略建议甚至是一句轻松的调侃。这个想法让原本只有几行代码的"玩具"项目,变成了一个横跨前端、后端、提示工程、自动化测试的完整工程实践。

这篇博客会把整个开发过程原原本本地记录下来,包括我为什么做某些决定、踩过哪些坑、怎样验证自己的代码是对的。我希望能给正在学习 Web 开发、想做一个完整项目练手的同学,或者对"如何让大模型输出结构化数据"好奇的朋友,提供一份真实而完整的参考资料。

二、项目背景与目标定位

2.1 项目背景

这个项目诞生于一次 Web 开发课程的课后实践。课程要求用 HTML5、CSS3 与 JavaScript 三件套完成一个趣味小游戏,重点考察三个能力:页面结构搭建、样式美化、脚本交互逻辑。猜数字游戏天然契合全部要求——它需要一个输入框和一个按钮,需要实时更新反馈文字,需要记录并展示状态,逻辑清晰却不算琐碎,是一个恰到好处的练习载体。

但在和同学讨论的过程中我们发现,绝大多数人提交的都是一个"能用"但"不好玩"的版本:白底黑字,一个输入框,一个按钮,一行提示。功能都能实现,却谈不上任何用户体验。于是我决定在这个基础要求之上做出差异化:把界面做精致,把交互做完整,再把当下最热门的大模型能力接入进来,让这个游戏拥有真正的"智能感"。

2.2 目标定位

项目的定位从一开始就很明确,它不是一个大而全的产品,而是一个小而美的完整工程。我为自己设定了以下几个目标:

第一,功能完整。页面加载后自动生成 1 到 100 之间的随机整数;用户输入猜测数字并提交;程序判断数字大小并给出文字提示;统计猜测次数;提供重置按钮随时重新开局。这是课程要求的底线功能,必须完整覆盖。

第二,体验优秀。界面要有现代感,配色统一,动效自然,在手机上也能够流畅使用。反馈不能只是干巴巴的"大了"或者"小了",要带有情绪和引导性。

第三,智能增强。在游戏进行中,玩家可以主动请求 AI 帮忙。AI 需要结合玩家已经提交的猜测记录,分析出当前可能的目标区间,并给出下一步的试探建议。更重要的是,AI 的返回必须是最适合前端展示的结构化数据——这正是本项目在技术上的核心亮点:通过系统提示词约束大模型输出严格 JSON,让机器和模型之间形成可靠的"数据契约"。

第四,工程严谨。项目必须有清晰的目录结构、完整的文档、合理的密钥管理,以及可重复执行的测试验证。不能是一个只能"跑起来"的脚本集合,而是一个可以给别人阅读、运行、复用的项目。

三、需求分析:把一句需求拆成可以交付的规格

很多新手拿到需求后第一件事就是打开编辑器写代码,这往往会导致做到一半发现遗漏了边界情况,不得不反复返工。正确的做法是先把需求"翻译"成具体的、可验证的功能清单。

3.1 核心玩法需求

我把猜数字的核心玩法拆解成一张最小的功能列表:

  1. 游戏启动:页面加载完成时,系统在闭区间 1 到 100 内均匀随机地生成一个目标整数,该数字只在程序内部保存,不向玩家展示;
  2. 玩家输入:提供一个数字输入框,仅接受 1 到 100 范围内的整数;
  3. 提交判定:玩家提交猜测后,程序将猜测值与目标值比较,产生三种结果之一——相等(猜中)、偏大、偏小;
  4. 结果反馈:根据判定结果给出对应的文字提示,同时在界面中更新当前可猜范围(例如猜了 80 偏大,则新的范围上界变为 79);
  5. 次数统计:每提交一次有效猜测,计数加一,界面上实时展示累计猜测次数;
  6. 游戏结束:猜中目标数字后,游戏状态切换为"已猜中",输入与提交按钮禁用,防止无效操作;
  7. 重新开局:任何时候点击重置按钮,都重新生成随机数、清空历史与计数、恢复初始状态。

3.2 输入校验的边界情况

需求里最容易忽略的就是"不符合规则的输入会怎么处理"。我在开发前就把这些边界情况全部列了出来:

  • 输入为空:点击提交时输入框为空,应提示用户先输入数字;
  • 输入非数字:由于浏览器端使用了 number 类型的输入框,非数字字符很难被输入,但仍需在逻辑层面兜底(例如被其他脚本注入或者输入了科学计数法);
  • 输入越界:小于 1 或大于 100,都应给出明确提示并拒绝本次提交;
  • 输入小数:本游戏规定目标数字是整数,因此小数输入应视为非法;
  • 游戏已结束仍提交:当游戏进入"已猜中"状态后,输入框与按钮均禁用,逻辑层面也做了二次防御。

3.3 非功能性需求

除了功能,我还梳理了非功能性需求:

  • 响应式:在 320 像素宽的手机屏幕和 1920 像素宽的桌面屏幕上都有良好的可读性与可用性;
  • 健壮性:前端对后端异常(后端未启动、接口超时、返回非法 JSON)都有兜底提示,不会让页面崩溃;
  • 安全:后端静态文件服务必须防止路径穿越攻击;API 密钥不能硬编码进公开仓库;
  • 可测试:游戏核心逻辑(随机数、比较、范围收缩)可以脱离 DOM 独立验证。

3.4 验收标准

我把验收标准写成了一段可以执行的检查清单:打开页面能看到完整界面;连玩十局,确认随机数每次都不同;故意输入 0、101、小数、空字符串,确认都被正确拦截;二分法策略连续游玩五千局,确认每局都能在有限步骤内猜中;点击 AI 提示,确认返回的数据能结构化展示,且不会直接暴露目标数字。

这些清单在后面全部转化成了自动化或半自动化的验证手段,极大减少了"感觉没问题"带来的不确定性。

四、技术选型:三个原则决定的方案

4.1 原则一:主力技术贴合课程要求

前端采用 HTML5 + CSS3 + JavaScript,这是课程指定的技术栈,也是 Web 开发最基础的三件套。我刻意没有引入 React、Vue 等框架——对于这样一个规模的小项目,原生实现更加轻量,也能更清晰地展示 DOM 操作与事件处理的原理。任何对框架有了解的同学,读起这份代码都会毫无障碍。

4.2 原则二:后端零第三方依赖

AI 提示功能需要一个后端来承接大模型的调用。为什么不能纯前端直接请求大模型?因为大模型的 API 需要一个密钥,而密钥一旦放进前端代码,就等于把密钥公开给了全世界;跨域(CORS)限制也要求有服务端代理。所以一个轻量后端是必需的。

后端语言我在 Python 和 Node.js 之间做了选择。考虑到 Python 的易读性以及大多数课程环境自带 Python,最终选择 Python。为了让项目在任何机器上都能"一键运行"而不需要 pip install 一堆依赖,我决定只用 Python 标准库:用 http.server 搭建 HTTP 服务,用 urllib.request 发起大模型请求,用 configparser 读取配置文件。整个后端零第三方依赖,这是我在技术选型上最满意的一个决定。

4.3 原则三:模型能力通过提示词工程获取

大模型部分选用了 DeepSeek-V4-Flash。选择它的原因有三:一是它足够聪明,对提示词的遵循程度高,适合做结构化输出;二是它的响应速度在流式和非流式场景下都足够快,适合游戏这种对实时性有一定要求的场景;三是价格友好,适合个人项目和教学场景反复调用。

真正考验功力的是系统提示词的设计。我需要在提示词里把游戏规则、历史记录格式、输出格式约束全部讲清楚,让模型返回一个字段固定的 JSON 对象。这部分内容我将在后文用一整章详细展开,因为它是整个项目中"技术含量"最高也最容易被忽略的部分。

五、整体架构:一次请求的全过程

在写任何代码之前,我画了一张非常简单的架构图,用箭头描述了完整的信息流。这里我用文字把它复述出来。

5.1 游戏主体(纯前端本地闭环)

游戏的核心玩法完全不依赖后端。页面加载时,script.js 通过 Math.floor(Math.random() * 100) + 1 在本地生成目标数字;玩家点击按钮后,前端脚本完成比较、反馈、范围收缩、计数与历史记录的一切逻辑。这意味着即使后端完全宕机,游戏本身依然可以完整游玩。这是一个非常关键的设计决策:AI 是"锦上添花",游戏的可用性绝不能建立在它之上。

5.2 AI 提示(前端 → 后端 → 大模型 → 后端 → 前端)

当玩家点击"AI 提示"按钮时,完整链路如下:

  1. 前端将玩家的历史猜测记录(每个元素包含猜的数字和结果是偏大还是偏小)组装成 JSON,通过 fetch 以 POST 方式发送到后端接口 /api/hint
  2. 后端读取请求体,校验字段类型,把历史记录翻译成自然语言,连同系统提示词一起拼装成大模型 API 的请求负载;
  3. 后端通过 urllib.request 调用大模型接口,等待模型返回文本;
  4. 后端用专门编写的 extract_json() 函数,把模型返回的文本清洗并解析成标准 JSON;
  5. 后端把 JSON 组装成响应体:成功时返回 {"ok": true, "hint": ..., "advice": ..., "tone": ...},失败时返回 {"ok": false, "error": ...}
  6. 前端收到响应,将三个字段分别渲染到页面上对应区域。

5.3 一个诚实的细节:目标数字永不出前端

我在这里做了一个设计上的坚持:前端在请求 AI 提示时,绝对不会把目标数字放进请求体。后端和模型只能看到"第几次猜了什么、结果是偏大还是偏小"。这样 AI 的提示才是真正基于推理的提示,而不是"作弊"式的剧透。这个细节也保证了公平性——模型必须真的做逻辑推理才能给出靠谱建议。

六、HTML5 页面结构:从一张白纸到信息层级

6.1 语义化标签的选择

页面结构使用语义化标签组织。整体是一个 <main class="container"> 容器,内部依次是 <header>(标题与副标题)、<section class="card game-card">(核心游戏卡片)和 <footer>(页脚说明)。

游戏卡片内部的信息层级按照"先看什么、再操作什么"的顺序排列:

  • 最上方是一个范围条,用醒目的方式展示当前可猜范围,让玩家时刻清楚目标所在的区间;
  • 接下来是输入区,输入框加"猜一猜"主按钮,是玩家使用频率最高的区域;
  • 输入区下方是反馈区,使用 aria-live="polite" 属性,屏幕阅读器会在此内容变化时自动播报,照顾到无障碍需求;
  • 反馈区之下是统计区,用两块卡片分别展示"已猜次数"与"游戏状态";
  • 随后是 AI 功能区:一个紫色描边的"AI 提示"按钮与一段解释性文字;
  • AI 提示面板默认隐藏,玩家点击后通过 class 变化实现淡入显示,面板内又细分为三个字段区:提示语、策略建议、趣味点评;
  • 再往下是猜测记录区,用胶囊样式的标签逐条展示每一次猜测;
  • 最底部是通栏的"重新开局"按钮。

6.2 交互状态的表示

HTML 层面还处理了三种状态区分:hidden 属性控制 AI 面板的显隐;disabled 属性控制输入框与按钮在游戏结束后的可用性;class 名称的增删则驱动 CSS 中的状态样式。这些状态没有混杂业务逻辑,而是纯粹由 JavaScript 在合适的时机切换,保持了"结构、样式、行为"三者的分离。

6.3 一个容易被忽略的小改进

浏览器在没有显式设置网站图标时,会自动向服务器的 /favicon.ico 发起一次请求,如果服务端没有这个文件就会返回 404,并且在前端控制台打印一条红色报错——这会让初学调试的人误以为自己的代码有问题,其实只是少了个图标。我在 <head> 里加了一行 <link rel="icon" href="data:,">,用内联数据告诉浏览器"没有图标,别再请求了",一次性消除了这个噪音。

七、CSS3 视觉设计:让"猜数字"也有高级感

如果说 HTML 决定了页面"有什么",那么 CSS 决定了页面"好不好看"。我在设计上花了相当多的时间,因为它直接决定玩家对这个项目的第一印象。

7.1 配色系统

我通过 CSS 自定义属性(变量)建立了整个页面的设计令牌,色彩语义明确:

  • 主色是一组金色渐变(#ffd166#f57c00),用在标题与主按钮上,象征"奖励"与"行动";
  • 辅助色是紫色系(#b388ff#7131d9),专属 AI 功能,让 AI 成为视觉上的"特殊存在";
  • 反馈色采用红、蓝、黄、绿四种语义色,分别对应"偏大"、“偏小”、“输入警告”、“猜中”,符合直觉且足够区分;
  • 背景是深蓝紫黑夜渐变色,通过 radial-gradient 叠加三团彩色光斑,配合 CSS 位移动画形成缓慢漂浮的氛围光效。

这种"语义即颜色"的做法让玩家不需要读文字,光看颜色就能理解游戏状态。

7.2 玻璃拟态卡片

核心卡片使用了当下流行的"玻璃拟态"(Glassmorphism)风格:半透明白色背景叠加 backdrop-filter: blur(16px) 实现毛玻璃效果,辅以细边框和深色投影。在深色背景的衬托下,卡片像一块悬浮的磨砂亚克力板,层次感立刻出来了。考虑到部分浏览器对 backdrop-filter 的兼容性问题,我还加了 -webkit- 前缀。

7.3 动效的克制与恰当

动效我严格遵守"克制的恰当"原则,不为炫技而加:

  • 猜大或猜小时,反馈条做一次幅度很小的水平抖动(shake),模拟"鼓了鼓劲"的感觉;
  • 猜中时,反馈条做一次缩放弹跳(pop),卡片边框同步变为绿色并泛起绿色光晕,形成成就时刻;
  • AI 提示面板淡入并伴有轻微上移,避免生硬的突然出现;
  • 背景光斑以不同的周期缓慢浮动,让静止的页面"活"起来。

所有动效都用 CSS 过渡或关键帧完成,不阻塞主线程,也没有任何繁复的动画依赖。

7.4 响应式适配

使用媒体查询,在屏幕宽度小于 520 像素时:整体内边距收紧、标题缩小、输入区与按钮从水平排列改为垂直堆叠、AI 操作区改为纵向排列,确保单手操作友好。由于布局主体是居中的单列卡片,移动端适配非常轻松。

八、JavaScript 游戏核心逻辑:随机数、判定与状态机

8.1 模块的组织方式

我采用了一个立即执行函数包裹全部逻辑,避免污染全局命名空间,同时利用闭包保住"目标数字"这个只有游戏逻辑可见的秘密。全局只暴露必要的 DOM 相关事件监听,读取代码的路径非常清晰。

8.2 随机数与状态字段

目标数字生成如下:

target = Math.floor(Math.random() * (MAX - MIN + 1)) + MIN;

其中 MIN = 1MAX = 100。这个公式是生成闭区间整数随机数的标准写法:先乘区间长度加一,取整后再加最小值。我曾经见过有人写成 Math.ceil(Math.random() * 100),那样会导致 0 有极小概率出现而边界分布不均;也有人写成 Math.round(Math.random() * 100),那会导致首尾两个数字的出现概率只有中间数字的一半。用 floor 配合 (MAX - MIN + 1) 才是概率完全均匀的正确写法。

游戏中维护的关键状态有:目标数字(闭包内变量)、已猜次数、当前可猜范围的下界与上界、一局是否仍在进行、历史猜测数组。

8.3 判定与反馈

每次有效提交,程序执行比较:

if (value === target) { /* 猜中分支 */ }
else if (value > target) { /* 偏大分支:上界收缩到 value - 1 */ }
else { /* 偏小分支:下界扩张到 value + 1 */ }

这里有一个细节值得强调:范围收缩使用了 Math.min(max, value - 1) 而不是直接把 max 设为 value - 1。原因在于玩家的输入乱序可能造成收缩目标既大于输入值的情况——例如玩家先猜 80 偏大(上界变 79),再猜 90,虽然 90 依然偏大,但如果直接覆盖上界,反而把范围从 79 扩大到了 89。用 Math.min 保证上界严格不降,是防止范围逆向扩张的保险丝。下界同理使用 Math.max

8.4 猜中的收尾流程

猜中时依次完成:更新反馈文案(包含正确答案与总次数)→ 切换状态文字为"已猜中"→ 禁用输入与按钮 → 卡片加 win 类触发特效 → 更新历史记录。所有这些操作完成后,游戏进入"终端"状态,等待玩家点击重新开局。

8.5 重置逻辑

重新开局本质上就是重新走一遍初始化:重新生成目标、清空计数与历史、复位范围、解锁控件、清空所有面板元素并让焦点回到输入框。为了确保任何状态下重置都能得到一致的初始画面,所有需要初始化的内容全部收拢在 startGame() 一个函数里——这是"单一事实来源"在状态管理上的一次微小实践。

8.6 历史记录的渲染

历史记录用数组保存,每次猜测后整体重绘。每次记录是一个对象 { guess: 数字, result: "high" | "low" | "correct" },渲染成带语义颜色的胶囊标签:“第 N 次 50 偏大”。这个数据结构设计得恰好与提交给 AI 的历史格式一致,后面实现 AI 提示时几乎零改造。

九、AI 提示系统:系统提示词与 JSON 结构化输出

这显然是整个项目中技术含量最高、也最能体现"工程感"的部分。我一章的主题是系统提示词与 JSON 数据契约,因为它回答了一个许多人都关心的问题:如何让大模型的输出可以被程序稳定地消费。

9.1 问题:自由文本无法被程序稳定消费

如果直接要求大模型"给个提示",它可能会返回这样一段话:“根据你的猜测,我觉得目标数字可能在 64 到 74 之间,建议你猜 69,60% 的概率……”——这段话人类读起来完全没问题,但程序无法可靠地从里面提取出"提示语、建议数字、点评"这三个结构化字段。字符串匹配在这个场景下非常脆弱:模型每次表述都不一样,一个句号、一个逗号的差异都可能导致解析失败。

要让程序稳定消费模型输出,就必须让模型输出"程序的形状"——也就是结构化数据。JSON 是 Web 世界里最自然的选择。

9.2 系统提示词的三层设计

我在 app.py 中精心编写了系统提示词,它由三个层次构成:

第一层是角色与游戏规则设定。提示词开篇声明"你是趣味猜数字网页游戏的 AI 游戏助手",随后完整交代游戏规则:系统会在 1 到 100 之间秘密生成一个目标数字,玩家反复猜测,程序反馈偏大或偏小,直到猜中为止。这一段的目的,是让模型对任务本身形成正确的认知框架。

第二层是历史数据格式说明。提示词告诉模型:玩家历史猜测记录以 JSON 数组传入,每个元素形如 {"guess": 数字, "result": "high"|"low"|"correct"},并逐一解释三个结果的语义——high 表示玩家猜大了,目标数字比该猜测更小;low 表示猜小了,目标数字比该猜测更大;correct 表示已经猜中。同时明确一条红线:绝不可以在提示中直接说出目标数字,只能基于边界给出建议区间、试探点或策略。

第三层是输出格式的硬性约束,这是最关键的一层。提示词要求模型:只输出一个 JSON 对象,严禁输出 JSON 之外的任何文字、解释或 Markdown 代码块标记;对象必须且只能包含 hintadvicetone 三个字段,并对每个字段的内容风格做了具体规定(hint 一句有引导性的话,advice 明确建议试探哪个区间或数字,tone 是最多 20 字的趣味点评);如果玩家还没有任何猜测记录,就给出一般性的开局策略,例如"对半猜"的思想。

9.3 用户消息的构造

除了系统提示词,用户消息也需要精心构造。后端把 JSON 类型的历史记录翻译成模型更易理解的逐条自然语言并编号:“第 1 次:猜 50,反馈:偏小(目标更大)”,最后一并附上已猜次数和一句"请输出符合格式的 JSON"。把结构化历史翻译成自然语言,比直接把 JSON 数组塞给模型更能让模型专注于思考,实践中发现这样得到的答案质量也更高。

9.4 模型并没有"保证"返回 JSON

很多人会有个误解,以为提示词写了"必须返回 JSON",模型就一定会返回 JSON。事实并非如此。模型偶尔仍会:在 JSON 前后加上一句"好的,这是你需要的提示:";把 JSON 包在 Markdown 代码块里;或者少返回一个字段。因此后端必须有预处理兜底。

我实现了一个 extract_json() 函数,处理顺序是:去除首尾空白 → 剥离 Markdown 代码块围栏(识别以 ```开头和结尾的行并剔除)→ 尝试直接 json.loads → 失败则扫描第一个 { 与最后一个 },截取中间片段再次解析 → 如果仍失败则抛出明确的错误信息。解析成功后,还会对缺失字段填充默认文案,例如 advice 为空时兜底为"结合上下边界,优先试探区间中点的数字"。这一整套"提示词约束 + 解析兜底 + 字段兜底"的组合,让接口的可靠性达到了生产可用的水平,而不是碰运气。

9.5 实测效果

在真实调用测试中,我给模型送去了三组历史:猜 50 偏小、猜 80 偏大、猜 65 偏小。逻辑上目标范围被锁定在 66 到 79 之间。模型返回的结构化结果是:hint 为"在 66 到 79 之间寻找答案,你离目标越来越近了",advice 为"下次优先尝试 73,正好在 66 到 79 的中间位置",tone 为"节奏不错,再接再厉!"。建议数字 73 恰好是区间的中点,这是一个完全基于推理的正确回答。它对"区间中点"有直觉,说明提示词让它理解了二分思想。

十、Python 后端:标准库实现的轻量服务

10.1 服务能力的划分

app.py 只做两件事:提供静态文件与提供 AI 提示接口。

静态文件服务负责返回 index.htmlcss/style.cssjs/script.js。代码从 URL 路径映射到磁盘路径时,只允许以 /css//js/ 开头的相对路径,并对最终路径做 os.path.normpath 规整,且强制校验解析后的绝对路径必须以项目根目录开头,否则直接返回 404。这样即使客户端传入 /../app.py 之类带有路径穿越语义的 URL,也无法读取到项目根目录之外的任何文件。我在自动化测试里专门构造了试图访问 /etc/passwd 的请求,确认返回 404。

AI 提示接口是 POST /api/hint。后端先读取请求体并做类型校验:history 必须是数组,否则按空数组处理;guess_count 缺省时使用历史记录长度。随后调用大模型,并对调用过程做了极简但必要的异常边界:任何一步出错(未配置密钥、网络超时、模型返回非法 JSON),都会被捕获并转换为 {"ok": false, "error": "..."} 响应,绝不把异常堆栈直接抛给前端。

10.2 为什么用 ThreadingHTTPServer

Python 的 http.serverHTTPServerThreadingHTTPServer 两种实现。前者是单线程的,同一时刻只能处理一个请求,一旦一个 AI 请求因为模型响应慢而阻塞 30 秒,另一个玩家的游戏请求就会被堵住,页面转圈。后者为每一个连接分配一个工作线程,并发场景下体验平滑。测试环境里可能只有我一个人在用,但"正确的东西和勉强能用"的差别,就体现在这些细节里。

10.3 中文乱码的三道防线

中文内容在这条链路里经过多次编码转换,每一环都可能出现乱码。我做了三道防线:

第一,所有响应头统一携带 charset=utf-8,无论是 HTML、CSS、JS 还是 JSON;
第二,读取本地文件时使用二进制模式读取,再由 http.server 按响应头的编码声明交给浏览器,避免 Python 在文本模式下对 UTF-8 内容做平台相关的编码猜测;
第三,JSON 序列化时显式使用 ensure_ascii=False,让中文以原文形式输出而不是转义成 \uXXXX,既减小包体,也排除了浏览器端解码翻车的可能。

这三道防线加在一起,保证了从 Python 到网络再到浏览器渲染,中文全程正确。

十一、密钥管理:把安全隐患挡在仓库之外

11.1 绝不硬编码密钥

大模型接口需要鉴权密钥,调用方必须在请求头带上。密钥一旦被提交进公开仓库,任何人都可以复制并盗用你的接口额度,产生费用甚至被用于不当用途。这个项目从设计上就杜绝了这种风险。

密钥的读取顺序是:先查环境变量 GUESS_GAME_LLM_API_KEY;如果没设置,再读项目根目录下的 config.ini[llm] 段的 api_key 字段;两者都没有,则干脆不发起请求,让 AI 接口返回清晰的配置错误提示——而不是在代码里写死一个假密钥。

11.2 gitignore 与示例配置的配合

项目提供了一个 config.ini.example,它写明了配置的正确姿势和各字段用途,可以被提交到仓库供他人参考;而真实的 config.ini 则被写进了 .gitignore,永远不会进入版本管理。这种"示例进仓库、真实文件留在本地"的模式,是配置管理的标准做法。提交记录里我也专门核对了文件列表,确认真实的 config.ini 与密钥都不存在于提交内容中。

11.3 本地运行不受影响

由于本地开发时 config.ini 确实存在且填入了有效密钥,所以本地启动一切正常;clone 到新环境的人则需要自己照抄示例文件并填入自己的密钥。这个权衡是值得的:宁可为新用户增加 30 秒的配置成本,也不能把安全底线交给侥幸。

十二、开发中踩过的坑与解决方案

开发过程中踩过的坑远不止前面提到的几个,这里专门记录下来,既是自我复盘,也希望后来者少走弯路。

12.1 number 输入框的隐藏陷阱

HTML 的 input[type=number] 在移动端会呼出数字键盘,十分方便,但它隐藏着坑:第一,它允许输入 e+-. 这类字符(用于科学计数法),所以 2e3 这种值在 Number("2e3") 里是合法的数字 2000,会悄悄绕过范围校验;第二,它不阻止用户手输超范围数字。单纯依赖 minmax 属性只能约束步进器按键,挡不住键盘输入。解决方案是在逻辑层用 Number.isInteger() 加上范围判断双保险,并在反馈文案里明确告知合法输入范围。

12.2 Math.random 不是用来做"公平抽签"的区间的甩锅对象

Math.random() 本身在 [0,1) 上近似均匀分布,问题往往出在由它推导整数时的映射写法上。前文提到 Math.round(Math.random() * 100) 会让首尾概率减半,就是一个典型例子。这类 bug 在代码审查里很难被发现,但用十万次采样的统计测试可以在几秒钟之内暴露出来。所以这提醒我们:凡是涉及分布的程序逻辑,都应该配一次抽样统计验证。

12.3 backdrop-filter 的兼容性前缀

在部分基于 WebKit 的浏览器中,backdrop-filter 必须写成带 -webkit- 前缀的形式,否则毛玻璃效果直接失效,卡片变成一块实心半透明板子,观感差异巨大。这类 CSS 兼容问题不像逻辑 bug 那样会报错,只能靠逐一核对目标浏览器平台来发现。

12.4 favicon 404 的"伪报错"

如 6.3 节所述,浏览器默认会请求 /favicon.ico,缺少该文件时控制台会打印红色报错。这个报错极具迷惑性,一度让我误以为页面有 JavaScript 异常。排查之后加了一行内联 favicon 声明就彻底解决了。

12.5 fetch 的错误处理需要分层

fetch 的 Promise 只在网络层失败(如服务器根本不可达)时才会 reject;如果服务器正常返回了一个 HTTP 500 或一个内容格式异常的响应,Promise 是 fulfilled 状态的,需要调用方自己去检查响应体里的 ok 字段。我在前端把"网络失败"和"后端返回了错误对象"分开处理,给玩家展示不同的提示文案,避免用一句笼统的"请求失败"糊弄过去——这属于"错误处理也要有信息量"的实践。

十三、测试与验证:不是跑通就行,而是可证伪

对一个课程项目而言,"能跑"通常就是验收标准,但我额外做了一层自己要求的测试体系。原因很简单:代码是写给人看的,更是写给未来运行的——验证这件事本身就应该可重复、可证明。

13.1 后端 JSON 解析的单元测试

extract_json() 是后端最容易出错的地方,我直接构造了四种输入来验证它:标准的纯 JSON、带 Markdown 围栏的 JSON、夹杂了说明文字的 JSON、缺少部分字段的 JSON。逐一运行并核对输出,确认围栏被剥离、杂质被截取、缺字段被兜底。这四个用例覆盖了解析函数最主要的几条路径。

13.2 游戏核心逻辑的批量模拟

我写了一个独立于 DOM 的模拟脚本,把随机数生成、比较判定、范围收缩的逻辑以等价形式重放:

首先,十万次随机数抽样,确认结果永远是闭区间 1 到 100 内的整数;
然后,五千局完整游戏模拟:每局随机一个目标值,玩家使用二分法(每次猜当前范围的中点)出招,检验三件事——每局都能在有限步数内猜中;范围上下界随反馈单调收缩、从不逆向扩张;正确目标始终落在范围之内;
最后,统计五千局中的最大猜测次数。

结果符合理论预期:对 1 到 100 的整数做二分查找,最坏情况需要 7 次,五千局的最大值恰好就是 7。理论推导与模拟实验在这里互相印证,这是最让我放心的验证方式。

13.3 真实浏览器自动化测试

逻辑模拟验证的是"算法",而页面最终是给真实浏览器里的真实用户看的,所以我用 Playwright 驱动了无头浏览器,对运行中的后端做了端到端的冒烟测试:

  • 页面加载:确认标题、"猜一猜"按钮、输入框、初始范围文案均渲染正常;
  • 游玩闭环:脚本以二分策略自动点击,循环读取反馈文字决定下一步,直到出现"猜对了",中途断言范围文案随反馈同步变化;
  • 状态断言:猜中后"游戏状态"变为"已猜中",输入框与按钮进入禁用态;
  • AI 提示:点击 AI 提示按钮,等待面板淡入,断言三个文本字段非空,且内容与当前猜测历史逻辑自洽;
  • 重置:点击重新开局,断言范围恢复为 1 ~ 100、计数归零、状态恢复"进行中";
  • 非法输入:输入 150 提交,断言出现"请输入 1 ~ 100 之间的整数"的警告提示。

测试同时监听页面控制台的错误输出,最终断言"零 JS 错误、零请求 404"。当这些自动化用例全部通过之后,我才认为这个项目的"可用性"是可以签字的。

十四、运行与部署指南

14.1 本地运行三步走

第一步,配置密钥:复制 config.ini.exampleconfig.ini 并填入有效密钥,或设置环境变量 GUESS_GAME_LLM_API_KEY

第二步,启动服务:在项目根目录执行 python3 app.py,终端会打印服务地址与接口信息。

第三步,打开浏览器访问 http://127.0.0.1:8000。游戏核心玩法不依赖密钥即可游玩;AI 提示在密钥缺失时会给出明确的配置指引而不是静默失败。

端口默认为 8000,可通过环境变量 GUESS_GAME_PORT 修改,方便在端口冲突的环境中使用。

14.2 部署注意事项

由于服务器同时提供静态文件与 API,这个项目可以当作一个单服务整体部署。需要特别说明的是:本项目定位为本地演示与学习用途,部署到公网时应在前置一层加强访问控制,例如网关鉴权、限流,避免接口被滥用并产生不必要的模型调用费用。README 中已明确提示这一注意事项。

十五、项目总结与未来展望

15.1 已经完成的闭环

回顾验收清单,项目全部达成:页面加载自动生成均匀随机数;输入、判定、反馈、统计、重置全部可用且边界完备;AI 提示接入大模型,通过系统提示词实现严格 JSON 结构化输出,前端结构化友好展示;密钥不落仓库;静态服务防路径穿越;核心逻辑通过十万级模拟与真实浏览器自动化测试的双重验证;README 文档完整覆盖了从玩法到接口契约到部署的全部信息。

开发过程还有一个意外的收获:二分法五千局最大七次这个结果,让我真实体会到了"算法复杂度分析的结论在实测中印证"的愉悦感。这种工程与理论互相印证的体验,是单纯调通代码很难获得的。

15.2 可以做得更好的地方

项目也留下了一些可以继续打磨的空间。首先是难度设置:目前固定猜测 1 到 100,后续可以增加多个难度档位,例如 1 到 1000 或者上下界可自定义,让"拆分区间"的策略挑战更有层次。其次是排行榜:将每局次数与耗时记录到本地存储甚至后端,形成个人最佳纪录的展示,能显著增加复玩动力。其三是 AI 的"心机竞速":可以统计玩家问 AI 的次数对成绩的影响,甚至让 AI 在提示的同时保持着只提供策略而不剧透的平衡。其四是多语言支持:目前文案为简体中文,抽取成 i18n 配置后可以轻松扩展其他语言。

15.3 关于"小项目里的大工程感"

最后想谈谈我对这个项目的整体思考。猜数字本身是一个再简单不过的需求,但我选择不把它当作"交差作业",而是当作一个完整的小型工程来做:明确目标、编写需求清单、选定技术、设计架构、安全加固、自动化验证、撰写文档。事实证明,即使是一个五百行不到量级的项目,只要把工程化的标准带进来,它就能在职级认可、技能沉淀、复用价值上远超同类的"能跑就行"的版本。所有的知识都是碎片的,而工程化就是把这些碎片牢固地拼成一个整体的方式。

结语

回顾整个开发过程,如果说有一件事是我最想通过这篇博客传达给读者的,那就是:把大模型接进自己的项目,门槛没有想象中那么高,但"让模型输出程序能稳定消费的数据"这件事,需要一点提示词工程的耐心,也需要一套可靠的解析兜底。系统提示词不是玄学,它和任何接口文档一样,是在定义一份和机器对话的数据契约。

这个猜数字游戏现在已经安静地躺在代码仓库里,等待任何一位玩家打开浏览器。如果你愿意,可以在本地运行它,猜一猜数字,再点一下 AI 提示,看看它如何像一个耐心的教练一样,根据你过往的每一步得失给出恰到好处的建议。技术、游戏与一点点恰到好处的智能,就这样在一个 1 到 100 的区间里,被完整地装下了。

Logo

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

更多推荐