HarmonyOS7应用《拼棋派对》实战:从四张数字牌实现24点游戏
打开《拼棋派对》的单人24点,屏幕上只有四张数字牌。选一张牌,点运算符,再选另一张牌,两张牌就会合成一个新数字。继续操作,直到四张牌合成24,提交后进入下一题。
这个玩法很适合用来练习 ArkTS 与 ArkUI:页面不复杂,却包含重复数字、状态更新、运算顺序、浮点精度和答题判定等实际问题。本篇以项目中的单人24点为例,把“四个数字能算出24”落实成一套可以操作、可以检查的游戏规则。
项目当前使用 DevEco Studio 26.0.0.851 与 API 26 Release SDK,主 App 页面采用 @Component、@State 的 V1 状态管理。本篇沿用这套实现;这些基础能力也可以用于较早版本的 HarmonyOS 工程。
读完本文,你可以理解并复用24点的牌槽模型、两牌合并与完成判定,知道重复数字和小数中间值该怎样处理。阅读代码前,建议先了解 ArkTS 的接口、数组与 ArkUI 基础状态管理。
本文目录
先看玩家怎样完成一道题
手机竖屏中,页面从上到下安排难度、答题状态、数字牌、表达式、运算符和工具按钮。数字牌负责操作,表达式区负责说明当前算到了哪里,玩家不需要依靠记忆还原每一步。
中级首题:四张数字牌、表达式与可用牌数量同时显示,底部提供运算和工具按钮。
以图中的 1、2、7、7 为例,可以这样操作:
- 选择第一张7,点击乘号,再选择另一张7,得到49。
- 选择49,点击减号,再选择1,得到48。
- 选择48,点击除号,再选择2,得到24。
- 点击提交,答对计分并进入下一题。
对应表达式是:
((7 × 7) - 1) ÷ 2 = 24
这里的两个7数值相同,却是两张不同的牌;减法与除法的先后顺序也必须保留。接下来先把这些规则放进数据模型。
给每张牌一个身份
如果只用 number[] 保存四个数字,页面能显示牌,却很难描述“这一张已经被用掉”“那一张是两步运算的结果”。项目为每个牌槽定义了四个字段,位于 model/GameModels.ets:
export interface TwentyFourSlot {
id: number;
value: number;
expr: string;
active: boolean;
}
id 区分牌槽,value 用于后续计算,expr 保存数字的来历,active 决定这张牌能否继续参与运算。
初始化时,common/GameLogic.ets 中的 createTwentyFourSlots 把题目数字转换为牌槽:
export function createTwentyFourSlots(cards: number[]): TwentyFourSlot[] {
const result: TwentyFourSlot[] = [];
for (let index = 0; index < cards.length; index++) {
result.push({
id: index,
value: cards[index],
expr: `${cards[index]}`,
active: true
});
}
return result;
}
对于 1、2、7、7,两个7分别属于槽位2和槽位3。选牌时记录槽位索引,就能准确判断玩家点的是同一张牌,还是另一张数值相同的牌。
这也解释了为什么不能用数值判断重复选择:判断 left.value === right.value 会把合法的 7 × 7 拦下来。应该检查的是两次选择是否指向同一个牌槽。
把选牌过程写成清楚的状态转换
在 pages/Index.ets 中,单人24点的核心操作状态包括:
@State single24Slots: TwentyFourSlot[] = [];
@State single24Selected: number = -1;
@State single24PendingOp: string = '';
三个字段分别回答:现在有哪些牌、选中了哪张牌、等待执行哪种运算。
玩家第一次点击数字牌时,只记录选择;点击运算符时,记录运算;再点击另一张有效牌,才执行合并。一次运算成功后,清空选中索引与运算符,让下一步重新开始。
项目的点击处理还有两个细节:再次点击已选中的同一张牌,会取消选择;没有选择运算符就点击另一张牌,会交换两张牌的位置。后一种操作也会进入撤销历史。只有选好运算符,第二张牌才会参与计算。
这些行为需要配上明确提示,例如“再选运算”“再点第二张牌”。提示与状态同步,玩家才能知道一次点击究竟产生了什么结果。
合并前的检查同样放在点击处理层:索引必须有效,牌必须处于可用状态,两次选择必须指向不同牌槽。计算函数只负责两个操作数之间的运算,页面负责保证这两个操作数来自合法选择。
用刚才的例题看,关键状态会这样变化:
| 玩家操作 | 选中槽位 | 待执行运算 | 可用数字 |
|---|---|---|---|
| 载入题目 | -1 | 空 | 1、2、7、7 |
| 点第一张7 | 2 | 空 | 1、2、7、7 |
| 点乘号 | 2 | × | 1、2、7、7 |
| 点第二张7,合并成功 | -1 | 空 | 1、2、49 |
最后一行的“空”很关键。如果成功后仍然保留乘号,下一次选牌就可能意外沿用上一轮操作。牌面更新与操作状态复位需要在一次成功处理里一起完成。
选7→乘法→得到49→减1得到48→除以2得到24→提交下一题。数字牌、表达式与可用牌数量随每一步一起变化。
合并两张牌,同时保存表达式
项目中的 applyTwentyFourOperation 根据运算符计算结果。下面是完整的计算函数,使用前需要从模型文件导入 TwentyFourSlot:
export function applyTwentyFourOperation(
left: TwentyFourSlot,
right: TwentyFourSlot,
operator: string
): TwentyFourSlot | undefined {
let value = 0;
if (operator === '+') {
value = left.value + right.value;
} else if (operator === '-') {
value = left.value - right.value;
} else if (operator === '×') {
value = left.value * right.value;
} else if (operator === '÷') {
if (Math.abs(right.value) < 0.000001) {
return undefined;
}
value = left.value / right.value;
} else {
return undefined;
}
return {
id: left.id,
value,
expr: `(${left.expr} ${operator} ${right.expr})`,
active: true
};
}
除法分支使用 0.000001 作为接近零的阈值。这是当前题库采用的计算策略,后续如果扩大数值范围,需要一起评估。运算符也要与按钮传入值一致:这里使用 ×、÷,传入 *、/ 会被视为不支持的运算。
表达式必须使用左右牌原有的 expr。第一步得到 (7 × 7),下一步就能继续生成 ((7 × 7) - 1),最后形成完整的解题过程。
括号保留了实际操作顺序。玩家先做乘法再做减法,表达式就应按这条路径展示;直接拼接数字和符号,会让页面显示的运算顺序与内部结果不一致。
接着,页面中的 combineTwentyFourSlots 克隆原数组,把结果写回第一张牌的位置,并将第二张牌标记为不可用:
const next = cloneTwentyFourSlots(slots);
next[first] = result;
next[second] = {
id: slots[second].id,
value: slots[second].value,
expr: slots[second].expr,
active: false
};
return next;
保留四个槽位,让牌区的位置稳定。一次合并后,可用牌数量从4变成3,再变成2,最后变成1。不可用牌仍占据原位置,页面通过显示与点击规则说明它已经参与过运算。
这里创建了新数组,也克隆了槽位对象,避免把之前的状态一起改掉。撤销需要旧状态,直接修改原对象容易让历史记录也跟着变化。
主 App 在更新牌数组后,还会同步各牌的显示字段与表达式快照。阅读实际代码时,需要把计算结果和页面刷新一起检查:只看到日志中的数值变化,并不能说明界面已经正确更新。
显示可以取舍,计算要保留精度
有些题目需要小数中间值,例如:
8 ÷ (3 - 8 ÷ 3) = 24
8 ÷ 3 在界面上可以显示成 2.67,但后续计算必须继续使用原始数值。如果把 2.67 写回 value,最后算出的结果就会偏离24。
项目用 formatTwentyFourNumber 处理显示:接近整数时显示整数,否则取到小数点后两位。它返回字符串,供文字组件使用;牌槽中的 value 保留计算值。
| 中间值的处理 | 最后一步的结果 |
|---|---|
保留 8 / 3 的计算精度 | 接近24,能够通过误差判定 |
把显示值 2.67 写回计算数据 | 8 / (3 - 2.67) 约为24.2424,判定失败 |
因此,显示函数应当用在 Text 的内容上,后续运算继续读取牌槽的数值。
表达式则仍然展示 (8 ÷ 3),帮助玩家理解这个中间值的来源。数值负责计算,显示字符串负责阅读,表达式负责解释过程,三个用途各有对应字段。
答对要同时满足两个条件
页面出现24,还不能立即判断成功。玩家可能只用两张牌就得到24,其余数字还没有参与运算。
项目的判定函数先数出可用牌数量,再判断最后的数值:
export function isTwentyFourSolved(slots: TwentyFourSlot[]): boolean {
let activeValue = 0;
let activeCount = 0;
for (let index = 0; index < slots.length; index++) {
if (slots[index].active) {
activeCount += 1;
activeValue = slots[index].value;
}
}
return activeCount === 1 && Math.abs(activeValue - 24) < 0.000001;
}
成功条件是:只剩一张可用牌,而且它的值接近24。

