在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

目录

  1. 海龟汤是什么
  2. 项目背景与设计目标
  3. 产品需求分析
  4. 技术架构概览
  5. 数据层:15 个谜题的建模实践
  6. UI 架构:从列表到游戏的页面流转
  7. 游戏交互设计:主持人与玩家的角色分工
  8. @Builder 的语法陷阱与避坑指南
  9. 状态管理策略
  10. 视觉设计:暗黑哥特风格
  11. 编译构建与部署
  12. 踩坑全记录
  13. 优化方向与后续迭代
  14. 总结

一、海龟汤是什么

海龟汤,英文原名是 Lateral Thinking Puzzle(水平思考谜题),是一种在东亚地区广泛流行的推理游戏。它的核心规则极其简单:

  • 主持人:知道整个故事的来龙去脉(汤底)
  • 玩家:只知道故事的表面现象(汤面)
  • 互动方式:玩家问「是 / 否」类问题,主持人作答
  • 目标:玩家通过问题拼凑出完整的真相

这个游戏的名字来源于一个经典谜题:一个人去餐馆吃了一道叫"海龟汤"的菜,吃完后问服务员"这真的是海龟汤吗?“服务员回答"不是”。然后这个人就自杀了——为什么?这个谜题的答案出人意料又令人唏嘘,也奠定了这类游戏的基调:表面平静,暗流汹涌。

海龟汤的魅力在于:

  1. 门槛极低:不需要任何道具,一张嘴就能玩
  2. 脑洞大开:答案往往颠覆常识,需要打破思维定式
  3. 社交属性强:一群人围在一起七嘴八舌地提问,气氛热烈
  4. 短小精悍:一个谜题玩 5-15 分钟,适合聚会的碎片时间

作为一个开发者,我意识到这个游戏天然适合做成 App——主持人需要偷偷看答案、控制提示的节奏、记录玩家问了多少问题,这些都可以用数字化方式做得比纸牌更好。


二、项目背景与设计目标

2.1 为什么要做这个 App

市面上虽然有一些海龟汤 App,但存在几个普遍的痛点:

  1. 广告太多:看答案前先看 30 秒广告,体验割裂
  2. 谜题质量参差不齐:很多谜题的答案是强行反转,逻辑不严谨
  3. 缺少主持模式:大部分 App 只提供单人推理(AI 作为主持人),但海龟汤的真正乐趣在于一群人一起玩
  4. UI 过于复杂:动画花哨、按钮分散,实际游戏时反而不方便操作

基于这些痛点,我设定了以下设计目标:

目标 优先级 说明
零广告 P0 纯粹的游戏体验,不插入任何商业广告
优质谜题 P0 收录经过验证的经典谜题,每个都经过逻辑审查
主持人优先 P0 App 的第一角色是「主持人的工具」,其次是「玩家的题库」
极简操作 P1 游戏过程中所有操作不超过两次点击
本地存储 P1 所有数据本地存储,无需联网

2.2 目标用户画像

这个 App 的核心用户是:

“聚会上负责活跃气氛的那个人。”

他们通常在朋友聚会、家庭聚餐、公司团建等场景中担任"气氛组"。他们需要的是:

  • 快速找到高质量的谜题(不需要自己现编)
  • 偷偷看答案不被发现(UI 设计要保证主持人的隐私)
  • 方便地记录已回答的问题数(防止玩家抵赖)
  • 在适当时机给出提示(控制游戏节奏)

三、产品需求分析

3.1 用户故事

作为用户,我希望能:

作为主持人,我能在 10 秒内选好一个谜题并开始游戏。
作为主持人,我能安全地看到答案而不被玩家发现。
作为主持人,我能在玩家提问时快速给出是/否/无关的回答。
作为主持人,我能在适当时机给出提示。
作为主持人,我能记录玩家问了多少问题。
作为玩家,我能看到清晰的谜面,从中寻找线索。
作为玩家,我能在猜出答案后立即看到完整的真相。

3.2 页面流转设计

整个 App 只有两个主要的视图状态:

