一、应用概述

石头剪刀布(Rock-Paper-Scissors) 是一款经典的人机对战游戏,用户与电脑进行石头剪刀布对决。应用提供了直观的图形化选择按钮(✊✌️🖐️),玩家点击选择后,电脑随机出拳,系统根据规则判定胜负并实时更新比分。应用基于 HarmonyOS 的 ArkTS 语言和声明式 UI 框架构建,完美展示了 Record 字典胜负表、Type 类型别名、枚举约束、计分追踪、以及状态驱动的胜负反馈等核心技术的综合运用。

对于鸿蒙开发者而言,石头剪刀布是一个展示游戏逻辑编码的经典案例——它让你在约87行代码中,同时掌握类型安全字典查找表随机选择算法计分系统四大核心能力,这些能力广泛适用于猜拳、棋牌、回合制游戏等场景。

1.1 功能特性

  • 三种出拳选择:石头(✊)、剪刀(✌️)、布(🖐️)
  • 电脑随机出拳:使用 Math.random() 随机选择
  • 胜负规则判定:基于字典查找表快速判定结果
  • 实时比分追踪:分别记录玩家和电脑的得分
  • 胜负状态反馈:不同结果使用不同颜色和文字提示
  • 比分重置功能:一键清除历史比分记录

1.2 适用场景

  • 休闲娱乐小游戏
  • 决策时作为随机辅助
  • 概率统计教学演示
  • 儿童逻辑思维训练

二、技术架构

2.1 整体架构概览

┌─────────────────────────────────────────┐
│           数据层                          │
│  Choice 类型 (石头|剪刀|布)               │
│  EMOJI / RULES / CHOICES 常量字典         │
├─────────────────────────────────────────┤
│           状态管理层                       │
│  @State playerChoice / computerChoice    │
│  @State result / playerScore /           │
│         computerScore / played           │
├─────────────────────────────────────────┤
│           业务逻辑层                       │
│  play(choice) —— 出拳 + 判定 + 计分      │
│  reset()      —— 重置比分                │
├─────────────────────────────────────────┤
│           UI 表示层                      │
│  电脑区域 / 结果显示 / 玩家区域 /          │
│  选择按钮 / 比分板 / 重置按钮             │
└─────────────────────────────────────────┘

2.2 游戏流程

用户点击选择(石头/剪刀/布)
       ↓
playerChoice = 用户选择
computerChoice = 随机选择
       ↓
查询 RULES[playerChoice][computerChoice]
       ↓
胜负判定 → 更新比分
       ↓
UI 重新渲染 → 显示结果和出拳

三、核心代码深度分析

3.1 完整的页面源码

import { router } from '@kit.ArkUI';

type Choice = '石头' | '剪刀' | '布';

const EMOJI: Record<Choice, string> = { '石头': '✊', '剪刀': '✌️', '布': '🖐️' };
const RULES: Record<Choice, Record<Choice, string>> = {
  '石头': { '石头': '平局', '剪刀': '你赢了!', '布': '电脑赢了' },
  '剪刀': { '石头': '电脑赢了', '剪刀': '平局', '布': '你赢了!' },
  '布': { '石头': '你赢了!', '剪刀': '电脑赢了', '布': '平局' },
};
const CHOICES: Choice[] = ['石头', '剪刀', '布'];

@Entry
@Component
struct index26 {
  @State playerChoice: Choice = '石头';
  @State computerChoice: Choice = '石头';
  @State result: string = '选择你的出拳';
  @State playerScore: number = 0;
  @State computerScore: number = 0;
  @State played: boolean = false;

  play(choice: Choice) {
    this.playerChoice = choice;
    this.computerChoice = CHOICES[Math.floor(Math.random() * 3)];
    this.result = RULES[this.playerChoice][this.computerChoice];
    if (this.result === '你赢了!') this.playerScore++;
    else if (this.result === '电脑赢了') this.computerScore++;
    this.played = true;
  }

  reset() {
    this.playerScore = 0;
    this.computerScore = 0;
    this.result = '选择你的出拳';
    this.played = false;
  }

