一、引言

猜词游戏(Hangman / Word Guess)是一款世界范围内广受欢迎的英文单词猜谜游戏——玩家通过逐个猜测字母来拼出隐藏的英文单词,每次猜错都会消耗一条命,在规定次数内猜出完整单词即为胜利。它的历史可以追溯到 19 世纪维多利亚时代的纸笔游戏,至今仍是语言学习和课堂互动中的经典工具。

从技术角度看,猜词游戏是一个集合匹配问题。与井字棋的"回合制落子"和华容道的"空间滑动"不同,猜词游戏的核心操作是字符匹配——每次猜测一个字母,在当前单词中查找。用户的思维过程是在 26 个字母的搜索空间中进行推理和排除,这个过程与打字式输入完全不同:每个字母只能猜一次,猜过的字母从"可用选项"变为"已排除项"。

本文用 ArkUI 从零构建一个英文猜词游戏,包含虚拟键盘输入、6 条命的生命值系统、中文提示和中文字库轮换机制。所有交互通过点击屏幕上的 26 个字母按钮完成——无需系统输入法,操作流畅一致。

阅读完本文,你将能够:

  • 用虚拟键盘替代系统输入法处理单字母输入
  • 实现"已猜字母"集合的追踪与排除
  • 用词池洗牌机制避免题目重复
  • 用三色反馈区分"未猜/猜对/猜错"三种字母状态
  • 实现生命值递减与游戏结束的条件判断

二、游戏设计

2.1 规则与目标

猜词游戏的规则极为简洁:

  • 系统从词库中随机选一个英文单词,展示中文提示
  • 玩家每次点击虚拟键盘上的一个字母
  • 如果该字母在单词中,所有出现位置都会被揭开
  • 如果该字母不在单词中,损失一条命(共 6 条)
  • 在 6 条命耗尽前揭开所有字母即胜利
  • 每个字母只能猜一次,重复猜测无效

2.2 词库设计

词库由 15 个英文单词及其对应的中文提示组成:

interface WordEntry {
  word: string;
  hint: string;
}

const WORD_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 中"每次随机选一个"的方式不同,猜词游戏使用了词池轮换机制:

private wordPool: WordEntry[] = [];
private poolIdx: number = 0;

shufflePool(): void {
  this.wordPool = [];
  for (let i = 0; i < WORD_BANK.length; i++) {
    this.wordPool.push(WORD_BANK[i]);
  }
  // Fisher-Yates 洗牌
  for (let i = this.wordPool.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    const tmp = this.wordPool[i];
    this.wordPool[i] = this.wordPool[j];
    this.wordPool[j] = tmp;
  }
  this.poolIdx = 0;
}

newGame(): void {
  if (this.poolIdx >= this.wordPool.length) {
    this.shufflePool();  // 词池耗尽,重新洗牌
  }
  const entry = this.wordPool[this.poolIdx];
  this.poolIdx++;
  this.targetWord = entry.word;
  this.hint = entry.hint;
  // ...
}

这个机制的运作方式:

  1. 首次进入页面时,wordPool 为空,poolIdx 为 0
  2. newGame() 检测到 poolIdx >= 0(初始空数组长度为 0),触发 shufflePool()
  3. shufflePool()WORD_BANK 的所有条目复制到 wordPool,然后用 Fisher-Yates 洗牌打乱顺序,将 poolIdx 重置为 0
  4. 此后每调用一次 newGame()poolIdx 递增 1,取出词池的下一个单词
  5. poolIdx 到达词池末尾时(第 16 次换题),shufflePool() 重新洗牌,开始新一轮循环

这种机制保证了两点:

  • 不重复:在词池的一轮循环中(15 个单词),同一个单词不会出现两次
  • 随机顺序:每一轮的单词顺序都是重新随机排列的,玩家无法预测下一个单词是什么

15 个单词的池子对于猜词游戏来说大小适中——太小(如 5 个)会让玩家很快看到重复题目,太大(如 50 个)则初始加载无意义。15 个单词需要约 3-5 分钟完成一轮,是合理的一次游戏时长。

2.4 交互流程

一局游戏的交互流程:

  1. 开始:系统从词池中取出一个单词,展示中文提示和 26 个字母的虚拟键盘
  2. 猜测:玩家点击一个字母 → 判断是否在单词中 → 更新对应位置的显示
  3. 猜对:单词中该字母的每个出现位置都被揭开
  4. 猜错:生命值减 1,错误字母显示在"错误"列表中
  5. 胜利:所有字母被揭开 → 显示绿色通关横幅
  6. 失败:6 条命耗尽 → 显示红色失败横幅,揭晓答案
  7. 换题:点击"换一题"按钮,从词池中取出下一个单词
    在这里插入图片描述

