猜谜游戏前端实现:JSON题库、判定逻辑与状态机设计

发布时间:2026/10/10 20:44:21
猜谜游戏前端实现:JSON题库、判定逻辑与状态机设计
简介猜谜游戏项目是一份基于JavaScript开发的互动式Web小游戏源码面向前端初学者与进阶学习者帮助理解事件监听、DOM操作、游戏逻辑、错误处理与模块化组织等核心概念。压缩包共8个文件涵盖HTML页面入口、CSS样式表、JavaScript主逻辑代码以及JSON/YML配置、Markdown说明文档和图片动图素材整体大小约1.12MB结构精简适合快速研读。已有138人学习下载。项目通过addEventListener捕获用户点击使用Math.random生成随机谜题利用DOM操作动态展示题目与反馈同时结合localStorage保存进度并以try...catch机制应对异常输入完整呈现了从交互触发到界面更新的闭环。对于想掌握Web游戏开发流程的读者可从源码中学到随机数生成、条件判断、事件处理、数据持久化等多处实用技巧配合说明文档还能理解项目配置与工程组织方式是一份既有教学意义又具实践价值的练手资源。1. 猜谜游戏从一句“猜对了”到一套可复用的答题逻辑一个猜谜游戏表面看就是把题目显示出来、接收输入、比对答案、给个对错。可真到自己动手写大家翻车大多翻在同一个地方判定逻辑。明明答案是对的程序却提示猜错了换了一台设备进度又不见了题目一多改个提示语要翻半天文件。这种问题查起来比功能本身还费时间。这篇笔记围绕猜谜游戏从零到落地的一套浏览器端方案把题目数据结构、判定策略、交互实现和踩坑排查完整串一遍。你不需要框架不需要后端一个 HTML 文件加一份题目数据就能跑起来。新手照着敲能出成品熟手可以直接抄走判题和存档的思路。2. 猜谜游戏的核心状态流转、题目结构与三种判定策略猜谜游戏本质是一个极简状态机等待作答、接收输入、判定结果、展示反馈、进入下一题。这个循环看起来简单但大部分实现烂就烂在把“判定”写死在事件回调里导致换题目类型、加提示、做统计时全要改代码。我先把这三件事拆开讲题目怎么存、答案怎么判、界面怎么走。2.1 谜题数据为什么适合用 JSON 组织我见过有人把题目直接写死在渲染函数里比如document.getElementById(q1).innerHTML 一口咬掉牛尾巴题目一多就变成一场灾难。常见做法是把题目抽成纯数据用 JSON 数组组织。这样做的好处是题目和逻辑分离改题不改代码加分类不加分支统计和出题都能直接遍历数据。一份最简题目结构长这样[ { id: 1, category: 字谜, question: 一口咬掉牛尾巴打一字, answer: 告, accepts: [告, 告字], hint: 想想“牛”少了尾巴下半截, difficulty: 1 } ]字段含义分别说一下id是题目唯一编号用于统计和去重category是分类后续可以按分类出题question是展示给用户的题干answer是规范答案用于判定和展示accepts是容错答案列表可以填同义词、口语化表达hint是提示文本用户卡住时展示difficulty是难度等级用于权重出题。这里有个容易被忽略的点answer只存一个最规范的答案accepts里存所有能接受的写法。比如题目“中国最大的内陆湖打一湖名”answer存“青海湖”accepts里额外放“青海”。判定时优先遍历accepts如果数组为空再退回比对answer。这样设计是为了避免每个题目都背着十几个同义词数组反而把规范答案搞没了。2.2 判定对错的三种策略精确匹配、规则归一与选项式猜谜游戏的判定策略直接决定用户体验。我梳理下来实际项目里逃不出三种第一种是精确匹配用户输入和答案字符串完全一致才算对。实现成本最低但用户会为“多了个空格”“括号是半角还是全角”“大小写不同”这种原因被误判。只适合答案极短且唯一的情况比如“打一个字”。第二种是规则归一化匹配先对用户输入做标准化处理去掉首尾空格、全角转半角、统一大小写、去掉中间空白再做比对。这是猜谜游戏里性价比最高的方案解决了绝大多数“明明对了却判错”的问题。第三种是选项式判定题目本身就是选择题用户从几个选项里选一个。这时候判定逻辑退化为selectedIndex correctIndex反而不能用归一化字符串去匹配因为选项文本可能相同但编号不同。我一般会在方案里同时支持第一种和第二种用归一化函数垫底再配合accepts列表覆盖特例。归一化函数的常见实现大致是function normalizeAnswer(input) { return input .replace(/[\uff01-\uff5e]/g, function (ch) { return String.fromCharCode(ch.charCodeAt(0) - 0xfee0); }) .replace(/\s/g, ) .toLowerCase(); }逻辑说明第一步把全角标点和字母转成半角区间\uff01-\uff5e覆盖了全角感叹号到全角波浪线第二步去掉所有空白字符包括空格、换行和制表符第三步统一成小写。参数上0xfee0是全角字符到半角字符的偏移量这是 Unicode 里固定的差值。注意这函数不会把所有字母都转小写之外的东西所以“A”和“a”、“”和“A”都会被判为一致。做完归一化后判定就变成一个简单的包含关系判断function checkAnswer(question, rawInput) { const normalizedInput normalizeAnswer(rawInput); const candidates question.accepts question.accepts.length ? question.accepts : [question.answer]; return candidates.some(accept normalizeAnswer(accept) normalizedInput); }逻辑说明先取出候选答案列表有accepts用accepts没有就用answer包一层数组然后用some逐个做归一化比对只要有一个匹配就返回真。参数上注意some是短路计算题目里accepts一般不会超过 5 个性能可以忽略但代码可读性好很多。2.3 猜谜状态机的四个环节出题、作答、反馈、推进猜谜游戏界面上的操作看似自由实际上状态是有限的。我把四个环节画成一张表格方便对照实现状态触发事件界面表现数据变更出题初始化或点击下一题显示题干、清空输入框、隐藏提示当前题号切换作答用户输入并提交输入框可编辑提交按钮可用无反馈提交后显示对错、正确答案、提示按钮记录本次对错推进点击下一题或自动跳转渲染新题重置输入状态答题计数加一为什么要单独强调状态机因为新手最常见的错误是把界面的重置逻辑散落在各个点击事件里提交时清空输入框下一题时又要清一次提示出现后忘了隐藏。状态一旦多起来界面就乱了。我实际做的时候会把四个环节收敛成两个函数renderQuestion(index)负责出题和推进handleSubmit()负责作答和反馈。renderQuestion只做一件事把当前题目的数据渲染到界面上并把所有交互控件恢复到初始状态。这样无论从哪个入口进入下一题界面表现都是一致的不会出现“答完题提示还在”这种尴尬。这里有个设计取舍反馈状态要不要自动消失我建议不要自动消失而是等用户主动点“下一题”再切换。猜谜游戏的核心体验在于思考用户可能需要盯着反馈看一会儿正确答案。自动跳转倒计时看着流畅实际会让用户产生“被赶着走”的压迫感。3. 落地一个可玩版本HTML 结构、核心函数与本地运行这一章直接给出一套能跑的完整实现。我用单文件方式组织题目数据放在脚本里不依赖外部请求这样双击打开就能玩部署到任意静态托管也可以。先搭骨架再写核心逻辑最后给运行命令。3.1 页面骨架输入框、按钮、反馈区的最小组合页面交互只需要四个元素题干展示区、输入框、提交按钮、反馈区。再加一个进度提示提升使用体验。HTML 骨架如下!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title猜谜游戏/title /head body div idapp p idprogress第 1 / 10 题/p h2 idquestion/h2 input idanswer typetext autocompleteoff placeholder输入你的答案 button idsubmit提交/button button idnext styledisplay:none下一题/button p idhint styledisplay:none/p p idfeedback/p /div script srcgame.js/script /body /html关键参数说明autocompleteoff是防止浏览器把之前的答案当作候选词弹出干扰输入progress显示当前进度让用户知道游戏有结束next按钮初始隐藏只有提交后才出现暗示用户先作答再推进。script放在body底部确保脚本执行时 DOM 已经解析完毕这是避免白屏最基础的手段。样式方面不用花哨我一般只做三点限制容器宽度居中、输入框和按钮的字体不小于 16px、反馈文字用颜色区分对错。字体不小于 16px 是在手机上避免 iOS 自动缩放输入框的坑后面排错章节会展开说。3.2 出题、判题、计分核心 JavaScript 函数把逻辑拆成题库数据、当前状态、渲染函数、提交处理四块。题目数据放前面状态量集中声明再按职责写函数。const QUESTIONS [ { id: 1, category: 字谜, question: 一口咬掉牛尾巴打一字, answer: 告, accepts: [], hint: “牛”少了尾巴被什么咬住了, difficulty: 1 }, { id: 2, category: 常识, question: 中国的母亲河是哪条河, answer: 黄河, accepts: [黄河], hint: 流经黄土高原, difficulty: 1 }, { id: 3, category: 脑筋急转弯, question: 什么门永远关不上, answer: 球门, accepts: [足球门, 球门], hint: 足球场上的那种门, difficulty: 2 } ]; let currentIndex 0; let correctCount 0; let answered false;状态量说明currentIndex是当前题目在数组中的下标correctCount统计答对次数用于结界面板answered是一个防重开关核心作用是防止用户对同一题重复提交刷分。这三个量一旦被修改界面就要通过渲染函数刷新后面会看到我为什么强调“渲染和状态分离”。出题函数的核心逻辑是渲染当前题目并且把所有交互控件重置到初始状态function renderQuestion() { const q QUESTIONS[currentIndex]; document.getElementById(progress).textContent 第 (currentIndex 1) / QUESTIONS.length 题; document.getElementById(question).textContent q.question; document.getElementById(answer).value ; document.getElementById(feedback).textContent ; document.getElementById(hint).style.display none; document.getElementById(next).style.display none; document.getElementById(submit).style.display inline-block; answered false; document.getElementById(answer).focus(); }逻辑说明先用currentIndex取出当前题目对象然后逐项更新界面。value 清空输入框反馈区和提示区都隐藏下一题按钮隐藏提交按钮恢复可见。最后把焦点放到输入框上用户在键盘上敲字不需要先点一下输入框。参数上这里用textContent而不是innerHTML是为了避免题干里包含 HTML 片段时被当成标签执行题目是纯文本textContent更安全。提交处理则是对作答和反馈的收口function handleSubmit() { if (answered) return; const q QUESTIONS[currentIndex]; const userAnswer document.getElementById(answer).value; const isCorrect checkAnswer(q, userAnswer); if (isCorrect) { correctCount; document.getElementById(feedback).textContent 答对了答案就是「 q.answer 」; document.getElementById(feedback).style.color #2a7a3e; } else { document.getElementById(feedback).textContent 没猜对正确答案是「 q.answer 」再想想下一题吧; document.getElementById(feedback).style.color #b02a2a; } document.getElementById(hint).textContent 提示 q.hint; document.getElementById(hint).style.display block; document.getElementById(submit).style.display none; document.getElementById(next).style.display inline-block; answered true; }逻辑说明answered开关防重复提交进入函数第一步判断如果已经提交过就直接返回。答对时累加correctCount无论对错都展示正确答案同时把提示区显示出来。按钮的显示切换实现状态迁移提交后提交按钮消失、下一题按钮出现这对应前文状态机里的“作答到反馈”环节。之所以答错也直接给正确答案是因为猜谜游戏的乐趣在“看了答案恍然大悟”而不是反复试错。下一题的处理更简单但有一个数组越界判断function nextQuestion() { if (currentIndex QUESTIONS.length - 1) { currentIndex; renderQuestion(); } else { document.getElementById(question).textContent 全部完成答对 correctCount / QUESTIONS.length 题; document.getElementById(progress).textContent 游戏结束; document.getElementById(answer).style.display none; document.getElementById(submit).style.display none; document.getElementById(next).style.display none; } }逻辑说明当currentIndex还没到数组末尾时递增后重新渲染。到末尾时隐藏所有输入控件只展示一个总结文本。correctCount是直接拼接进字符串的不需要额外模板。这里需要留意的是currentIndex的越界判断只能阻止“主动进入下一题”无法阻止用户刷新页面刷新后一切归零这属于预期行为本地存档方案放最后一章讲。3.3 本地运行与调试不用框架也能跑的最小命令现在这个项目已经是一个完整的静态页面运行方式有三种。如果题库直接写在game.js里双击index.html就能打开运行不需要任何服务。如果题目数据换成外部 JSON 文件通过fetch加载则必须起一个本地 HTTP 服务否则浏览器会因跨域限制拦截请求。我一般建议直接起服务养成习惯因为后续大概率会引入外部题库。最小命令是cd /path/to/guess-game python3 -m http.server 8000然后在浏览器打开http://localhost:8000。参数说明8000是端口号可以换成任意未被占用的端口python3 -m http.server是标准库自带的静态文件服务不需要安装额外依赖。如果不用 Python也可以用npx serve或 VS Code 自带的 Live Server 插件。无论哪种方式核心都是一样的给浏览器提供一个合法的 HTTP 上下文让页面能够加载本地资源。调试时打开浏览器开发者工具的 Console 面板。猜谜游戏最常见的运行时错误有两个QUESTIONS is not defined表示脚本加载顺序不对要么game.js路径写错要么脚本变量名拼错Cannot read properties of undefined则表示currentIndex越界或者题目对象字段缺失。看一眼报错的堆栈信息基本能定位到具体函数。4. 题库与规则设计难度、分类与随机出题的参数取舍代码跑通了刷题阶段才算真正开始。猜谜游戏好玩与否七成靠题目质量三成靠出题规则。题目字段怎么设计、难度权重怎么调、分类怎么用这些直接决定用户是否会点第二次。4.1 题目字段怎么设计才不返工我先给一套经过多次调整后的字段规范这套结构适合绝大多数的猜谜游戏{ id: 101, category: 成语, question: 最遥远的地方打一成语, answer: 天涯海角, accepts: [天涯海角, 天涯, 海角], hint: 想想“天涯”和“海角”的距离, difficulty: 2, tags: [方位, 夸张] }参数说明id必须是全库唯一的整数方便统计时建立映射category是展示用分类名不建议用数字编号代替因为调试时一眼看懂比省几个字节重要difficulty取值建议为 1 到 3分别对应简单、中等、困难。tags是可选的标签数组用于更精细的出题过滤比如“只出方位类成语”。容易返工的设计是把accepts和answer混在一起用。常见错误是answer里存多个答案用顿号分隔然后判定时再切分字符串。这个方案可行但很脆弱因为某一个答案里恰好包含顿号就会被切错。answer永远只存一个规范答案容错写法统一放accepts这应该是一条硬约束。还有一个容易踩坑的是题干里带标点。比如“最遥远的地方打一成语”括号到底是全角还是半角我的习惯是题库里统一使用全角括号因为题干是展示给用户的中文句子全角括号排版更自然。但判定时完全不受这个影响答案在answer字段里不在题干里。换句话说题干是给人看的怎么排版都行答案是给判定逻辑用的必须干净。4.2 难度权重与洗牌算法随机不等于均匀题目少的时候按顺序出题没有体验问题。题目一旦超过 30 道就必须考虑随机出题否则用户每次玩到的都是同一批题新鲜感断崖式下降。我见过两种翻车做法第一种是每次从全部题目里完全随机抽结果连续三道都是困难题新手直接被劝退第二种是纯按数组顺序出题目分布倒是均匀但玩了两次就记住了答案顺序。常见做法是把“洗牌”和“权重”结合起来先把所有题目按难度加权打散再从打散后的序列里依次取题。加权打散的实现可以这样function weightedShuffle(questions) { const weighted []; questions.forEach(q { const weight q.difficulty 1 ? 3 : q.difficulty 2 ? 2 : 1; for (let i 0; i weight; i) { weighted.push(q); } }); for (let i weighted.length - 1; i 0; i--) { const j Math.floor(Math.random() * (i 1)); [weighted[i], weighted[j]] [weighted[j], weighted[i]]; } return weighted; }逻辑说明先把每题按难度复制多份简单题复制 3 份、中等题复制 2 份、困难题复制 1 份然后对这个扩容后的数组做 Fisher-Yates 洗牌。这样抽到简单题的概率是困难题的三倍但每一轮的序列仍然是随机的。参数上那个weight取值可以按需调整比如想让困难题出现更少就把难度 3 的 weight 改成 0.5让数组里出现半份——但这里有一个坑复制份数必须是整数否则for循环的次数说不清楚。想支持非整数权重需要换成“按权重抽取不放回”的算法复杂度会高不少。对猜谜游戏来说整数权重已经足够。洗牌算法本身值得一提。很多初学者会用Math.random() - 0.5做排序比较函数也就是arr.sort(() Math.random() - 0.5)。这个写法代码最短但结果分布不均匀而且会随着数组长度增加越来越偏。Fisher-Yates 是每次从未处理的部分随机挑一个元素放到末尾复杂度 O(n)分布均匀也是我唯一会写进生产代码的洗牌方式。4.3 冷启动与归类先做 20 题还是先做 60 题做题库最常见的惨痛教训是先花两周攒了 500 道题然后发现判定逻辑有个基础 bug500 道题全部要重新过一遍。冷启动阶段我强烈建议只做 20 道题覆盖 4 到 5 个分类每个分类 4 到 5 题。对这 20 题的目标只有一个验证判定策略的覆盖率。具体验证方法是把每个题目的answer和accepts逐条喂进checkAnswer确认返回结果为真。再写几个故意出错的输入比如半角括号、答案带空格、大小写混用确认误判率在可接受范围。20 道题足够暴露绝大多数数据层面问题比如某个答案里带了引号导致 JSON 解析失败、某个accepts项没有去重导致统计重复计数。等判定逻辑稳定了再按分类扩库。扩库时保持每个分类的题目量相对均衡避免出现“字谜 80 题、常识 10 题”的头重脚轻局面。出题权重也可以按分类调整比如新手期多出简单类别活跃期多出困难类别。这些规则在题库大了以后再引入代码层面只需要在weightedShuffle前面加一层过滤先按当前用户等级筛出符合条件的题目集合再对这个子集做加权洗牌。5. 猜谜游戏常见问题排查与避坑从白屏到判错这一章是平时被问得最多的地方。我按现象到原因到解决的顺序写每条都是实际踩过的坑。遇到问题先对照现象不要急着改代码。5.1 页面打开是空白脚本加载顺序与路径现象双击 HTML 文件打开页面只有背景色题目、输入框、按钮全部不显示。Console 面板里报错但很多人不看。原因最常见的是script srcgame.js的路径写错。相对路径是相对于当前 HTML 文件的目录不是相对于站点根目录。如果game.js和index.html不在同一目录srcgame.js必然 404。第二个常见原因是脚本放在了head里且没有加defer脚本执行时 DOM 还没解析完document.getElementById拿不到元素直接报错。解决把脚本移到/body之前或者给script标签加defer属性。路径用相对当前文件的写法先确认文件在同一个文件夹里。打开开发者工具的 Network 面板如果game.js显示红色状态码 404说明就是路径问题没有其它可能。这类问题十有八九是文件放错层级而不是代码本身有 bug。5.2 答案明明对却判错全角、空格与标点现象用户输入“黄河”答案也是“黄河”但反馈显示“没猜对”。用户截图过来两边肉眼看起来一模一样。原因常见的是输入法在全角状态下输入字母和数字变成了全角字符或者用户复制答案时前后带了不可见空格还有题目答案是“生日快乐”而用户输入“生日快乐”带了感叹号。这些问题本质上都是字符串比对太严格。解决用前文normalizeAnswer做统一归一化同时把answer和accepts里的每个选项也过一遍归一化。需要注意代码里写死的answer也可能带全角字符比如某道题的答案里有一个全角空格这是复制题目时带进来的。建议在代码里对answer字段做一次清洗校验写一个小脚本遍历题库把答案里的全角空格、首尾空白全部剔除发现异常直接报出来。这类问题看起来像“玄学”查下来大多是编码或不可见字符。5.3 刷新后进度清零内存变量与 localStorage 的边界现象答到第 8 题不小心按了刷新回到第 1 题答对数量也归零。用户抱怨“玩了一半丢了”。原因currentIndex和correctCount都只是 JavaScript 内存变量刷新页面后脚本重新执行所有数据重新初始化。这是预期行为但没有存档的猜谜游戏确实体验残缺。解决引入localStorage保存进度在renderQuestion和handleSubmit里同步写入。写入时注意两点一是所有状态合并成一个对象再存不要拆成多个 key否则数据可能不一致二是localStorage.setItem在浏览器禁用存储时会抛异常必须包一层try/catch降级为无存档模式。存档的完整代码放在下一章这里先记住边界内存变量管当次会话localStorage管跨会话两者职责不清才是问题根源。5.4 中文输入法回车“吞答案”composition 事件现象用户用拼音输入法打字打完拼音按空格上屏再按回车提交。但实际效果是“按回车直接提交了上一次输入”答案被截断成拼音或者上一个词。原因输入法在打字过程中会触发compositionstart上屏时触发compositionend。在这期间用户按回车是输入法用来确认候选词的但页面的提交事件监听器也会被触发。输入法的回车被页面当成提交按钮的回车导致用户还来不及上屏就提交了。解决在输入框的keydown事件里加判断如果当前处于中文输入合成状态就直接放行不触发提交。实现上维护一个标志位let isComposing false; answerInput.addEventListener(compositionstart, () { isComposing true; }); answerInput.addEventListener(compositionend, () { isComposing false; }); answerInput.addEventListener(keydown, (e) { if (e.key Enter !isComposing) { handleSubmit(); } });逻辑说明compositionstart和compositionend是输入法合成文本的生命周期事件前者在拼音开始输入时触发后者在候选词上屏时触发。isComposing在合成期间为真keydown里判断到合成状态就跳过提交。注意还应该同时处理compositionupdate事件在某些 Android 浏览器上输入法会持续触发它虽然处理逻辑不变但监听它有助于保持状态同步。这个坑在英文环境下永远不会出现只有中文输入法用户会遇到所以特别容易被人忽略。5.5 移动端点击迟滞与键盘遮挡现象手机上打开页面点击提交按钮要等半秒才有反应输入框聚焦后弹出键盘键盘把反馈区挡住用户看不到对错结果。原因第一个现象是浏览器为了区分单击和双击缩放在click事件上人为加了约 300ms 延迟。现在已经很少见但老设备或未设置 viewport 的页面仍然存在。第二个现象是页面布局没有考虑视觉视口收缩键盘弹出后可视区域变矮反馈区被推到折叠区域下方。解决在head里已经设置了viewport的情况下点击延迟基本消失因为widthdevice-width告诉浏览器不需要等待双击缩放。如果还觉得迟滞可以把提交事件改成pointerdown触发但要注意pointerdown在滑动页面时也会误触发需要配合简单的位移判断。键盘遮挡问题更稳妥的解法是让反馈区出现在输入框上方而不是页面底部或者监听visualViewport的 resize 事件把反馈区滚入可视范围。还有一个基础习惯所有可点击元素的字体不小于 16px按钮点击区域不小于 44px 见方这能让移动端误触率显著下降。6. 猜谜游戏进阶把每局成绩存成本地统计表基础版本能玩之后我建议加一个低成本高感知的功能本地对错统计。用户每次答完题把结果记下来之后可以查看自己哪类题目容易错、每天答了多少题。这一步不需要后端localStorage完全够用。核心思路是把判定和记录拆开判定函数只负责返回真假记录函数单独处理数据写入。记录的结构按日期分桶每天的记录包含总题数和正确题数键名带版本号方便未来迁移function saveRecord(isCorrect) { const key guess_stats_v1; let stats {}; try { stats JSON.parse(localStorage.getItem(key) || {}); } catch (e) { stats {}; } const today new Date().toISOString().slice(0, 10); if (!stats[today]) { stats[today] { total: 0, correct: 0 }; } stats[today].total; if (isCorrect) stats[today].correct; try { localStorage.setItem(key, JSON.stringify(stats)); } catch (e) { // 隐私模式下写入会失败忽略即可 } }逻辑说明每次提交判定完成后调用saveRecord(isCorrect)传入的是判定结果的真假值而不是在记录函数里重新判一次。today用toISOString().slice(0, 10)取出日期格式是YYYY-MM-DD兼容所有主流浏览器。外层try/catch包住读取和写入是为了在隐私模式或存储被禁用时不阻断游戏流程。参数上注意 key 里的v1版本号很关键以后调整数据结构时直接换v2旧数据不用迁移。看了统计数据用户能直观发现自己答错的题集中在哪个分类。我之前做的一个版本里有人反馈“字谜每天错最多”于是我就把字谜分类改成默认展示提示答对的概率明显提升。数据驱动的题目调整比拍脑袋改题高效得多。最后说一个我自己的教训最早做统计功能时我把记录逻辑直接写在了handleSubmit里结果“答错重试”也会被记成一次新的答题统计里的总题数虚高。后来把“判定”和“记录”拆成两个函数handleSubmit只调判定判定返回后才决定是否记录重试。这个边界的教训是猜谜游戏的每一步逻辑都应该有清晰的职责判定归判定渲染归渲染记录归记录。现在再做这类小项目我会习惯性地在第一版就把这三个模块分文件放好而不是等项目长大了再重构。希望这些思路对你做猜谜游戏有帮助照着这套结构改做出自己满意的版本并不难。本文还有配套的精品资源点击获取