首页(谜题列表)
  │
  ├── 点击「开始游戏」──→ 游戏页(未看答案)
  │                           ├── 偷看汤底 → 确认弹窗 → 显示汤底
  │                           ├── 揭晓提示 → 逐条增加
  │                           ├── 回答问题 → 计数器 +1
  │                           └── 猜中 → 自动展示答案
  │
  └── 点击「浏览」─────→ 游戏页(直接显示答案,预览模式)
  │
  任意页面 ───→ 点击返回 → 首页

3.3 状态定义

每个游戏会话需要维护以下状态:

currentSoup: TurtleSoup | null   // 当前谜题
showAnswer: boolean              // 是否已看/展示汤底
currentHintIndex: number         // 当前已揭晓的提示索引(-1=未揭晓)
questionCount: number            // 玩家已问问题数
showAnswerWarning: boolean       // 是否显示"确认偷看"弹窗
isSolved: boolean                // 玩家是否已猜中

四、技术架构概览

4.1 整体架构

App 采用单页面 + 状态驱动的架构模式:

┌──────────────────────────────────────┐
│            Index.ets                  │
│  ┌────────────────────────────────┐   │
│  │  @State currentPage: 'list'    │   │
│  │        │  'game'                  │   │
│  │        ▼                          │   │
│  │  ┌─────────┐  ┌──────────────┐  │   │
│  │  │ 列表页   │  │  游戏页      │  │   │
│  │  │ - 卡片列表│  │ - 汤面      │  │   │
│  │  │ - 难度标签│  │ - 主持区    │  │   │
│  │  │ - 预览   │  │ - 底部按钮  │  │   │
│  │  └─────────┘  └──────────────┘  │   │
│  └────────────────────────────────┘   │
│                     ↑                 │
│            ┌────────┴────────┐        │
│            │ TurtleSoupData  │        │
│            │ (15 个谜题)     │        │
│            └─────────────────┘        │
└──────────────────────────────────────┘

4.2 为什么选择单页面而非路由

这个 App 只有两个"页面"——列表和游戏。如果使用 router 路由:

// router 方案(不采用)
import { router } from '@kit.ArkUI';
router.pushUrl({ url: 'pages/GamePage' });

需要额外注册页面、处理参数传递、管理页面栈。对于只有两个页面的 App,这完全是不必要的复杂度。

使用状态驱动方案:

@State currentPage: string = 'list';

// 在 build() 中根据状态切换
if (this.currentPage === 'list') {
  this.buildListPage();
} else {
  this.buildGamePage();
}

优点:

  • 零额外文件:所有代码在一个 Component 中
  • 状态共享:游戏状态直接是 @State 成员变量,无需传参
  • 无页面栈管理:返回就是改个字符串

缺点:

  • 如果页面超过 5 个,代码会膨胀到不可维护
  • 但 2 个页面完全在可接受范围内

五、数据层:15 个谜题的建模实践

5.1 数据结构设计

每个海龟汤谜题包含五个字段:

export interface TurtleSoup {
  id: number;              // 唯一编号
  title: string;           // 谜题标题(如"企鹅肉")
  difficulty: '简单' | '中等' | '困难' | '烧脑';  // 难度等级
  surface: string;         // 汤面 — 展示给玩家的故事
  answer: string;          // 汤底 — 完整的真相
  hints: string[];         // 逐步提示(3 条)
}

5.2 难度分级标准

在设计谜题集合时,我对每个谜题进行了难度评级:

难度 评判标准 示例 数量
简单 答案在 5 个问题内可猜中,思路直接 电梯里的男人 2
中等 需要 8-12 个问题,有点小转折 潜水艇、盲人与狗 4
困难 需要 15+ 个问题,答案令人意想不到 企鹅肉、音乐家之死 5
烧脑 需要打破固有思维定式,答案反直觉 电梯游戏、山顶木屋 4

5.3 谜题筛选标准

从几十个候选谜题中精选出 15 个,遵循以下标准:

  1. 逻辑自洽:答案不能有逻辑漏洞,所有细节必须能闭环
  2. 适度反转:反转是必须的,但不能为了反转而强行不合理
  3. 文化适配:选择中国玩家熟悉的场景和设定
  4. 多样性:涵盖情感、悬疑、科幻、日常等多种类型

例如,"电梯里的男人"这个谜题,虽然简单,但它完美体现了海龟汤的核心趣味——答案出人意料但合情合理。而"山顶木屋"更复杂,涉及到雪地追踪、心理绝望等元素。

5.4 数据存储考量