三、核心数据结构

3.1 已猜字母的表示

猜词游戏的核心状态是"已经猜过哪些字母"。使用一个字符串 guessedLetters 来存储:

@State guessedLetters: string = '';

每猜一个字母,就将其追加到字符串末尾:

this.guessedLetters = this.guessedLetters + ch;

这种设计有三个好处:

  1. 去重检测简单this.guessedLetters.indexOf(ch) !== -1 即可判断是否已猜过
  2. 遍历方便:猜测字母、错误字母都可以通过遍历字符串获得
  3. 序列化简单:字符串可以直接用于调试日志,不需要额外的格式化

为什么不使用数组 string[]?因为字符串的 indexOf 方法和 + 拼接操作在 ArkTS 中的行为更可预测,避免了数组不可变更新时可能遇到的引用问题。而且猜词游戏最多只猜 26 次(26 个字母),26 个字符的字符串拼接性能完全不是瓶颈。

3.2 单词显示

displayWord() 方法构建当前单词的显示字符串:

displayWord(): string {
  let result = '';
  for (let i = 0; i < this.targetWord.length; i++) {
    const ch = this.targetWord[i];
    if (this.guessedLetters.indexOf(ch) !== -1 || this.gameOver) {
      result = result + ch + ' ';
    } else {
      result = result + '_ ';
    }
  }
  return result;
}

两个分支:

  • 已猜中或游戏结束:显示字母本身 + 空格(如 H A R M O N Y
  • 未猜中:显示下划线 + 空格(如 _ _ _ _ _ _ _

字母之间用空格分隔,一是在视觉上形成清晰的字母边界,二是等宽字体下每个字母/下划线占据相同宽度,形成整齐的网格感。

游戏结束时(this.gameOver)也显示完整单词——这样玩家在失败后可以立即看到正确答案,而不是只看到部分揭开的字母。

3.3 错误字母

wrongLetters() 方法从所有已猜字母中筛选出不在目标单词中的字母:

wrongLetters(): string {
  let result = '';
  for (let i = 0; i < this.guessedLetters.length; i++) {
    const ch = this.guessedLetters[i];
    if (this.targetWord.indexOf(ch) === -1) {
      result = result + ch + ' ';
    }
  }
  return result;
}

这个方法有两个作用:

  1. 视觉提示:让玩家快速看到自己已经猜过哪些无效字母,避免重复猜测
  2. 进度指示:错误字母列表的长度直观反映了剩余生命值的消耗情况

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.guessedLetters = this.guessedLetters + ch;

  if (this.targetWord.indexOf(ch) === -1) {
    // 不在单词中 → 生命-1
    this.wrongCount++;
    if (this.wrongCount >= MAX_LIVES) {
      this.gameOver = true;
    }
  } else {
    // 在单词中 → 检查是否全猜对
    let allGuessed = true;
    for (let i = 0; i < this.targetWord.length; i++) {
      if (this.guessedLetters.indexOf(this.targetWord[i]) === -1) {
        allGuessed = false;
        break;
      }
    }
    if (allGuessed) {
      this.gameWon = true;
    }
  }
}

整个方法分为三个阶段:

阶段一:守卫条件(两道防线)

  • 游戏已结束或已胜利 → 直接返回,不再处理任何输入
  • 字母已经猜过 → 直接返回,防止重复扣命

这两个守卫条件共同构成了"一次性字母"规则——每个字母在整个游戏过程中只能被点击一次。这要求在 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 中(即 allGuessedtrue),设置 this.gameWon = true

这里有一个不明显的细节:通关检测只在猜对字母时执行。因为猜错字母不会揭开任何字母,所以"是否全猜对"的状态不会因为猜错而改变。只在猜对时检测避免了不必要的遍历。

4.2 生命值系统

const MAX_LIVES: number = 6;

livesDisplay(): string {
  let result = '';
  for (let i = 0; i < MAX_LIVES; i++) {
    if (i < MAX_LIVES - this.wrongCount) {
      result = result + '❤️';
    } else {
      result = result + '🖤';
    }
  }
  return result;
}

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';
}

