在这里插入图片描述

目录

  1. 概述
  2. 游戏规则
  3. 核心功能
  4. 实战案例
  5. 编译过程详解
  6. 游戏扩展
  7. 最佳实践
  8. 常见问题

概述

本文档介绍如何在 Kotlin Multiplatform (KMP) 鸿蒙跨端开发中实现一个经典小游戏 - 石头剪刀布游戏。这个案例展示了如何使用 Kotlin 的集合操作、Map 数据结构和函数式编程来创建一个完整的游戏系统。通过 KMP,这个游戏可以无缝编译到 JavaScript,在 OpenHarmony 应用中运行。

游戏的特点

  • 经典玩法:人机对战,规则简单易懂
  • 数据结构应用:使用 Map 存储游戏规则
  • 函数式设计:使用 Lambda 实现游戏逻辑
  • 跨端兼容:一份 Kotlin 代码可同时服务多个平台
  • 实时反馈:即时显示每轮结果和最终统计

游戏规则

基本规则

  1. 游戏选项:石头、剪刀、布
  2. 胜负规则
    • 石头 > 剪刀(石头赢)
    • 剪刀 > 布(剪刀赢)
    • 布 > 石头(布赢)
    • 相同选择 = 平局
  3. 游戏流程
    • 玩家选择一个选项
    • 电脑随机选择一个选项
    • 比较结果并显示胜负
    • 重复多轮

游戏流程图

开始游戏
  ↓
玩家选择 (石头/剪刀/布)
  ↓
电脑选择 (随机)
  ↓
比较选择
  ├→ 相同 → 平局
  ├→ 玩家赢 → 玩家得分
  └→ 电脑赢 → 电脑得分
  ↓
显示结果
  ↓
继续下一轮或结束游戏

核心功能

1. 游戏规则 Map

val winRules = mapOf(
    "石头" to "剪刀",  // 石头赢剪刀
    "剪刀" to "布",    // 剪刀赢布
    "布" to "石头"     // 布赢石头
)

代码说明:

这个 Map 定义了游戏的胜负规则。使用 mapOf() 创建一个不可变 Map,键是玩家的选择,值是该选择能赢的对手选择。例如,“石头” 能赢 “剪刀”,“剪刀” 能赢 “布”,“布” 能赢 “石头”。这种数据结构使得规则查询非常高效,避免了复杂的 if-else 判断。

2. 判断胜负函数

val determineWinner: (String, String) -> String = { player, computer ->
    when {
        player == computer -> "平局"
        winRules[player] == computer -> "玩家赢"
        else -> "电脑赢"
    }
}

代码说明:

这是一个 Lambda 函数,用于判断游戏的胜负。函数接收两个字符串参数(玩家选择和电脑选择),返回游戏结果字符串。使用 when 表达式进行条件判断:如果两者相同返回"平局";如果玩家的选择能赢电脑的选择(通过查询 winRules Map)返回"玩家赢";否则返回"电脑赢"。这个函数是游戏的核心逻辑。

3. 格式化结果函数

val formatResult: (String, String, String) -> String = { player, computer, result ->
    val icon = when (result) {
        "玩家赢" -> "✅"
        "电脑赢" -> "❌"
        else -> "🤝"
    }
    "$icon 玩家: $player vs 电脑: $computer$result"
}

代码说明:

这是一个格式化函数,用于将游戏结果转换为易读的字符串。接收三个参数:玩家选择、电脑选择和游戏结果。首先根据结果选择相应的 emoji 图标:玩家赢显示"✅",电脑赢显示"❌",平局显示"🤝"。然后返回一个格式化的字符串,包含图标、双方选择和结果。这个函数增强了游戏的可读性和用户体验。

4. 统计函数

val playerWins = gameResults.count { it.contains("玩家赢") }
val computerWins = gameResults.count { it.contains("电脑赢") }
val draws = gameResults.count { it.contains("平局") }

代码说明:

这段代码使用集合操作统计游戏结果。count() 函数接收一个 Lambda 表达式,计算满足条件的元素个数。第一行统计玩家赢的次数,第二行统计电脑赢的次数,第三行统计平局的次数。通过检查结果字符串中是否包含特定的关键词来判断。这种方法简洁高效,避免了手动循环计数。


实战案例

案例:完整的石头剪刀布游戏