  build() {
    Column() {
      Row() {
        Text('‹ 返回').fontSize(16).fontColor('#0077FF').onClick(() => router.back())
        Text('✊ 石头剪刀布').fontSize(18).fontWeight(FontWeight.Bold)
        Blank()
      }.width('92%').padding({ top: 16, bottom: 8 })

      Column() {
        // 电脑区域
        Text('电脑').fontSize(16).fontColor('#888')
        Text(EMOJI[this.computerChoice]).fontSize(64)
        if (this.played) Text(this.computerChoice).fontSize(16)

        // 结果
        Text(this.result).fontSize(22).fontWeight(FontWeight.Bold)
          .fontColor(this.result === '你赢了!' ? '#4CAF50' :
                     this.result === '电脑赢了' ? '#F44336' : '#FF9800')
          .margin(16)

        // 玩家区域
        Text(EMOJI[this.playerChoice]).fontSize(64)
        Text('你').fontSize(16).fontColor('#888')
        if (this.played) Text(this.playerChoice).fontSize(16)

        // 选择按钮
        Row({ space: 12 }) {
          ForEach(CHOICES, (c: Choice) => {
            Button() {
              Column() {
                Text(EMOJI[c]).fontSize(32)
                Text(c).fontSize(14).fontColor('#fff')
              }
            }.width(90).height(90).borderRadius(16).backgroundColor('#0077FF')
              .onClick(() => this.play(c))
          })
        }.margin({ top: 16 })

        // 比分板
        Row({ space: 20 }) {
          Text(`你: ${this.playerScore}`).fontSize(18).fontWeight(FontWeight.Bold)
            .fontColor('#4CAF50')
          Text(`电脑: ${this.computerScore}`).fontSize(18).fontWeight(FontWeight.Bold)
            .fontColor('#F44336')
        }.margin({ top: 12 })

        // 重置按钮
        Button('重置比分').width(120).height(40).borderRadius(20)
          .backgroundColor('#9E9E9E').margin({ top: 8 })
          .onClick(() => this.reset())
      }
      .layoutWeight(1).justifyContent(FlexAlign.Center)
    }
    .width('100%').height('100%').backgroundColor('#f8f8f8')
  }
}

3.2 Type 类型别名与类型安全

type Choice = '石头' | '剪刀' | '布';

type 是 ArkTS 中定义类型别名的关键字。这里定义了一个联合类型(Union Type),Choice 只能是三个字符串字面量之一。

使用类型别名的好处:

  1. 编译期类型检查:任何不符合 Choice 类型的赋值都会在编译时报错
  2. 代码自文档化:清晰地表达了"这个变量的取值范围只有三种"
  3. IDE 智能提示:在编码时获得准确的自动补全和类型提示

类型别名在整个应用中被多处使用:

const EMOJI: Record<Choice, string> = { ... };    // 键类型为 Choice
const RULES: Record<Choice, Record<Choice, string>> = { ... };  // 双层嵌套
const CHOICES: Choice[] = ['石头', '剪刀', '布'];  // 数组元素类型为 Choice

play(choice: Choice) { ... }  // 参数类型为 Choice

这种类型约束确保了数据的一致性和安全性,是 ArkTS 类型系统的核心优势。

3.3 胜负判定表的设计

const RULES: Record<Choice, Record<Choice, string>> = {
  '石头': { '石头': '平局', '剪刀': '你赢了!', '布': '电脑赢了' },
  '剪刀': { '石头': '电脑赢了', '剪刀': '平局', '布': '你赢了!' },
  '布': { '石头': '你赢了!', '剪刀': '电脑赢了', '布': '平局' },
};

这是一个二维查找表(Lookup Table),是游戏开发中实现规则判定的经典模式:

                 电脑出拳
             石头    剪刀    布
玩家出拳  石头  平局    你赢了  电脑赢了
          剪刀  电脑赢了  平局    你赢了
          布    你赢了  电脑赢了  平局

第一维(外层 Record 的键):玩家的出拳
第二维(内层 Record 的键):电脑的出拳
:判定结果字符串

这种设计比使用 if-else 链式判断更清晰、更易维护:

// ❌ 难以维护的链式判断
if (player === '石头' && computer === '剪刀') return '你赢了!';
if (player === '石头' && computer === '布') return '电脑赢了';
// ... 需要写9个条件分支

// ✅ 优雅的查找表
return RULES[player][computer];  // 一行搞定