结果横幅使用条件渲染——只有 gameOvergameWontrue 时才显示:

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.Wrap:26 个按钮在水平方向排列不下时自动换行,适应不同屏幕宽度
  • justifyContent: FlexAlign.Center:每一行的按钮居中排列
  • 每个按钮 34×34 vp:足够大的点击区域(手指轻松点击),同时 26 个按钮在 360vp 宽的屏幕上约 7-8 个一行,形成 4 行键盘
  • BorderRadius.SM(4vp 圆角):比默认的方形更友好,但又不像圆形按钮那样浪费空间
  • 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.FULL(9999vp)实现胶囊形,搭配紫色渐变阴影(#667eea44,28% 透明度)形成微妙的悬浮感。点击后调用 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键虚拟键盘 Flex+ForEach
│   └── 换题按钮
└── 生命周期
    └── aboutToAppear() — 调用 newGame()

七、与前面 Demo 的对比

猜词游戏在技术层面上与本系列的前几个游戏 Demo 形成了清晰的对比:

7.1 输入方式

前面 Demo 的输入方式各不相同:

  • 计算器/秒表:按钮操作确定性的计算/计时逻辑
  • 待办清单/日记本:系统输入法文本输入
  • 井字棋/华容道:棋盘格子点击(空间操作)
  • 记忆翻牌:卡片翻转配对(记忆匹配)

猜词游戏的输入方式最独特:26 个字母按钮替代了系统输入法。这种设计的优势在于:

  • 操作一致:所有交互都是"点击按钮",不需要切换输入法
  • 视觉反馈:每个字母按钮的状态(未猜/已猜对/已猜错)直接可见
  • 字母去重:已经猜过的字母视觉上不可再点(不需要额外检查)

7.2 状态表示

猜词游戏的状态表示比井字棋(9 格 3 状态)和华容道(16 格 16 值)更简单:

  • primary 状态guessedLetters 字符串(26 个字母的子集)
  • 派生状态:单词显示、错误列表、生命值、胜负判断——全部从 guessedLetters 推导

这种"一个主状态 + 多个派生视图"的架构在 ArkUI 中非常自然——@State 变量变化自动触发所有依赖它的 TextColumn 重新渲染。不存在"同步多个状态"的复杂度。

7.3 难度调节

猜词游戏的难度由两个隐含因素决定:

  1. 单词长度:7-10 个字母的单词(HAMONY 7 个,JAVASCRIPT 10 个)是理想长度。太短(3-4 个字母)容易猜中,缺乏挑战;太长(12+ 个字母)需要猜的字母太多,容错率不足
  2. 提示精确度:"华为自研操作系统"缩小了搜索范围——知道这是操作系统名称,有助于排除大量无关单词

这种"提示引导 + 字母推理"的难度曲线比纯随机猜测要平滑——玩家在早期利用提示缩小范围,在后期利用已揭开的字母推断剩余字母,整个过程从"知识检索"过渡到"模式匹配"。

八、总结

本文从零构建了一个英文猜词游戏。与井字棋(策略博弈)、记忆翻牌(记忆力挑战)和华容道(空间推理)不同,猜词游戏的核心是字符串匹配与集合推理——每次猜测在 26 个字母的集合中进行排除,逐步缩小候选空间。从技术角度看,它也是虚拟键盘输入、三色状态反馈和词池轮换机制的完整示例。

核心要点回顾:

  1. 虚拟键盘替代系统输入法:26 个 Text 按钮使用 Flex({ wrap: FlexWrap.Wrap }) 布局,A-Z 字母顺序排列,每个按钮 34×34vp。所有交互通过点击完成,操作一致且视觉反馈直接。

  2. 字符串表示集合guessedLetters 是一个字符串,存储所有已猜字母。indexOf() 检测是否已猜过(去重)、是否在单词中(对错判断)。字符串的 + 拼接操作在 ArkTS 中行为稳定,26 个字符级的性能完全无问题。

  3. 三色反馈状态机:每个字母按钮有三种视觉状态——未猜(浅灰蓝底 + 深色字)、猜对(浅绿底 + 绿色字)、猜错(浅灰底 + 灰色字)。三种状态通过 letterBg()letterColor() 方法从 guessedLetterstargetWord 推导,没有额外的状态变量。

  4. 词池轮换避免重复:使用 Fisher-Yates 洗牌打乱 15 个单词的顺序,顺序取用。一轮 15 题中不会出现重复单词,词池耗尽后重新洗牌开始新一轮。这比"每次随机选"更能保证玩家体验的多样性。

  5. 通关条件判断:只在猜对字母时检测通关(遍历目标单词的每个字符,检查是否都在 guessedLetters 中)。猜错字母不可能改变"是否全猜对"的状态,避免不必要的遍历。

  6. 派生视图模式displayWord()wrongLetters()livesDisplay() 都是纯粹的派生方法——它们只读取 @State 变量,不修改任何状态。当 guessedLetters 变化时,所有使用它的视图自动重新渲染。这是 ArkUI 声明式 UI 的核心优势。

猜词游戏是一款"规则简单、策略丰富"的经典游戏。这个 260 行的 ArkUI 实现抓住了它的核心乐趣:虚拟键盘的触感反馈、猜对字母时的豁然开朗、以及最后几个字母揭晓前的紧张感。它是本系列第四篇游戏类文章,也是字符串集合操作和虚拟键盘交互的完整示例。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