左:三次合并后只剩24,表达式保留完整的计算路径。右:提交后进入第2/5题,答对数增加到1,新题牌面与表达式同步刷新。
这个判定建立在前面的合法合并流程上。只要每次合并都使用两张不同的有效牌,并让第二张牌退出后续计算,四张初始牌经过三次合并就会全部参与。单独把一张值为24的牌交给判定函数,函数本身无法证明它曾使用四个原始数字。
提交成功后,主 App 增加答对数,再加载下一题;第五题完成后结束本轮训练,保存成绩。题库有18道固定题,分为三个难度,每档6道;一次训练使用其中5题。题库数量和每轮题数属于不同配置,讲解或扩展时要分别处理。
在模拟器里检查这些操作
进入单人24点后,可以先选择中级,用 1、2、7、7 走完上面的三次合并,再提交检查回合是否推进。之后用下面几项操作检查容易遗漏的边界。
| 操作 | 应检查的结果 |
|---|---|
| 使用两个数值相同的7 | 两张牌都能参与乘法 |
| 再次点击同一张选中牌 | 取消选择,不执行合并 |
| 点击已经合并的牌 | 不进入新的运算,给出提示 |
| 先得到0,再把它作为除数 | 牌面不发生非法合并,提示不能除以0 |
| 只用部分数字得到24后提交 | 判定未完成 |
| 使用需要分数中间值的题目 | 显示格式不影响最终计算 |
| 合并后点击撤销 | 牌面、选中状态和待执行运算符恢复 |
| 答对并进入下一题 | 新题数字与表达式一起刷新 |
检查时要同时看数字牌、表达式和可用牌数量。比如牌面已经变成48,表达式却还停在 (7 × 7),就是一次不完整的状态更新。

