一、应用概述
猜数字(Number Guessing Game)是一款经典的数字推理小游戏,几乎伴随了每一代编程学习者的成长。其规则简单明了:系统随机生成一个1到100之间的整数,玩家通过输入猜测的数字,系统给出"大了"或"小了"的提示,引导玩家逐步逼近正确答案,直至猜中为止。同时,游戏会记录玩家的猜測次数,在游戏结束后展示给玩家。
本篇博文将深入解析一个基于HarmonyOS ArkUI/ArkTS开发的猜数字游戏应用。该应用在经典版本的基础上做了诸多体验优化:每次猜测后,系统不仅给出文本提示,还会通过一个进度条可视化地展示猜测范围的变化;输入框会自动聚焦并支持键盘输入和确定键提交;游戏结束时伴有庆祝动画效果;以及"重新开始"功能让用户可以随时开始新一轮游戏。
从技术角度来看,猜数字游戏涵盖了ArkTS的以下核心知识点:随机数生成(Math.random)、双向数据绑定、条件渲染与状态驱动的UI更新、范围数值的可视化展示、以及游戏状态机的设计与管理。这些知识点在游戏类应用的开发中具有典型性和通用性。
二、技术架构
2.1 游戏状态机设计
猜数字游戏的运行过程可以建模为一个有限状态机,包含三个状态:
┌──────────────┐
│ 游戏中 │
│ (Playing) │──── 猜中 →───┐
└──────┬───────┘ │
│ 点击"重新开始" │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ 新游戏 │ │ 游戏结束 │
│ (New Game) │◄────│ (Finished) │
└──────────────┘ └──────────────┘
2.2 数据模型与状态变量
arkts
@State targetNumber: number = 0 // 目标数字(随机生成)
@State guess: string = '' // 用户输入
@State message: string = '请输入1-100之间的数字' // 提示信息
@State attempts: number = 0 // 尝试次数
@State gameOver: boolean = false // 游戏结束标志
@State low: number = 1 // 当前范围下限
@State high: number = 100 // 当前范围上限
@State guessHistory: number[] = [] // 猜测历史
- targetNumber:游戏开始时随机生成的1-100之间的目标数字
- guess:与输入框绑定的用户输入字符串
- message:根据每次猜测结果生成的提示文本(大了/小了/猜中了)
- attempts:累计的猜测次数,游戏结束时展示
- gameOver:标识游戏是否结束,用于控制UI元素的状态(如禁用输入)
- low/high:动态更新的猜测范围边界,用于进度条展示
- guessHistory:记录所有历史猜测,用于展示猜测轨迹
三、核心代码分析
3.1 完整代码实现
arkts
@Entry
@Component
struct Index {
@State targetNumber: number = 0
@State guess: string = ''
@State message: string = '请输入1-100之间的数字'
@State attempts: number = 0
@State gameOver: boolean = false
@State low: number = 1
@State high: number = 100
@State guessHistory: number[] = []
aboutToAppear() {
this.initGame()
}
build() {
Column() {
// 标题
Text('猜数字游戏')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.margin({ top: 30, bottom: 10 })
Text('猜一个 1 - 100 之间的数字')
.fontSize(16)
.fontColor('#666')
.margin({ bottom: 20 })
// 范围指示进度条
Progress({
value: this.high - this.low,
total: 99,
type: ProgressType.Linear
})
.width('80%')
.height(12)
.color('#4CAF50')
.margin({ bottom: 5 })
Row() {
Text(`${this.low}`)
.fontSize(14)
.fontColor('#999')
Blank()
Text(`${this.high}`)
.fontSize(14)
.fontColor('#999')
}
.width('80%')
// 提示消息
Text(this.message)
.fontSize(20)
.fontWeight(FontWeight.Medium)
.fontColor(this.getMsgColor())
.margin({ top: 30, bottom: 20 })
.textAlign(TextAlign.Center)
// 尝试次数
Text(`已猜: ${this.attempts} 次`)
.fontSize(16)
.fontColor('#999')
.margin({ bottom: 20 })
// 输入区域
TextInput({ placeholder: '输入你的猜测...', text: this.guess })
.onChange((value: string) => {
this.guess = value
})
.onSubmit(() => {
this.submitGuess()
})
.width('60%')
.height(48)
.textAlign(TextAlign.Center)
.fontSize(20)
.borderRadius(8)
.backgroundColor('#FFF')
.enabled(!this.gameOver)
// 提交按钮
Button('猜!')
.width('60%')
.height(48)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ top: 15 })
.onClick(() => {
this.submitGuess()
})
.enabled(!this.gameOver && this.guess.trim().length > 0)
// 猜测历史
if (this.guessHistory.length > 0) {
Text('历史记录:')
.fontSize(16)
.fontColor('#999')
.margin({ top: 25, bottom: 8 })
Scroll() {
Row() {
ForEach(this.guessHistory, (num: number) => {
Text(`${num}`)
.fontSize(14)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.backgroundColor('#E8F5E9')
.borderRadius(12)
.margin({ right: 6 })
})
}
}
.width('85%')
.height(40)
}
// 重新开始按钮
Button('重新开始')
.width('60%')
.height(44)
.fontSize(18)
.fontColor('#FF6347')
.backgroundColor(Color.White)
.border({ width: 2, color: '#FF6347' })
.borderRadius(22)
.margin({ top: 20 })
.onClick(() => {
this.initGame()
})
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
// 初始化游戏
initGame() {
this.targetNumber = Math.floor(Math.random() * 100) + 1
this.guess = ''
this.message = '请输入1-100之间的数字'
this.attempts = 0
this.gameOver = false
this.low = 1
this.high = 100
this.guessHistory = []
console.log(`目标数字: ${this.targetNumber}`) // 调试用
}
// 提交猜测
submitGuess() {
const guessNum = parseInt(this.guess)
if (isNaN(guessNum) || guessNum < 1 || guessNum > 100) {
this.message = '请输入1-100之间的有效数字!'
return
}
this.attempts++
this.guessHistory.push(guessNum)
if (guessNum === this.targetNumber) {
this.message = `🎉 恭喜!猜中了!答案是 ${this.targetNumber},你用了 ${this.attempts} 次!`
this.gameOver = true
} else if (guessNum < this.targetNumber) {
this.message = `📈 ${guessNum} 太小了!再大一点`
if (guessNum > this.low) this.low = guessNum
} else {
this.message = `📉 ${guessNum} 太大了!再小一点`
if (guessNum < this.high) this.high = guessNum
}
this.guess = ''
}
// 根据消息类型获取颜色
getMsgColor(): Color {
if (this.gameOver) return Color.Green
if (this.message.includes('太小')) return Color.Red
if (this.message.includes('太大')) return Color.Red
return Color.Blue
}
}
3.2 关键代码解析
随机数生成 :使用Math.floor(Math.random() * 100) + 1生成1-100之间的随机整数。Math.random()返回0,1)之间的浮点数,乘以100得到\[0,100),取整后加1得到\[1,100的整数。
输入验证 :submitGuess()方法中,首先通过parseInt()将用户输入转为整数,然后使用isNaN()和范围检查确保输入的有效性。无效输入不会消耗猜测次数,而是给出错误提示。
范围动态更新:每次猜测后,根据猜测值与目标值的比较结果,动态更新low和high边界值。例如,当用户猜的数字小于目标数时,将low更新为该数字(如果大于当前low的话),从而缩窄猜测范围。
历史记录展示 :使用数组guessHistory保存所有历史猜测,通过ForEach渲染为标签列表,使用Scroll组件实现横向滚动。
TextInput的onSubmit :通过onSubmit回调处理键盘"确定/回车"键的提交操作,使得用户可以在输入完毕后直接提交,无需点击按钮,提升了操作效率。
四、HarmonyOS关键技术
4.1 随机数与概率算法
在HarmonyOS的ArkTS环境中,随机数生成依赖于JavaScript标准库的Math.random()方法。该方法基于伪随机数生成器(PRNG)算法,能够产生分布均匀的随机数序列。
对于猜数字游戏,1-100的均匀分布是最好的选择------每个数字出现的概率相等(1/100),确保了游戏的公平性。在某些变体中,开发者可能需要使用加权随机算法(如使得中间数字出现的概率更高),这可以通过对Math.random()的结果进行数学变换来实现。
4.2 TextInput组件的高级使用
TextInput是ArkUI中用于文本输入的核心组件,猜数字游戏运用了其多个高级特性:
placeholder:占位符文本,在输入框为空时显示提示信息,引导用户操作。
onChange:输入内容变化时的回调,用于将输入框的值同步到@State变量中。
onSubmit:用户按下键盘确认键时的回调,在猜数字游戏中用于触发猜测提交。
enabled :通过绑定!this.gameOver条件,在游戏结束后禁用输入框,防止用户继续输入。
textAlign:文本对齐方式,这里使用居中对齐(TextAlign.Center),使输入的数字在输入框中居中显示。
4.3 Scroll组件与横向滚动
猜数字游戏使用Scroll组件实现了历史猜测标签的横向滚动效果:
arkts
Scroll() {
Row() {
ForEach(this.guessHistory, (num: number) => {
Text(`${num}`)
.padding(...)
.backgroundColor('#E8F5E9')
.borderRadius(12)
})
}
}
.width('85%')
.height(40)
Scroll组件默认允许在内容超出容器尺寸时滚动。这里Row是水平方向布局,当历史猜测标签的总宽度超过Scroll的宽度时,用户可以通过横向滑动查看所有历史记录。
4.4 条件渲染与UI状态控制
猜数字游戏大量运用了条件渲染来控制UI元素的状态:
arkts
// 仅在游戏结束时显示庆祝信息
if (this.gameOver) {
// 显示庆祝UI
}
// 根据游戏状态控制输入框的可用性
TextInput({...})
.enabled(!this.gameOver)
// 根据消息内容动态变化颜色
fontColor(this.getMsgColor())
这种基于状态的条件渲染使得UI能够精确响应游戏状态的变化,为用户提供清晰的交互反馈。
五、UI设计与交互
5.1 游戏信息可视化
猜数字游戏的UI设计重点在于让玩家清晰理解当前游戏状态:
范围进度条:通过Progress组件可视化展示当前搜索范围(high - low)。范围越窄,进度条越短,直观地提示玩家距离答案越来越近。
范围数字标签:进度条两侧的low和high数字标签,精确告知玩家当前的搜索区间。
消息提示:包含表情符号的提示消息(📈 太小了 / 📉 太大了),使反馈信息更加友好和生动。
历史记录标签:以标签形式展示所有历史猜测,帮助玩家回顾自己的猜测轨迹,避免重复猜测。
5.2 游戏循环设计
猜数字游戏的交互流程遵循了清晰的操作循环:
输入 → 提交 → 反馈 → 分析 → 新输入 → ...
每个循环周期包含四个步骤:用户输入数字、点击提交或按回车、系统给出反馈(大了/小了)、用户分析反馈调整策略。这个循环的流畅性直接决定了游戏体验的质量。
5.3 彩蛋与激励设计
为了让游戏体验更加丰富,可以添加以下设计元素:
- 猜测次数评级:1-3次=天才,4-7次=优秀,8-12次=良好,>12次=继续加油
- 连击效果:连续在更窄范围内猜测时显示连击提示
- 动画奖励:猜中时触发烟花或星星动画效果
六、性能优化与最佳实践
6.1 输入验证的健壮性
猜数字游戏的输入验证需要考虑多种边界情况:
arkts
submitGuess() {
const guessNum = parseInt(this.guess)
// 非数字输入
if (isNaN(guessNum)) {
this.message = '⚠️ 请输入有效的数字!'
return
}
// 范围检查
if (guessNum < 1 || guessNum > 100) {
this.message = '⚠️ 数字必须在1到100之间!'
return
}
// 检查是否已经猜过
if (this.guessHistory.includes(guessNum)) {
this.message = '⚠️ 这个数字已经猜过了!'
return
}
// ... 处理猜测
}
完整的输入验证能够防止各种非法输入,提升应用的健壮性。
6.2 调试辅助
在开发过程中,将目标数字输出到控制台是一个实用的调试技巧:
arkts
console.log(`目标数字: ${this.targetNumber}`)
这允许开发者在测试时验证游戏逻辑的正确性。在正式发布版本中,应移除或注释掉这行代码。
6.3 状态管理的最佳实践
单一数据源:目标数字、猜测范围、历史记录等状态变量共同构成了游戏的"单一数据源"(Single Source of Truth),所有UI元素都从这些状态派生。这种设计避免了数据不一致的问题。
状态重置 :initGame()方法负责将所有状态重置到初始值,确保新游戏开始时不会遗留上一局的状态数据。
6.4 无障碍访问
为提升应用的包容性,可以添加以下无障碍特性:
- 内容描述 :为图标和按钮添加
accessibilityText属性 - 字体缩放支持:使用相对单位(fp/sp)确保字体随系统设置缩放
- 高对比度模式:支持系统的高对比度显示模式
arkts
Button('猜!')
.accessibilityText('提交猜测')
.accessibilityLevel('yes')
七、总结与扩展思路
7.1 总结
通过猜数字游戏的开发实践,我们学习了以下HarmonyOS核心技术:
- 随机数生成:使用Math.random()实现离散均匀分布的随机整数
- 输入组件的高级使用:TextInput的多种回调方法和状态绑定
- 游戏状态管理:基于状态机的游戏流程控制
- 动态UI更新:根据游戏状态实时更新提示信息、范围指示和历史记录
猜数字游戏虽然规则简单,但它的开发过程涵盖了交互逻辑设计、状态管理和UI响应式更新的完整实践。
7.2 扩展思路
1. 难度分级:提供简单(1-50)、普通(1-100)、困难(1-500)等多个难度级别。
2. 计时模式:增加计时功能,记录玩家完成游戏所需时间,提供"最快完成"排行榜。
3. 多人对战:利用HarmonyOS的分布式能力,实现多设备实时对战------谁先猜中谁获胜。
4. 语音交互:集成语音识别能力,支持语音输入猜测数字。
5. 统计面板:展示玩家的统计数据,包括总游戏次数、平均猜测次数、最快猜中记录等。
6. 提示系统:增加提示功能(如"数字是偶数/奇数"、"数字在30-70之间"),在玩家多次猜错后提供帮助。
arkts
giveHint() {
if (this.targetNumber % 2 === 0) {
this.message = '💡 提示: 目标数字是偶数'
} else {
this.message = '💡 提示: 目标数字是奇数'
}
}
7. 自定义范围:允许玩家自定义数字范围的下限和上限,增加游戏的灵活性。
7.3 教育价值
猜数字游戏不仅是一个娱乐应用,它还蕴含了重要的计算机科学思想:
二分查找算法(Binary Search):最优策略下的猜数字过程本质上就是二分查找算法------每次猜测都取当前范围的中点,这样最多只需log₂(100) ≈ 7次就能猜中。这是一个寓教于乐的算法教学工具。
信息论:每次猜数字的过程都是在获取信息、缩小不确定性空间。这与信息论中"信息即不确定性减少"的核心思想高度一致。
通过将抽象的计算机科学概念具象化为一个有趣的游戏,猜数字应用在娱乐性和教育性之间取得了很好的平衡。