一、引言猜词游戏Hangman / Word Guess是一款世界范围内广受欢迎的英文单词猜谜游戏——玩家通过逐个猜测字母来拼出隐藏的英文单词每次猜错都会消耗一条命在规定次数内猜出完整单词即为胜利。它的历史可以追溯到 19 世纪维多利亚时代的纸笔游戏至今仍是语言学习和课堂互动中的经典工具。从技术角度看猜词游戏是一个集合匹配问题。与井字棋的回合制落子和华容道的空间滑动不同猜词游戏的核心操作是字符匹配——每次猜测一个字母在当前单词中查找。用户的思维过程是在 26 个字母的搜索空间中进行推理和排除这个过程与打字式输入完全不同每个字母只能猜一次猜过的字母从可用选项变为已排除项。本文用 ArkUI 从零构建一个英文猜词游戏包含虚拟键盘输入、6 条命的生命值系统、中文提示和中文字库轮换机制。所有交互通过点击屏幕上的 26 个字母按钮完成——无需系统输入法操作流畅一致。阅读完本文你将能够用虚拟键盘替代系统输入法处理单字母输入实现已猜字母集合的追踪与排除用词池洗牌机制避免题目重复用三色反馈区分未猜/猜对/猜错三种字母状态实现生命值递减与游戏结束的条件判断二、游戏设计2.1 规则与目标猜词游戏的规则极为简洁系统从词库中随机选一个英文单词展示中文提示玩家每次点击虚拟键盘上的一个字母如果该字母在单词中所有出现位置都会被揭开如果该字母不在单词中损失一条命共 6 条在 6 条命耗尽前揭开所有字母即胜利每个字母只能猜一次重复猜测无效2.2 词库设计词库由 15 个英文单词及其对应的中文提示组成interfaceWordEntry{word:string;hint:string;}constWORD_BANK:WordEntry[][{word:HARMONY,hint:华为自研操作系统},{word:JAVASCRIPT,hint:网页交互的编程语言},{word:ALGORITHM,hint:解决问题的计算步骤},{word:FUNCTION,hint:可重复调用的代码单元},{word:VARIABLE,hint:存储和引用值的容器},{word:INTERNET,hint:连接全球的网络系统},{word:MOUNTAIN,hint:高耸入云的自然地貌},{word:KEYBOARD,hint:计算机的输入外设},{word:DIAMOND,hint:最坚硬的天然矿物},{word:PYTHON,hint:人工智能首选编程语言},{word:PICTURE,hint:静态的视觉记录},{word:SCIENCE,hint:系统化的知识体系},{word:LIBRARY,hint:存放书籍的建筑},{word:WEATHER,hint:每日的天气状况},{word:CHICKEN,hint:最常见的家禽}];15 个单词涵盖了操作系统、编程语言、数据结构、自然科学、生活常识等多个领域。每个单词附带一个中文提示——提示的作用是缩小搜索空间玩家看到华为自研操作系统就知道这个单词与操作系统相关而非泛泛地从 26 个字母中随机尝试。提示使游戏从盲目搜索变为线索推理。2.3 词池机制与前面 Demo 中每次随机选一个的方式不同猜词游戏使用了词池轮换机制privatewordPool:WordEntry[][];privatepoolIdx:number0;shufflePool():void{this.wordPool[];for(leti0;iWORD_BANK.length;i){this.wordPool.push(WORD_BANK[i]);}// Fisher-Yates 洗牌for(letithis.wordPool.length-1;i0;i--){constjMath.floor(Math.random()*(i1));consttmpthis.wordPool[i];this.wordPool[i]this.wordPool[j];this.wordPool[j]tmp;}this.poolIdx0;}newGame():void{if(this.poolIdxthis.wordPool.length){this.shufflePool();// 词池耗尽重新洗牌}constentrythis.wordPool[this.poolIdx];this.poolIdx;this.targetWordentry.word;this.hintentry.hint;// ...}这个机制的运作方式首次进入页面时wordPool为空poolIdx为 0newGame()检测到poolIdx 0初始空数组长度为 0触发shufflePool()shufflePool()将WORD_BANK的所有条目复制到wordPool然后用 Fisher-Yates 洗牌打乱顺序将poolIdx重置为 0此后每调用一次newGame()poolIdx递增 1取出词池的下一个单词当poolIdx到达词池末尾时第 16 次换题shufflePool()重新洗牌开始新一轮循环这种机制保证了两点不重复在词池的一轮循环中15 个单词同一个单词不会出现两次随机顺序每一轮的单词顺序都是重新随机排列的玩家无法预测下一个单词是什么15 个单词的池子对于猜词游戏来说大小适中——太小如 5 个会让玩家很快看到重复题目太大如 50 个则初始加载无意义。15 个单词需要约 3-5 分钟完成一轮是合理的一次游戏时长。2.4 交互流程一局游戏的交互流程开始系统从词池中取出一个单词展示中文提示和 26 个字母的虚拟键盘猜测玩家点击一个字母 → 判断是否在单词中 → 更新对应位置的显示猜对单词中该字母的每个出现位置都被揭开猜错生命值减 1错误字母显示在错误列表中胜利所有字母被揭开 → 显示绿色通关横幅失败6 条命耗尽 → 显示红色失败横幅揭晓答案换题点击换一题按钮从词池中取出下一个单词三、核心数据结构3.1 已猜字母的表示猜词游戏的核心状态是已经猜过哪些字母。使用一个字符串guessedLetters来存储StateguessedLetters:string;每猜一个字母就将其追加到字符串末尾this.guessedLettersthis.guessedLettersch;这种设计有三个好处去重检测简单this.guessedLetters.indexOf(ch) ! -1即可判断是否已猜过遍历方便猜测字母、错误字母都可以通过遍历字符串获得序列化简单字符串可以直接用于调试日志不需要额外的格式化为什么不使用数组string[]因为字符串的indexOf方法和拼接操作在 ArkTS 中的行为更可预测避免了数组不可变更新时可能遇到的引用问题。而且猜词游戏最多只猜 26 次26 个字母26 个字符的字符串拼接性能完全不是瓶颈。3.2 单词显示displayWord()方法构建当前单词的显示字符串displayWord():string{letresult;for(leti0;ithis.targetWord.length;i){constchthis.targetWord[i];if(this.guessedLetters.indexOf(ch)!-1||this.gameOver){resultresultch ;}else{resultresult_ ;}}returnresult;}两个分支已猜中或游戏结束显示字母本身 空格如H A R M O N Y未猜中显示下划线 空格如_ _ _ _ _ _ _字母之间用空格分隔一是在视觉上形成清晰的字母边界二是等宽字体下每个字母/下划线占据相同宽度形成整齐的网格感。游戏结束时this.gameOver也显示完整单词——这样玩家在失败后可以立即看到正确答案而不是只看到部分揭开的字母。3.3 错误字母wrongLetters()方法从所有已猜字母中筛选出不在目标单词中的字母wrongLetters():string{letresult;for(leti0;ithis.guessedLetters.length;i){constchthis.guessedLetters[i];if(this.targetWord.indexOf(ch)-1){resultresultch ;}}returnresult;}这个方法有两个作用视觉提示让玩家快速看到自己已经猜过哪些无效字母避免重复猜测进度指示错误字母列表的长度直观反映了剩余生命值的消耗情况3.4 字母按钮的三色反馈虚拟键盘上每个字母按钮的颜色由其状态决定letterBg(ch:string):string{if(this.guessedLetters.indexOf(ch)-1)return#F5F5FA;// 未猜过if(this.targetWord.indexOf(ch)!-1)return#C8E6C9;// 猜对return#E8E8EE;// 猜错}letterColor(ch:string):string{if(this.guessedLetters.indexOf(ch)-1)return#1a1a2e;// 深色文字if(this.targetWord.indexOf(ch)!-1)return#2E7D32;// 绿色文字return#CCCCCC;// 灰色文字}三色反馈形成了一个清晰的视觉状态机状态背景色文字色语义未猜过#F5F5FA浅灰蓝#1a1a2e深色可用等待点击猜对了#C8E6C9浅绿#2E7D32深绿正确不可再点猜错了#E8E8EE浅灰#CCCCCC中灰错误不可再点这种三色方案与大多数猜词游戏的视觉语言一致——绿色表示有效信息灰色表示无效信息。已猜过的字母无论对错都会失去视觉可点击感——背景色变淡、文字色变浅——形成自然的已使用感知。四、游戏逻辑4.1 guessLetter 方法每次点击字母按钮触发的核心逻辑guessLetter(ch:string):void{// 守卫条件1游戏已结束if(this.gameOver||this.gameWon)return;// 守卫条件2已经猜过if(this.guessedLetters.indexOf(ch)!-1)return;this.guessedLettersthis.guessedLettersch;if(this.targetWord.indexOf(ch)-1){// 不在单词中 → 生命-1this.wrongCount;if(this.wrongCountMAX_LIVES){this.gameOvertrue;}}else{// 在单词中 → 检查是否全猜对letallGuessedtrue;for(leti0;ithis.targetWord.length;i){if(this.guessedLetters.indexOf(this.targetWord[i])-1){allGuessedfalse;break;}}if(allGuessed){this.gameWontrue;}}}整个方法分为三个阶段阶段一守卫条件两道防线游戏已结束或已胜利 → 直接返回不再处理任何输入字母已经猜过 → 直接返回防止重复扣命这两个守卫条件共同构成了一次性字母规则——每个字母在整个游戏过程中只能被点击一次。这要求在guessedLetters中记录所有已猜字母无论对错而不仅仅记录猜对的字母。阶段二字母判断this.targetWord.indexOf(ch) -1字母不在单词中 → 错误分支this.targetWord.indexOf(ch) ! -1字母在单词中 → 正确分支indexOf的返回值如果字母存在于单词中返回首次出现的索引0 到 word.length-1如果不存在返回 -1。这个 API 比includes更可靠——ArkTS 中string.includes在某些版本中可能不可用而indexOf是 ECMAScript 早期标准兼容性最好。阶段三通关检测在正确分支中需要检测是否所有字母都已被揭开。做法是遍历目标单词的每个字符检查它是否在guessedLetters中。如果存在任何一个未被猜过的字母allGuessed保持false游戏继续如果所有字母都在guessedLetters中即allGuessed为true设置this.gameWon true。这里有一个不明显的细节通关检测只在猜对字母时执行。因为猜错字母不会揭开任何字母所以是否全猜对的状态不会因为猜错而改变。只在猜对时检测避免了不必要的遍历。4.2 生命值系统constMAX_LIVES:number6;livesDisplay():string{letresult;for(leti0;iMAX_LIVES;i){if(iMAX_LIVES-this.wrongCount){resultresult❤️;}else{resultresult;}}returnresult;}6 条命对应 6 个爱心图标剩余生命❤️红色爱心已损失生命黑色爱心为什么是 6 条命这是一个约定俗成的数字——经典的 Hangman 游戏通常使用 6 次错误机会对应 6 次猜错绘制出完整的绞刑架图案头、身体、左手、右手、左腿、右腿。本 Demo 虽然使用爱心而非绞刑架但保留了 6 条命的设计——6 次错误在 26 个字母的搜索空间中给予了玩家足够的容错率同时又不至于让游戏变得太简单。4.3 游戏结果横幅gameMessage():string{if(this.gameOver)return 游戏结束;if(this.gameWon)return 恭喜猜对;return;}gameSubMessage():string{if(this.gameOver)return答案是this.targetWord;if(this.gameWon)return剩余 (MAX_LIVES-this.wrongCount) 条命;return;}gameBannerColor():string{if(this.gameOver)return#FF4D4F;return#52C41A;}结果横幅使用条件渲染——只有gameOver或gameWon为true时才显示if(this.gameOver||this.gameWon){Column(){Text(this.gameMessage())Text(this.gameSubMessage())}.backgroundColor(this.gameBannerColor())// ...}横幅的两个状态失败红色背景#FF4D4F显示 游戏结束和正确答案胜利绿色背景#52C41A显示 恭喜猜对和剩余生命gameBannerColor()方法有一个有意思的逻辑简化它只判断gameOver返回红色其余情况返回绿色。因为在条件渲染的if已经保证了要么 gameOver 要么 gameWon所以不存在两者都为 false 但仍需返回颜色的情况。五、UI 设计5.1 页面结构页面从上到下分为五个区域┌────────────────────────────────────┐ │ 猜词游戏深色标题栏 │ ├────────────────────────────────────┤ │ ❤️❤️❤️❤️❤️❤️6条命 │ ← 生命值 │ 提示华为自研操作系统 │ ← 中文提示 ├────────────────────────────────────┤ │ [ 恭喜猜对 剩余 4 条命]可选 │ ← 结果横幅 ├────────────────────────────────────┤ │ H A R M O N Y │ ← 单词显示 │ 错误B C D E F │ ← 错误字母 ├────────────────────────────────────┤ │ A B C D E F G H I J K L M │ ← 虚拟键盘 │ N O P Q R S T U V W X Y Z │ (26个字母按钮) ├────────────────────────────────────┤ │ 换一题 │ ← 换题按钮 └────────────────────────────────────┘5.2 虚拟键盘26 个字母按钮使用Flex({ wrap: FlexWrap.Wrap })布局Flex({wrap:FlexWrap.Wrap,justifyContent:FlexAlign.Center}){ForEach(ALPHABET,(ch:string){Text(ch).fontSize(13).fontColor(this.letterColor(ch)).fontWeight(FontWeight.Bold).width(34).height(34).backgroundColor(this.letterBg(ch)).borderRadius(BorderRadius.SM).textAlign(TextAlign.Center).margin({left:1.5,right:1.5,bottom:Spacing.SM}).onClick((){this.guessLetter(ch);})})}键盘布局的要点FlexWrap.Wrap26 个按钮在水平方向排列不下时自动换行适应不同屏幕宽度justifyContent: FlexAlign.Center每一行的按钮居中排列每个按钮 34×34 vp足够大的点击区域手指轻松点击同时 26 个按钮在 360vp 宽的屏幕上约 7-8 个一行形成 4 行键盘BorderRadius.SM4vp 圆角比默认的方形更友好但又不像圆形按钮那样浪费空间margin: { left: 1.5, right: 1.5, bottom: Spacing.SM }按钮间距极小最大化利用宽度与输入法键盘QWERTY不同这里的 26 个字母按 A-Z 字母顺序排列。A-Z 顺序对于中文用户来说直观——用户不需要学习布局找字母时按首字母自然排序比 QWERTY 更快。5.3 换题按钮Button( 换一题).fontSize(FontSize.MEDIUM).fontColor(#FFFFFF).fontWeight(FontWeight.Bold).backgroundColor(#667eea).borderRadius(BorderRadius.FULL).padding({left:28,right:28}).height(48).shadow({radius:8,color:#667eea44}).onClick((){this.newGame();})换题按钮使用BorderRadius.FULL9999vp实现胶囊形搭配紫色渐变阴影#667eea4428% 透明度形成微妙的悬浮感。点击后调用newGame()从词池中取出下一个单词重置所有游戏状态。六、完整代码结构WordGuessPage (~260 行) ├── 数据定义 │ ├── WordEntry 接口 — 单词提示 │ ├── WORD_BANK[15] — 词库 │ └── ALPHABET[26] — 26个字母 ├── 状态变量 │ ├── State targetWord — 当前单词 │ ├── State hint — 中文提示 │ ├── State guessedLetters — 已猜字母串 │ ├── State wrongCount — 错误次数 │ └── State gameOver / gameWon — 游戏阶段 ├── 词池管理 │ ├── shufflePool() — Fisher-Yates洗牌 │ └── newGame() — 取词初始化 ├── 游戏逻辑 │ └── guessLetter() — 守卫→判断→检测 ├── 视图辅助 │ ├── displayWord() — 单词/下划线显示 │ ├── wrongLetters() — 错误字母列表 │ ├── letterBg/letterColor() — 三色反馈 │ ├── livesDisplay() — 爱心生命值 │ └── gameMessage/subMessage/bannerColor() ├── 视图 │ ├── 标题栏 — 猜词游戏 │ ├── 生命值 — ❤️ 显示 │ ├── 中文提示 │ ├── 结果横幅条件渲染 │ ├── 单词显示区 │ ├── 错误字母区条件渲染 │ ├── 26键虚拟键盘 FlexForEach │ └── 换题按钮 └── 生命周期 └── aboutToAppear() — 调用 newGame()七、与前面 Demo 的对比猜词游戏在技术层面上与本系列的前几个游戏 Demo 形成了清晰的对比7.1 输入方式前面 Demo 的输入方式各不相同计算器/秒表按钮操作确定性的计算/计时逻辑待办清单/日记本系统输入法文本输入井字棋/华容道棋盘格子点击空间操作记忆翻牌卡片翻转配对记忆匹配猜词游戏的输入方式最独特26 个字母按钮替代了系统输入法。这种设计的优势在于操作一致所有交互都是点击按钮不需要切换输入法视觉反馈每个字母按钮的状态未猜/已猜对/已猜错直接可见字母去重已经猜过的字母视觉上不可再点不需要额外检查7.2 状态表示猜词游戏的状态表示比井字棋9 格 3 状态和华容道16 格 16 值更简单primary 状态guessedLetters字符串26 个字母的子集派生状态单词显示、错误列表、生命值、胜负判断——全部从guessedLetters推导这种一个主状态 多个派生视图的架构在 ArkUI 中非常自然——State变量变化自动触发所有依赖它的Text、Column重新渲染。不存在同步多个状态的复杂度。7.3 难度调节猜词游戏的难度由两个隐含因素决定单词长度7-10 个字母的单词HAMONY 7 个JAVASCRIPT 10 个是理想长度。太短3-4 个字母容易猜中缺乏挑战太长12 个字母需要猜的字母太多容错率不足提示精确度华为自研操作系统缩小了搜索范围——知道这是操作系统名称有助于排除大量无关单词这种提示引导 字母推理的难度曲线比纯随机猜测要平滑——玩家在早期利用提示缩小范围在后期利用已揭开的字母推断剩余字母整个过程从知识检索过渡到模式匹配。八、总结本文从零构建了一个英文猜词游戏。与井字棋策略博弈、记忆翻牌记忆力挑战和华容道空间推理不同猜词游戏的核心是字符串匹配与集合推理——每次猜测在 26 个字母的集合中进行排除逐步缩小候选空间。从技术角度看它也是虚拟键盘输入、三色状态反馈和词池轮换机制的完整示例。核心要点回顾虚拟键盘替代系统输入法26 个Text按钮使用Flex({ wrap: FlexWrap.Wrap })布局A-Z 字母顺序排列每个按钮 34×34vp。所有交互通过点击完成操作一致且视觉反馈直接。字符串表示集合guessedLetters是一个字符串存储所有已猜字母。indexOf()检测是否已猜过去重、是否在单词中对错判断。字符串的拼接操作在 ArkTS 中行为稳定26 个字符级的性能完全无问题。三色反馈状态机每个字母按钮有三种视觉状态——未猜浅灰蓝底 深色字、猜对浅绿底 绿色字、猜错浅灰底 灰色字。三种状态通过letterBg()和letterColor()方法从guessedLetters和targetWord推导没有额外的状态变量。词池轮换避免重复使用 Fisher-Yates 洗牌打乱 15 个单词的顺序顺序取用。一轮 15 题中不会出现重复单词词池耗尽后重新洗牌开始新一轮。这比每次随机选更能保证玩家体验的多样性。通关条件判断只在猜对字母时检测通关遍历目标单词的每个字符检查是否都在guessedLetters中。猜错字母不可能改变是否全猜对的状态避免不必要的遍历。派生视图模式displayWord()、wrongLetters()、livesDisplay()都是纯粹的派生方法——它们只读取State变量不修改任何状态。当guessedLetters变化时所有使用它的视图自动重新渲染。这是 ArkUI 声明式 UI 的核心优势。猜词游戏是一款规则简单、策略丰富的经典游戏。这个 260 行的 ArkUI 实现抓住了它的核心乐趣虚拟键盘的触感反馈、猜对字母时的豁然开朗、以及最后几个字母揭晓前的紧张感。它是本系列第四篇游戏类文章也是字符串集合操作和虚拟键盘交互的完整示例。