查找表的优势:

  1. O(1) 时间复杂度:直接索引查找,无需条件分支
  2. 易于扩展:增加新规则只需修改表结构
  3. 可读性强:表格形式直观展示所有规则

3.4 游戏主逻辑

play(choice: Choice) {
  this.playerChoice = choice;                          // 1. 记录玩家选择
  this.computerChoice = CHOICES[Math.floor(Math.random() * 3)];  // 2. 电脑随机选择
  this.result = RULES[this.playerChoice][this.computerChoice];   // 3. 查询胜负
  if (this.result === '你赢了!') this.playerScore++;   // 4. 玩家得分
  else if (this.result === '电脑赢了') this.computerScore++;  // 5. 电脑得分
  this.played = true;                                  // 6. 标记已开始
}

六行代码完成了完整的游戏逻辑:

电脑随机选择

this.computerChoice = CHOICES[Math.floor(Math.random() * 3)];

Math.random() 生成 [0,1) 浮点数,乘以3得到 [0,3) 区间,Math.floor() 取整得到 0、1、2 三个整数索引,对应 CHOICES 数组中的三个选项。

胜负判定

this.result = RULES[this.playerChoice][this.computerChoice];

通过二维查找表,一行代码完成胜负判定。

计分逻辑

if (this.result === '你赢了!') this.playerScore++;
else if (this.result === '电脑赢了') this.computerScore++;

注意这里没有为"平局"做处理,因为平局不计分。

3.5 状态驱动的胜负反馈

Text(this.result).fontSize(22).fontWeight(FontWeight.Bold)
  .fontColor(this.result === '你赢了!' ? '#4CAF50' :
             this.result === '电脑赢了' ? '#F44336' : '#FF9800')

结果的文字颜色根据胜负状态动态变化:

结果 颜色 含义
你赢了! #4CAF50 绿色 积极、胜利
电脑赢了 #F44336 红色 消极、失败
平局 #FF9800 橙色 中性、提示

这种状态驱动的视觉反馈让玩家一眼就能判断胜负,无需仔细阅读文字内容。


四、HarmonyOS 特性深度解析

4.1 Button 内容插槽与复合按钮

Button() {
  Column() {
    Text(EMOJI[c]).fontSize(32)
    Text(c).fontSize(14).fontColor('#fff')
  }
}.width(90).height(90).borderRadius(16).backgroundColor('#0077FF')

HarmonyOS 的 Button 组件支持内容插槽(Content Slot),允许在按钮内部嵌入任意 UI 布局。这里使用 Column 垂直排列 Emoji 图标和文字标签,创建了复合按钮——比单纯的文字按钮更直观、更有吸引力。

每个按钮的布局:

┌──────────────┐
│     ✊       │  ← 32px Emoji
│    石头      │  ← 14px 文字
└──────────────┘

这种设计在游戏类应用中尤为常见,兼顾了视觉直观性和操作准确性。

4.2 条件渲染的出拳信息显示

Text(EMOJI[this.computerChoice]).fontSize(64)
if (this.played) Text(this.computerChoice).fontSize(16)

played 标志位控制是否显示文字版本的出拳信息。在游戏开始前(played=false),只显示默认的 Emoji(石头),不显示文字标签。游戏开始后(played=true),在 Emoji 下方补充显示文字描述,让用户确认电脑的出拳。

这种渐进式的信息披露设计,让界面在初始状态下保持简洁,在游戏过程中提供更完整的信息。

4.3 比分板的视觉设计

Row({ space: 20 }) {
  Text(`你: ${this.playerScore}`).fontSize(18).fontWeight(FontWeight.Bold)
    .fontColor('#4CAF50')
  Text(`电脑: ${this.computerScore}`).fontSize(18).fontWeight(FontWeight.Bold)
    .fontColor('#F44336')
}

比分板使用对称布局,玩家分数用绿色(代表积极),电脑分数用红色(代表消极)。space: 20 的间距确保两个分数清晰可辨,不会相互干扰。


五、UI/UX 设计模式

5.1 对战式布局