当前所有数据硬编码在 TurtleSoupData.ets 中,文件大小约 16KB。对于 15 个谜题来说这是合理的。但如果扩展到 50+ 个谜题,建议迁移到 JSON 资源文件:

// resources/rawfile/puzzles.json
{
  "puzzles": [
    {
      "id": 1,
      "title": "企鹅肉",
      "surface": "...",
      "answer": "...",
      "hints": ["...", "...", "..."]
    }
  ]
}

然后用 getContext().resourceManager.getRawFileContent() 读取。这样做的好处是:

  • 数据与代码分离,修改谜题不需要重新编译
  • 方便后续添加云端同步功能
  • 数据文件可以单独测试和验证

六、UI 架构:从列表到游戏的页面流转

6.1 首页:谜题卡片列表

首页使用卡片式布局展示所有谜题。每张卡片包含:

┌─────────────────────────────────────┐
│  [#困难]                     #1    │
│                                      │
│  企鹅肉                              │
│  一个人在南极旅行,去餐馆吃饭时点   │
│  了一道"南极烤企鹅肉"……            │
│                                      │
│  [🎮 开始游戏]        [📖 浏览]     │
└─────────────────────────────────────┘

实现要点:

@Builder
buildSoupCard(soup: TurtleSoup) {
  Column() {
    // 难度标签(带颜色)
    Text(soup.difficulty)
      .fontColor(this.getDiffColor(soup.difficulty))
      .backgroundColor(this.getDiffBg(soup.difficulty))

    // 标题
    Text(soup.title).fontSize(20).fontWeight(FontWeight.Bold)

    // 预览摘要(截取前 60 个字)
    Text(soup.surface.substring(0, 60) + '……')
      .maxLines(2)
      .textOverflow({ overflow: TextOverflow.Ellipsis })

    // 两个操作按钮
    Button('🎮 开始游戏')   // → 进入游戏,答案隐藏
    Button('📖 浏览')       // → 进入游戏,直接展示答案
  }
}

两个按钮的设计区分了两种使用场景:

  • 开始游戏:用户是主持人,需要先偷偷看答案再主持
  • 浏览:用户是学习者,直接看答案和解析

6.2 游戏页:主持人的控制台

游戏页是 App 的核心界面,分为三个区域:

┌─────────────────────────────────────┐
│ ← 返回    #1 企鹅肉        困难    │  ← 顶部导航
├─────────────────────────────────────┤
│                                     │
│  📜 汤面(展示给玩家)              │
│  ┌─────────────────────────────┐   │
│  │ 一个人在南极旅行……          │   │
│  └─────────────────────────────┘   │
│                                     │
│  🎭 主持区(仅主持人可见)          │
│  ┌─────────────────────────────┐   │
│  │ [👁️ 偷看汤底]  [💡 提示]   │   │
│  └─────────────────────────────┘   │
│                                     │
│  问题数:3    提示:1/3             │  ← 状态栏
│                                     │
├─────────────────────────────────────┤
│ [✅是] [❌否] [➖无关]  [🏆猜中]  │  ← 底部操作栏
└─────────────────────────────────────┘

这个布局遵循一个核心设计原则:信息层级从上到下递减

  • 顶部:最重要的信息——汤面(所有玩家和主持人共同关注的焦点)
  • 中部:主持人专属操作区(需要一定的隐私保护)
  • 底部:快速操作按钮(游戏过程中最常点击的区域)

七、游戏交互设计:主持人与玩家的角色分工

7.1 主持人的工作流

一个典型的主持流程:

  1. 选谜题:在首页浏览卡片,选择一个难度合适的谜题
  2. 偷看汤底:进入游戏页,点击"偷看汤底",弹出确认框
  3. 读汤面:将汤面内容读给所有玩家听(或传阅手机)
  4. 回答问题:玩家提问,主持人点击"是/否/无关"
  5. 给予提示:玩家卡壳时,点击"提示"揭晓一条线索
  6. 判定胜负:有人猜中时,点击"猜中",展示完整答案

7.2 安全偷看机制

这是整个 App 最关键的细节设计。如果主持人偷看答案时被玩家瞥见,游戏就直接毁了。

解决方案是一个双层确认机制

// 第一层:按钮文案明确提示
Button('👁️ 偷看汤底')

// 第二层:点击后弹出确认弹窗
if (this.showAnswerWarning) {
  Column() {
    Text('⚠️ 确认偷看汤底?')
    Text('汤底是整个谜题的答案。偷看后你将无法再以"未知"的身份参与游戏。')
    Button('取消')     // 关闭弹窗
    Button('确认偷看')  // 展示答案
  }
  .border({ width: 1, color: '#E8C87A' }) // 金色边框强调
}

这个设计有几个巧妙的点:

  • 弹窗的文案提醒主持人"你正在做什么",防止误触
  • 金色边框让弹窗在视觉上与其他内容区分
  • 弹窗本身也起到了"遮挡"作用——即使玩家瞥到屏幕,看到的也只是弹窗而不是答案

7.3 问题计数器的社交价值

问题计数器看似是一个简单的数字,但在实际游戏中有重要的社交功能:

Text('问题数:' + this.questionCount)

它的价值在于:

  • 制造压力:玩家知道自己的问题数被记录,会更谨慎地提问
  • 成就系统:猜中时问题数越少,成就感越强
  • 复盘参考:游戏结束后可以回顾「用了 XX 个问题猜中的」

在一些海龟汤的变体规则中,甚至有限制问题数的玩法(如「20 个问题之内猜中」),计数器为这种玩法提供了技术支持。

7.4 提示的分级解锁

每个谜题预设了 3 条提示,从模糊到明确:

// 以"企鹅肉"为例
hints: [
  '重点在于"味道不同"——为什么南极的企鹅肉和城市里的味道不一样?',
  '想想他"吃到的"真的是企鹅吗?当时只有他和朋友两个人。',
  '朋友为什么一直没回来?那只"企鹅"是怎么出现的?',
]

第一条提示给出思考方向,第二条缩小范围,第三条几乎直接点破真相。主持人根据玩家的进展决定是否解锁下一条,保持了游戏的节奏控制权。


八、@Builder 的语法陷阱与避坑指南

8.1 问题背景

在开发过程中,我遇到了 ArkTS @Builder 装饰器的若干语法限制。这是 ArkTS 与标准 TypeScript 差异最大的地方,也最容易踩坑。

8.2 @Builder 中的禁止操作

以下操作在 @Builder 方法是不允许的:

@Builder
buildGameContent() {
  // ❌ 错误 1:不能使用 const
  const soup = this.currentSoup;
  
  // ❌ 错误 2:不能使用 let
  let count = this.questionCount;
  
  // ❌ 错误 3:不能使用 return
  if (this.currentSoup === null) {
    return;
  }

  // ❌ 错误 4:不能直接调用非 Builder 方法并赋值
  const hint = this.getHintText(); // 错误!

  // ✅ 正确:可以在组件属性中调用方法
  Text(this.getHintText()) // 正确!
  
  // ✅ 正确:可以调用其他 @Builder 方法
  this.buildOtherComponent()
}

8.3 解决方案

有三种解决策略:

策略一:内联表达式(最简单)

不使用变量,直接在使用处写完整表达式:

@Builder
buildGameContent() {
  // 不声明变量,直接使用 this.currentSoup!
  Text('#' + (this.currentSoup as TurtleSoup).id)
}

缺点是重复代码多,可读性差。

策略二:抽取为普通方法

将计算逻辑提取到普通方法中,然后在 @Builder 中调用:

// 普通方法(非 @Builder)
getHintText(soup: TurtleSoup): string {
  return soup.hints.slice(0, this.currentHintIndex + 1).join('\n\n');
}

// @Builder 中调用
@Builder
buildHintCard(soup: TurtleSoup) {
  this.buildContentCard(this.getHintText(soup), '#8B7E66')
}

这是最推荐的方案——保持 @Builder 干净,逻辑在普通方法中。

策略三:条件判断放在外层

不要试图在 @Builder 内部做复杂的条件分支,而是在调用处判断:

// ✅ 推荐:在 build() 中判断
if (this.currentSoup === null) {
  Text('加载失败')
} else {
  this.buildGameContent()
}

// ❌ 不推荐:在 @Builder 内部 return

8.4 原因分析

为什么 ArkTS 要对 @Builder 做这些限制?

根本原因是方舟编译器的编译模型。@Builder 方法会被编译为独立的渲染函数,它们的执行环境与普通方法不同。编译器需要对 @Builder 的代码做额外的静态分析以优化渲染性能。允许 constletreturn 等操作会使这种分析复杂化。

这是一种为了性能而牺牲语法灵活性的设计取舍。在最新的 API 版本中,部分限制已经有所放宽(如支持 if/else),但 let/const/return 仍然被禁止。


九、状态管理策略

9.1 状态设计原则

这个 App 的状态管理遵循一个简单原则:

每个 @State 变量对应一个独立的用户操作。

用户操作 对应的 @State 变化范围
切换页面 currentPage 整个内容区域重建
翻看卡片 无(列表是静态的)
点击开始游戏 currentSoup + currentPage 从列表切换到游戏
偷看答案 showAnswer + showAnswerWarning 答案区域显示
回答是/否 questionCount 计数器数字更新
揭晓提示 currentHintIndex 提示区域追加
猜中 isSolved + showAnswer 展示答案

9.2 跨方法的状态共享

所有 @State 变量都在 @Component struct Index 中声明,@Builder 方法和普通方法共享同一份 this 上下文:

@Component
struct Index {
  @State currentPage: string = 'list';
  @State showAnswer: boolean = false;
  @State questionCount: number = 0;

  // @Builder 可以直接访问 @State
  @Builder
  buildGameStatus() {
    Text('问题数:' + this.questionCount)  // 直接读取
  }

  // 普通方法也可以读写
  resetGameState(): void {
    this.questionCount = 0;  // 直接修改
  }
}

这是单页面架构的最大优势——不需要通过参数传递状态,没有 Prop drilling 的问题。

9.3 状态重置的时机

每次开始新游戏或返回列表时,都需要重置游戏状态:

resetGameState(): void {
  this.showAnswer = false;
  this.currentHintIndex = -1;
  this.questionCount = 0;
  this.showAnswerWarning = false;
  this.isSolved = false;
}

这个方法的调用位置需要特别注意:

  1. 点击"开始游戏"时 → 重置(确保新游戏是干净状态)
  2. 点击"返回"时 → 重置(防止下次进入时残留旧状态)
  3. "偷看确认"弹窗的取消 → 不重置(只是关闭弹窗)

十、视觉设计:暗黑哥特风格

10.1 设计理念

海龟汤的故事通常带有悬疑、恐怖或悲伤的色彩(企鹅肉、音乐家之死、黑夜敲门声……)。所以视觉设计采用了暗黑哥特风格——深色背景、暖色文字、低饱和度。

这与上一个 App「孔雀东南飞」的古风主题形成鲜明对比,但深色系的思路是一脉相承的。

10.2 色彩系统

色值 用途 设计意图
#0D0D1A 主背景 极深的蓝黑,像深夜
#1A1520 卡片背景 比主背景略浅,形成层级
#2A2018 边框 深棕色细边框,不抢眼
#F0E6D3 主文字 米白色,温暖不刺眼
#C9A87C 金色强调 汤底、重要按钮
#8B7E66 次级文字 提示、标签、说明
#6B5E4A 弱化文字 统计信息、辅助内容

10.3 难度标签的颜色编码

四种难度使用不同的颜色,让用户一目了然:

getDiffColor(difficulty: string): ResourceColor {
  switch (difficulty) {
    case '简单': return '#7BCF8C';  // 绿色
    case '中等': return '#E8C87A';  // 黄色
    case '困难': return '#E87A7A';  // 红色
    case '烧脑': return '#C97BE8';  // 紫色
  }
}

这种颜色映射借鉴了游戏行业的难度分级惯例——绿色代表简单,红色代表困难,紫色代表"高难度/稀有"。

10.4 卡片设计

列表页的每张卡片都有 1px 的深色边框和 16px 的圆角:

.backgroundColor('#1A1520')
.borderRadius(16)
.border({ width: 1, color: '#2A2018' })

这种设计在浅色背景上可能显得"脏",但在深色背景上反而呈现出一种精致的质感——像一张被烛光照亮的羊皮纸。


十一、编译构建与部署

11.1 构建配置

项目使用 hvigor 6.1.1 构建工具,API 24 (SDK 6.1.1)。核心配置在 build-profile.json5 中:

{
  "products": [{
    "name": "default",
    "signingConfig": "default",
    "targetSdkVersion": "6.1.1(24)",
    "compatibleSdkVersion": "6.1.1(24)",
    "runtimeOS": "HarmonyOS"
  }]
}

entry 模块的 build-profile.json5 中,apiType 设置为 "stageMode",这是 API 24 强制要求的模型。

11.2 构建命令

hvigorw assembleHap --mode module -p product=default --no-daemon

完整构建流程:

PreBuild → CreateModuleInfo → MergeProfile → ProcessResource →
CompileResource → CompileArkTS → PackageHap → SignHap → BUILD SUCCESSFUL

其中 CompileArkTS 阶段耗时最长(约 3-4 秒),主要完成:

  1. 语法检查(类型检查、@Builder 规则检查)
  2. 方舟字节码编译
  3. 摇树优化(Tree Shaking)

11.3 构建产物

构建成功的产物位于:

entry/build/default/outputs/default/entry-default-unsigned.hap

HAP 文件大小约 1.8MB(未签名)。这个体积对于包含 15 个文本谜题的 App 来说是合理的——纯文本数据占用的空间极小,主要体积来自 ArkUI 运行时库的引用。


十二、踩坑全记录

12.1 坑一:字符串引号不匹配

现象:编译错误 “Unterminated string literal”

位置:TurtleSoupData.ets 第 59 行

原因:字符串以单引号 ' 开头,以双引号 " 结尾:

// ❌ 错误
'音乐会中发生了什么特别的事?"
// 单引号开头,双引号结尾 —— 不匹配

// ✅ 正确
'音乐会中发生了什么特别的事?'

教训:在写大量中文文本时,引号匹配是最容易犯的低级错误。建议在写完所有字符串后做一次全局的引号匹配检查。

12.2 坑二:@Builder 中的 let/const 限制

现象:编译错误 “Only UI component syntax can be written here”

位置:Index.ets 中所有 @Builder 方法

原因:在 @Builder 方法中使用了 letconst 声明变量。

解决方案:将计算逻辑抽取到普通方法中。

教训:@Builder 不是普通的函数。它的编译规则更接近 JSX——你只能在"视图构建"的语境中写代码。任何非 UI 的逻辑都应该放到普通方法中。

12.3 坑三:数组元素间缺少逗号

现象:编译错误 “‘,’ expected”

位置:TurtleSoupData.ets 第 60 行

原因:数组的两个元素之间缺少逗号:

// ❌ 错误
hints: [
  '第一条'
  '第二条',  // 第一行末尾缺少逗号
]

// ✅ 正确
hints: [
  '第一条',
  '第二条',
]

教训:在 TypeScript/ArkTS 中,数组元素之间的逗号不能省略(不像 JavaScript 中在某些情况下可以省略)。修改多个相邻字符串时要特别注意。

12.4 坑四:错误信息定位不准

ArkTS 编译器的错误信息有时会指向错误行号之后的若干行。这是因为编译器在发现错误后需要继续解析才能确定错误的精确范围。例如,缺少逗号的错误可能被报告在下一行,而不是实际缺少逗号的那一行。

应对策略:当错误行号看起来不对时,向上查找 2-3 行,通常能发现真正的问题。

12.5 坑五:@Builder 中的条件判断

在最初的版本中,我尝试在 @Builder 方法中这样写:

@Builder
buildSomething() {
  if (this.x) {
    // 一些 UI
  }
}

这其实是允许的——@Builder 中可以使用 if/else 进行条件渲染。但不允许的是在 if/else 之外写 returnletconst 等。

12.6 踩坑总结表

# 错误类型 表现 根因 修复方式
1 引号不匹配 Unterminated string '开头"结尾 统一引号
2 Builder 限制 Only UI component syntax let/const 在 @Builder 中 抽取到普通方法
3 缺少逗号 ‘,’ expected 数组元素间无逗号 加逗号
4 错误偏移 行号不准确 编译器错误传递 向上查 2-3 行
5 可为空引用 编译警告 currentSoup 可能为 null as TurtleSoup 断言
6 颜色字符串 无错误但无效 颜色值拼写错误 统一使用 7 位 Hex

十三、优化方向与后续迭代

13.1 短期优化(MVP 之后)

  1. 谜题收藏功能:用户可以收藏喜欢的谜题,方便复玩
  2. 计时器:记录每局游戏的用时,增加竞速玩法
  3. 已玩标记:已玩过的谜题在列表上显示标记,避免重复
  4. 搜索/筛选:按难度、类型、关键词筛选谜题

13.2 中期迭代

  1. 自定义谜题:用户自己编写海龟汤谜题,分享给朋友
  2. 多人联机:通过华为近距离通信(Nearby)实现同房间的联机游戏
  3. 语音支持:集成语音识别,玩家可以直接语音提问,主持人不用打字
  4. 云端题库:从云端下载新谜题,保持内容更新

13.3 长期愿景

将 App 发展为一个推理游戏平台,不仅支持海龟汤,还支持:

  • 剧本杀短篇
  • 密室逃脱文字版
  • 逻辑推理谜题

每个游戏类型共享同一套基础架构(数据模型 + 状态管理 + 主持模式),只需要替换数据内容和 UI 文案。

13.4 技术债务

当前版本遗留的技术债务:

  1. 硬编码数据:15 个谜题硬编码在 .ets 文件中。如果扩展到 50+,应迁移到 JSON 资源文件
  2. 单文件膨胀:Index.ets 约 530 行。如果继续增加功能,应考虑拆分为多个 Component
  3. 缺少测试:当前没有写单元测试。hypium 测试框架已经集成,但测试用例还未补上
  4. 访问性:没有添加 Accessibility 支持。Text 组件的 accessibilityText 属性未设置

十四、总结

14.1 项目数据

指标 数值
开发总工时 约 2.5 小时
源代码文件 2 个(数据 + 页面)
总代码行数 ~700 行
谜题数量 15 个
构建时间 ~10 秒
HAP 体积 ~1.8 MB

14.2 技术收获

  1. @Builder 的语法限制:这是 ArkTS 与标准 TypeScript 最大的差异点。理解了这些限制背后的编译器设计考量后,编码时就能避免犯错。

  2. 单页面架构的适用场景:对于 2-3 个页面的小型 App,状态驱动的单页面模式比 router 路由更简洁。但对于 5 个页面以上的 App,这种模式会变得难以维护。

  3. 数据与UI的分离:将 15 个谜题放在独立的 TurtleSoupData.ets 中,不仅在架构上是好的实践,也让 UI 层的修改不会影响数据,反之亦然。

14.3 写给 ArkTS 初学者的建议

如果你正在学习 ArkTS,这里有几个实用的建议:

  1. 不要把 ArkTS 当成 TypeScript 写。虽然语法相似,但 @Builder 的规则是独特的。先读懂编译器的错误信息,比看十篇教程都管用。

  2. 善用 @Builder 的参数传递。@Builder 方法可以接收参数,这是拆分 UI 的正确方式。

  3. 状态变量越少越好。每个 @State 都会增加框架的跟踪开销。能用计算属性(普通 getter)代替的就不要用 @State。

  4. 从简单的 App 开始。海龟汤和孔雀东南飞这类单页面 App 是学习 ArkTS 的最佳起点——不涉及复杂的路由、网络、动画,可以专注于理解声明式 UI 的核心概念。


附录A:谜题难度分布

难度 数量 占比
🟢 简单 2 13%
🟡 中等 4 27%
🔴 困难 5 33%
🟣 烧脑 4 27%

附录B:关键编译配置

// build-profile.json5 (项目根目录)
{
  app: {
    products: [{
      name: "default",
      targetSdkVersion: "6.1.1(24)",
      compatibleSdkVersion: "6.1.1(24)",
      runtimeOS: "HarmonyOS"
    }]
  }
}
// entry/build-profile.json5
{
  apiType: "stageMode",
  buildOption: { resOptions: { copyCodeResource: { enable: false } } }
}

附录C:项目文件结构

entry/src/main/ets/
├── models/
│   └── TurtleSoupData.ets   # 15 个谜题数据 + 类型定义
└── pages/
    └── Index.ets             # 主页面(列表 + 游戏)

本文涉及的完整源码可在 DevEco Studio 项目中查看(models/TurtleSoupData.ets + pages/Index.ets)。

构建环境:HarmonyOS API 24 (SDK 6.1.1),DevEco Studio 6.1.x,hvigor 6.1.1。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