Kotlin 源代码
@OptIn(ExperimentalJsExport::class)
@JsExport
fun rockPaperScissorsGame(): String {
    // 定义游戏选项
    val choices = listOf("石头", "剪刀", "布")
    
    // 定义获胜规则
    val winRules = mapOf(
        "石头" to "剪刀",  // 石头赢剪刀
        "剪刀" to "布",    // 剪刀赢布
        "布" to "石头"     // 布赢石头
    )
    
    // 定义判断结果的函数
    val determineWinner: (String, String) -> String = { player, computer ->
        when {
            player == computer -> "平局"
            winRules[player] == computer -> "玩家赢"
            else -> "电脑赢"
        }
    }
    
    // 定义显示结果的函数
    val formatResult: (String, String, String) -> String = { player, computer, result ->
        val icon = when (result) {
            "玩家赢" -> "✅"
            "电脑赢" -> "❌"
            else -> "🤝"
        }
        "$icon 玩家: $player vs 电脑: $computer$result"
    }
    
    // 模拟游戏过程
    val playerMoves = listOf("石头", "布", "剪刀", "石头", "布")
    val computerMoves = listOf("剪刀", "石头", "剪刀", "布", "布")
    
    // 计算游戏结果
    val gameResults = playerMoves.zip(computerMoves).map { (player, computer) ->
        val result = determineWinner(player, computer)
        formatResult(player, computer, result)
    }
    
    // 统计胜负
    val playerWins = gameResults.count { it.contains("玩家赢") }
    val computerWins = gameResults.count { it.contains("电脑赢") }
    val draws = gameResults.count { it.contains("平局") }
    
    return "🎮 石头剪刀布游戏\n" +
           "━━━━━━━━━━━━━━━━━━━━━\n" +
           "总轮数: ${playerMoves.size}\n\n" +
           "游戏过程:\n" +
           gameResults.joinToString("\n") + "\n\n" +
           "━━━━━━━━━━━━━━━━━━━━━\n" +
           "玩家胜: $playerWins 次\n" +
           "电脑胜: $computerWins 次\n" +
           "平局: $draws 次\n" +
           "最终结果: " + when {
               playerWins > computerWins -> "🏆 玩家获胜!"
               computerWins > playerWins -> "🤖 电脑获胜!"
               else -> "🤝 平手!"
           }
}

代码说明:

这是石头剪刀布游戏的完整 Kotlin 实现。函数使用 @JsExport 装饰器将其导出为 JavaScript 可调用的函数。首先定义游戏选项列表和获胜规则 Map。定义判断胜负的 Lambda 函数和格式化结果的 Lambda 函数。模拟玩家和电脑的多轮选择。使用 zip() 配对两个列表,然后使用 map() 遍历每一对,调用判断函数获取结果,格式化输出。统计玩家赢、电脑赢和平局的次数。最后根据总体胜负确定最终赢家,返回完整的游戏过程和统计数据。

编译后的 JavaScript 代码
function rockPaperScissorsGame() {
  // 定义游戏选项
  var choices = ['石头', '剪刀', '布'];
  
  // 定义获胜规则
  var winRules = {
    '石头': '剪刀',
    '剪刀': '布',
    '布': '石头'
  };
  
  // 定义判断结果的函数
  var determineWinner = function(player, computer) {
    if (player === computer) {
      return '平局';
    } else if (winRules[player] === computer) {
      return '玩家赢';
    } else {
      return '电脑赢';
    }
  };
  
  // 定义显示结果的函数
  var formatResult = function(player, computer, result) {
    var icon;
    if (result === '玩家赢') {
      icon = '✅';
    } else if (result === '电脑赢') {
      icon = '❌';
    } else {
      icon = '🤝';
    }
    return icon + ' 玩家: ' + player + ' vs 电脑: ' + computer + ' → ' + result;
  };
  
  // 模拟游戏过程
  var playerMoves = ['石头', '布', '剪刀', '石头', '布'];
  var computerMoves = ['剪刀', '石头', '剪刀', '布', '布'];
  
  // 计算游戏结果
  var gameResults = [];
  for (var i = 0; i < playerMoves.length; i++) {
    var player = playerMoves[i];
    var computer = computerMoves[i];
    var result = determineWinner(player, computer);
    gameResults.push(formatResult(player, computer, result));
  }
  
  // 统计胜负
  var playerWins = 0, computerWins = 0, draws = 0;
  for (var i = 0; i < gameResults.length; i++) {
    if (gameResults[i].indexOf('玩家赢') !== -1) playerWins++;
    else if (gameResults[i].indexOf('电脑赢') !== -1) computerWins++;
    else draws++;
  }
  
  // 确定最终结果
  var finalResult;
  if (playerWins > computerWins) {
    finalResult = '🏆 玩家获胜!';
  } else if (computerWins > playerWins) {
    finalResult = '🤖 电脑获胜!';
  } else {
    finalResult = '🤝 平手!';
  }
  
  return '🎮 石头剪刀布游戏\n' +
         '━━━━━━━━━━━━━━━━━━━━━\n' +
         '总轮数: ' + playerMoves.length + '\n\n' +
         '游戏过程:\n' +
         gameResults.join('\n') + '\n\n' +
         '━━━━━━━━━━━━━━━━━━━━━\n' +
         '玩家胜: ' + playerWins + ' 次\n' +
         '电脑胜: ' + computerWins + ' 次\n' +
         '平局: ' + draws + ' 次\n' +
         '最终结果: ' + finalResult;
}