┌─────────────────────────────────┐
│           电脑区域                │
│      电脑 (灰色标签)              │
│      ✊ (64px Emoji)              │
│      石头 (出拳名称)              │
├─────────────────────────────────┤
│       ★ 胜负结果 ★              │  ← 居中,彩色显示
├─────────────────────────────────┤
│           玩家区域                │
│      ✊ (64px Emoji)              │
│      你 (灰色标签)                │
│      石头 (出拳名称)              │
├─────────────────────────────────┤
│      选择按钮 (✊✌️🖐️)           │
├─────────────────────────────────┤
│      比分板 + 重置按钮           │
└─────────────────────────────────┘

这种上下对称的布局清晰地展示了"玩家 vs 电脑"的对战关系,结果展示在中间,形成视觉焦点。

5.2 交互流程

初始状态: 显示默认出拳,提示"选择你的出拳"
    │
    │ 用户点击选择按钮
    ▼
游戏进行: 显示双方出拳,展示胜负结果,更新比分
    │
    │ 用户再次点击
    ▼
继续游戏: 新的出拳和结果,比分累加
    │
    │ 用户点击"重置比分"
    ▼
重置: 分数归零,恢复到初始提示

5.3 色彩心理学应用

元素 颜色 心理效应
选择按钮 #0077FF 蓝色 信任、可靠、可点击
玩家分数 #4CAF50 绿色 积极、成长、优势
电脑分数 #F44336 红色 警告、劣势、警惕
胜利结果 #4CAF50 绿色 成功、喜悦
失败结果 #F44336 红色 失败、警示
平局结果 #FF9800 橙色 中性、等待

六、最佳实践与经验总结

6.1 类型别名 vs 枚举

本应用使用 type(类型别名)而非 enum(枚举)来定义 Choice:

// 类型别名方式(本应用使用)
type Choice = '石头' | '剪刀' | '布';

// 枚举方式(另一种选择)
enum Choice { 石头 = '石头', 剪刀 = '剪刀', 布 = '布' }

类型别名的优势:

  1. 更简洁:无需定义枚举值到字符串的映射
  2. 与 JSON 兼容:直接使用字符串值,便于序列化
  3. 更灵活:可以组合其他类型

6.2 查找表的优势

使用 Record 字典作为查找表比 if-elseswitch-case 有明显的优势:

// ❌ if-else 方式
if (player === '石头' && computer === '石头') result = '平局';
else if (player === '石头' && computer === '剪刀') result = '你赢了!';
// ... 需要9个分支

// ❌ switch-case 方式
switch (player) {
  case '石头':
    switch (computer) {
      case '石头': result = '平局'; break;
      // ...
    }
    break;
  // ...
}

// ✅ 查找表方式 (推荐)
result = RULES[player][computer];

查找表是声明式编程的体现——你声明"规则是什么"而非"如何判断",代码更简洁、更易维护。

6.3 状态重置的最佳实践

reset() {
  this.playerScore = 0;
  this.computerScore = 0;
  this.result = '选择你的出拳';
  this.played = false;
}

重置函数应同时恢复所有相关状态到初始值,包括比分、结果文字和游戏状态标记。遗漏任何一个变量的重置可能导致 UI 显示异常。

6.4 游戏扩展建议

对于更完整的石头剪刀布游戏,可以考虑:

  1. 多局制:实现"三局两胜"或"五局三胜"的赛制
  2. 动画过渡:出拳时添加 Emoji 切换动画
  3. 出拳历史:显示最近几局的出拳记录
  4. 音效反馈:集成 HarmonyOS 的音效 API
  5. 联机对战:通过分布式能力实现远程对战
  6. AI 策略:根据玩家历史出拳模式调整电脑策略

七、总结

石头剪刀布应用在约87行代码中,完美展示了 ArkTS 声明式 UI 开发的多个核心概念:

  • Type 类型别名:联合类型的类型安全约束
  • Record 查找表:O(1) 时间复杂度的规则判定
  • 状态驱动的视觉反馈:胜负结果的颜色编码
  • Button 内容插槽:复合按钮的灵活构建
  • 计分系统:简单的累加计分逻辑
  • 条件渲染:played 标志位控制信息显示

这个应用以"石头剪刀布"这一经典游戏为切入点,展示了如何在 ArkTS 中构建交互式游戏应用。清晰的代码结构、优雅的查找表设计和类型安全的编程风格,使其成为学习 HarmonyOS 游戏开发的优秀入门案例。

Logo

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

更多推荐