KMP 实现鸿蒙跨端:Kotlin 小游戏 - 石头剪刀布游戏
本文介绍了使用Kotlin Multiplatform (KMP)实现跨平台石头剪刀布游戏的开发方法。文章详细讲解了游戏的核心功能实现,包括:1) 使用Map数据结构存储游戏规则;2) 通过Lambda函数实现胜负判断逻辑;3) 格式化输出游戏结果;4) 统计游戏胜负数据。案例展示了完整的Kotlin代码实现,包括游戏选项定义、规则判定、结果统计等功能模块。该游戏采用函数式编程风格,代码简洁高效,

目录
概述
本文档介绍如何在 Kotlin Multiplatform (KMP) 鸿蒙跨端开发中实现一个经典小游戏 - 石头剪刀布游戏。这个案例展示了如何使用 Kotlin 的集合操作、Map 数据结构和函数式编程来创建一个完整的游戏系统。通过 KMP,这个游戏可以无缝编译到 JavaScript,在 OpenHarmony 应用中运行。
游戏的特点
- 经典玩法:人机对战,规则简单易懂
- 数据结构应用:使用 Map 存储游戏规则
- 函数式设计:使用 Lambda 实现游戏逻辑
- 跨端兼容:一份 Kotlin 代码可同时服务多个平台
- 实时反馈:即时显示每轮结果和最终统计
游戏规则
基本规则
- 游戏选项:石头、剪刀、布
- 胜负规则:
- 石头 > 剪刀(石头赢)
- 剪刀 > 布(剪刀赢)
- 布 > 石头(布赢)
- 相同选择 = 平局
- 游戏流程:
- 玩家选择一个选项
- 电脑随机选择一个选项
- 比较结果并显示胜负
- 重复多轮
游戏流程图
开始游戏
↓
玩家选择 (石头/剪刀/布)
↓
电脑选择 (随机)
↓
比较选择
├→ 相同 → 平局
├→ 玩家赢 → 玩家得分
└→ 电脑赢 → 电脑得分
↓
显示结果
↓
继续下一轮或结束游戏
核心功能
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() 函数 | 循环计数 |
关键转换点
- Map 转换:Kotlin Map 转换为 JavaScript 对象
- Lambda 表达式:转换为 JavaScript 函数
- 集合操作:转换为数组操作
- 字符串处理:保持功能一致
游戏扩展
扩展 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:
- 避免不必要的对象创建
- 使用 inline 函数减少函数调用开销
- 使用 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
更多推荐

所有评论(0)