代码说明:

这是 Kotlin 代码编译到 JavaScript 后的结果。可以看到 Kotlin 的语言特性被转换为 JavaScript 等价物:mapOf() 变成对象字面量,Lambda 函数变成匿名函数,when 表达式变成 if-else 语句,zip()map() 变成 for 循环。虽然编译后的代码看起来不同,但它保留了原始 Kotlin 代码的逻辑。使用 ES Module 格式,可以被其他模块导入。包含完整的类型定义(.d.ts 文件),提供 TypeScript 支持。可以直接在浏览器或 Node.js 中运行。

ArkTS 调用代码
import { rockPaperScissorsGame } from './hellokjs';

@Entry
@Component
struct Index {
  @State message: string = '加载中...';
  @State results: string[] = [];
  @State caseTitle: string = '小游戏 - 石头剪刀布游戏';

  aboutToAppear(): void {
    this.loadResults();
  }

  loadResults(): void {
    try {
      // 调用 Kotlin 编译的 JavaScript 函数
      const gameResult = rockPaperScissorsGame();
      this.results = [gameResult];
      this.message = '✓ 游戏已加载';
    } catch (error) {
      this.message = `✗ 错误: ${error}`;
    }
  }

  build() {
    Column() {
      // 顶部标题栏
      Row() {
        Text('KMP 鸿蒙跨端')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)
        Spacer()
        Text('Kotlin 案例')
          .fontSize(14)
          .fontColor(Color.White)
      }
      .width('100%')
      .height(50)
      .backgroundColor('#3b82f6')
      .padding({ left: 20, right: 20 })
      .alignItems(VerticalAlign.Center)
      .justifyContent(FlexAlign.SpaceBetween)

      // 案例标题
      Column() {
        Text(this.caseTitle)
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor('#1f2937')
        Text(this.message)
          .fontSize(13)
          .fontColor('#6b7280')
          .margin({ top: 5 })
      }
      .width('100%')
      .padding({ left: 20, right: 20, top: 20, bottom: 15 })
      .alignItems(HorizontalAlign.Start)

      // 结果显示区域
      Scroll() {
        Column() {
          ForEach(this.results, (result: string) => {
            Column() {
              Text(result)
                .fontSize(13)
                .fontFamily('monospace')
                .fontColor('#374151')
                .width('100%')
                .margin({ top: 10 })
            }
            .width('100%')
            .padding(16)
            .backgroundColor(Color.White)
            .border({ width: 1, color: '#e5e7eb' })
            .borderRadius(8)
            .margin({ bottom: 12 })
          })
        }
        .width('100%')
        .padding({ left: 16, right: 16 })
      }
      .layoutWeight(1)
      .width('100%')

      // 底部按钮区域
      Row() {
        Button('刷新')
          .width('48%')
          .height(44)
          .backgroundColor('#3b82f6')
          .fontColor(Color.White)
          .fontSize(14)
          .onClick(() => {
            this.loadResults();
          })

        Button('返回')
          .width('48%')
          .height(44)
          .backgroundColor('#6b7280')
          .fontColor(Color.White)
          .fontSize(14)
          .onClick(() => {
            // 返回操作
          })
      }
      .width('100%')
      .padding({ left: 16, right: 16, bottom: 20 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f9fafb')
  }
}

代码说明:

这是 OpenHarmony ArkTS 页面的完整实现,展示了如何集成和调用 Kotlin 编译生成的游戏函数。首先通过 import 语句从 ./hellokjs 模块导入 rockPaperScissorsGame 函数。页面使用 @Entry@Component 装饰器定义为可入口的组件。定义了三个响应式状态变量:message 显示操作状态,results 存储游戏结果,caseTitle 显示标题。aboutToAppear() 生命周期钩子在页面加载时调用 loadResults() 进行初始化。loadResults() 方法调用 Kotlin 函数进行游戏,将结果存储在 results 数组中,并更新 message 显示状态。使用 try-catch 块捕获异常。build() 方法定义了完整的 UI 布局,包括顶部标题栏、游戏标题、结果显示区域和底部按钮区域。使用 monospace 字体显示游戏过程,保持格式对齐。


编译过程详解

Kotlin 到 JavaScript 的转换

Kotlin 特性 JavaScript 等价物
Map 数据结构 对象 (Object)
Lambda 函数 匿名函数
when 表达式 if-else 语句
List.zip() 数组配对
count() 函数 循环计数

关键转换点

  1. Map 转换:Kotlin Map 转换为 JavaScript 对象
  2. Lambda 表达式:转换为 JavaScript 函数
  3. 集合操作:转换为数组操作
  4. 字符串处理:保持功能一致

游戏扩展

扩展 1:添加难度级别

val easyMode = listOf("石头", "石头", "布")  // 电脑偏好
val normalMode = listOf("石头", "剪刀", "布")  // 随机
val hardMode = listOf("剪刀", "布", "石头")  // 克制玩家

代码说明:

这段代码演示了如何添加不同难度级别。每个难度级别是一个列表,表示电脑的选择偏好。简单模式中,电脑常选"石头",有利于玩家。普通模式是完全随机。困难模式中,电脑会选择克制玩家的选择,对玩家不利。这个设计允许玩家根据自己的技能水平选择合适的难度。

扩展 2:添加玩家历史记录

data class GameRecord(val playerMove: String, val computerMove: String, val result: String)
val history = mutableListOf<GameRecord>()

代码说明:

这段代码演示了如何添加游戏历史记录。定义一个数据类 GameRecord 存储每一轮游戏的信息:玩家选择、电脑选择和游戏结果。创建一个可变列表存储所有的游戏记录。这个设计允许玩家查看游戏历史、分析游戏数据。

扩展 3:添加排行榜

data class PlayerScore(val name: String, val wins: Int, val losses: Int, val draws: Int)
val leaderboard = mutableListOf<PlayerScore>()

代码说明:

这段代码演示了如何添加排行榜系统。定义一个数据类 PlayerScore 存储玩家成绩:玩家名字、胜数、负数和平局数。创建一个可变列表存储所有玩家的成绩记录。这个设计允许跟踪多个玩家的成绩,实现排行榜功能。

扩展 4:添加连胜统计

var currentWinStreak = 0
var maxWinStreak = 0

代码说明:

这段代码演示了如何添加连胜统计。定义两个变量:currentWinStreak 跟踪当前的连胜次数,maxWinStreak 跟踪最高连胜次数。每当玩家赢一轮时,将 currentWinStreak 加 1;每当玩家输或平局时,将 currentWinStreak 重置为 0。并更新 maxWinStreak。这个设计允许跟踪玩家的最佳表现。


最佳实践

1. 使用 Map 存储规则

// ✅ 好:使用 Map 存储游戏规则
val winRules = mapOf("石头" to "剪刀", "剪刀" to "布", "布" to "石头")

// ❌ 不好:使用多个 if 语句
if (player == "石头" && computer == "剪刀") { /* ... */ }
if (player == "剪刀" && computer == "布") { /* ... */ }

代码说明:

这个示例对比了两种存储游戏规则的方法。第一种方法使用 Map 数据结构,规则查询只需一次 O(1) 的操作,代码简洁高效。第二种方法使用多个 if 语句,代码冗长且容易出错,维护困难。最佳实践是:使用 Map 存储规则,提高代码的可读性和效率。

2. 使用 zip() 配对数据

// ✅ 好:使用 zip() 配对
val results = playerMoves.zip(computerMoves).map { (p, c) -> /* ... */ }

// ❌ 不好:使用索引循环
for (i in playerMoves.indices) {
    val p = playerMoves[i]
    val c = computerMoves[i]
}

代码说明:

这个示例对比了两种配对两个列表的方法。第一种方法使用 zip() 函数,将两个列表配对成一个元组列表,然后使用 map() 进行转换,代码简洁优雅。第二种方法使用索引循环,容易出错且代码冗长。最佳实践是:使用 zip() 配对数据。

3. 使用 count() 统计

// ✅ 好:使用 count() 统计
val wins = results.count { it.contains("玩家赢") }

// ❌ 不好:使用 for 循环
var wins = 0
for (result in results) {
    if (result.contains("玩家赢")) wins++
}

代码说明:

这个示例对比了两种统计满足条件元素的方法。第一种方法使用 count() 函数,直接计数满足条件的元素,代码简洁高效。第二种方法使用 for 循环手动计数,代码冗长且容易出错。最佳实践是:使用 count() 进行条件计数。

4. 使用 emoji 增加可读性

// ✅ 好:使用 emoji
"✅ 玩家赢"
"❌ 电脑赢"
"🤝 平局"

// ❌ 不好:没有视觉反馈
"Player wins"
"Computer wins"
"Draw"

代码说明:

这个示例对比了两种显示游戏结果的方法。第一种方法使用 emoji 图标,增加了文本的可读性和视觉吸引力,用户能快速识别结果。第二种方法使用纯文本,缺乏视觉反馈,用户体验较差。最佳实践是:合理使用 emoji,增强用户体验。


常见问题

Q1: 如何实现真正的随机选择?

A: 在 Kotlin/JS 中,可以使用 JavaScript 的 Math.random()

external fun jsRandom(): Double = definedExternally

fun getRandomChoice(): String {
    val choices = listOf("石头", "剪刀", "布")
    val index = (jsRandom() * choices.size).toInt()
    return choices[index]
}

代码说明:

这段代码展示了如何实现真正的随机选择。使用 external 关键字声明一个外部函数 jsRandom(),它调用 JavaScript 的 Math.random() 方法。生成 0 到 1 之间的随机数,乘以选择列表的大小得到 0 到 2 之间的数,转换为整数后作为索引。这样就能随机选择一个游戏选项。

Q2: 如何保存游戏历史?

A: 使用本地存储:

external object localStorage {
    fun setItem(key: String, value: String)
    fun getItem(key: String): String?
}

// 保存游戏记录
localStorage.setItem("gameHistory", history.toString())

代码说明:

这段代码展示了如何保存游戏历史。定义一个 external object 来访问浏览器的 localStorage API。使用 setItem() 方法将游戏历史转换为字符串并保存到本地存储。使用 getItem() 方法可以读取保存的游戏历史。这允许玩家在刷新页面后继续查看历史记录。

Q3: 如何实现多人联网对战?

A: 使用 WebSocket:

external class WebSocket(url: String) {
    fun send(data: String)
    var onmessage: ((String) -> Unit)?
}

val ws = WebSocket("ws://game-server.com")
ws.send("石头")

代码说明:

这段代码展示了如何实现多人联网对战。定义一个 external class 来访问浏览器的 WebSocket API。创建一个 WebSocket 连接到游戏服务器。使用 send() 方法发送玩家的选择到服务器。使用 onmessage 回调处理来自服务器的消息(对手的选择)。这允许实现实时的网络多人对战功能。

Q4: 如何优化游戏性能?

A:

  1. 避免不必要的对象创建
  2. 使用 inline 函数减少函数调用开销
  3. 使用 Sequence 处理大数据集

Q5: 如何添加音效?

A: 使用 Web Audio API:

external fun playSound(url: String)

// 游戏获胜时播放声音
if (playerWins > computerWins) {
    playSound("victory.mp3")
}

代码说明:

这段代码展示了如何添加音效。定义一个 external fun 来调用 Web Audio API 播放音频文件。当玩家获胜时,调用 playSound() 播放胜利音效。这增强了游戏的沉浸感和用户体验。


总结

关键要点

  • ✅ 使用 Map 存储游戏规则
  • ✅ 使用 Lambda 实现游戏逻辑
  • ✅ 使用集合操作处理游戏数据
  • ✅ 使用 zip() 配对数据
  • ✅ KMP 能无缝编译到 JavaScript

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