左:直接提交原始四张牌,页面提示“还没到24”。右:从24撤销一步,恢复48与2,除法与选中状态也一起恢复。
本次在 HarmonyOS 7.0.0 Beta2、API26 三折叠模拟器的单屏形态中运行1.0.19版本,完成了例题计算、未完成提交、撤销恢复和提交进入下一题的检查。上表中的除零和分数题可以继续按相同步骤检查。
《拼棋派对》的24点实现把牌的身份、计算值、表达式和可用状态组织在一起,再由点击流程约束每一步操作。这套结构既能支撑单人练习,也为双人玩法复用运算规则提供了基础。下一篇继续沿着这条操作路径,讲撤销快照、提示次数与回合结算怎样配合。
相关代码与参考资料
本文涉及的代码位于主 App 的 entry/src/main/ets/ 下:
| 文件 | 阅读重点 |
|---|---|
model/GameModels.ets | TwentyFourSlot 与 TwentyFourQuestion |
common/GameConfig.ets | 三档难度与18道固定题 |
common/GameLogic.ets | 初始化、克隆、计算、显示格式与答对判定 |
pages/Index.ets | 单人24点页面、选牌处理、合并与提交 |
学习语言与状态管理时,可以配合阅读华为官方文档:
计算函数可以复用到其他页面;页面片段仍需要结合工程中的模型导入与组件方法。运行时先选择单人24点,按本文的例题完成一次合并与提交,再逐项检查边界操作。
更多推荐
所有评论(0)