HarmonyOS7应用拼棋派对开发实战系列统一封面

打开《拼棋派对》的单人24点,屏幕上只有四张数字牌。选一张牌,点运算符,再选另一张牌,两张牌就会合成一个新数字。继续操作,直到四张牌合成24,提交后进入下一题。

这个玩法很适合用来练习 ArkTS 与 ArkUI:页面不复杂,却包含重复数字、状态更新、运算顺序、浮点精度和答题判定等实际问题。本篇以项目中的单人24点为例,把“四个数字能算出24”落实成一套可以操作、可以检查的游戏规则。

项目当前使用 DevEco Studio 26.0.0.851 与 API 26 Release SDK,主 App 页面采用 @Component、@State 的 V1 状态管理。本篇沿用这套实现;这些基础能力也可以用于较早版本的 HarmonyOS 工程。

读完本文,你可以理解并复用24点的牌槽模型、两牌合并与完成判定,知道重复数字和小数中间值该怎样处理。阅读代码前,建议先了解 ArkTS 的接口、数组与 ArkUI 基础状态管理。

本文目录

先看玩家怎样完成一道题

手机竖屏中,页面从上到下安排难度、答题状态、数字牌、表达式、运算符和工具按钮。数字牌负责操作,表达式区负责说明当前算到了哪里,玩家不需要依靠记忆还原每一步。

手机模拟器中的单人24点初始题目:1、2、7、7

中级首题:四张数字牌、表达式与可用牌数量同时显示,底部提供运算和工具按钮。

以图中的 1、2、7、7 为例,可以这样操作:

  1. 选择第一张7,点击乘号,再选择另一张7,得到49。
  2. 选择49,点击减号,再选择1,得到48。
  3. 选择48,点击除号,再选择2,得到24。
  4. 点击提交,答对计分并进入下一题。

对应表达式是:

((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
点第一张72空1、2、7、7
点乘号2×1、2、7、7
点第二张7,合并成功-1空1、2、49

最后一行的“空”很关键。如果成功后仍然保留乘号,下一次选牌就可能意外沿用上一轮操作。牌面更新与操作状态复位需要在一次成功处理里一起完成。

1、2、7、7依次合并成49、48、24,提交进入下一题

选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,表达式完整,可用牌数量为1 提交成功后进入第二题,答对数增加到1

左:三次合并后只剩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以及除法选择状态

左:直接提交原始四张牌,页面提示“还没到24”。右:从24撤销一步,恢复48与2,除法与选中状态也一起恢复。

本次在 HarmonyOS 7.0.0 Beta2、API26 三折叠模拟器的单屏形态中运行1.0.19版本,完成了例题计算、未完成提交、撤销恢复和提交进入下一题的检查。上表中的除零和分数题可以继续按相同步骤检查。

《拼棋派对》的24点实现把牌的身份、计算值、表达式和可用状态组织在一起,再由点击流程约束每一步操作。这套结构既能支撑单人练习,也为双人玩法复用运算规则提供了基础。下一篇继续沿着这条操作路径,讲撤销快照、提示次数与回合结算怎样配合。

相关代码与参考资料

本文涉及的代码位于主 App 的 entry/src/main/ets/ 下:

文件阅读重点
model/GameModels.etsTwentyFourSlot 与 TwentyFourQuestion
common/GameConfig.ets三档难度与18道固定题
common/GameLogic.ets初始化、克隆、计算、显示格式与答对判定
pages/Index.ets单人24点页面、选牌处理、合并与提交

学习语言与状态管理时,可以配合阅读华为官方文档:

计算函数可以复用到其他页面;页面片段仍需要结合工程中的模型导入与组件方法。运行时先选择单人24点,按本文的例题完成一次合并与提交,再逐项检查边界操作。

Logo

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

更多推荐