KMP 实现鸿蒙跨端:Kotlin 小游戏 - 掷骰子游戏
本文介绍了一个基于Kotlin Multiplatform (KMP)的掷骰子游戏实现,展示了如何利用Kotlin的数学运算和集合操作开发跨端应用。游戏核心包括:定义骰子点数范围(1-6)、实现比较逻辑判定胜负、格式化输出每轮结果,以及统计总分、平均值和胜负次数。通过KMP技术,该游戏可编译为JavaScript并在OpenHarmony应用中运行,实现了"一次编写,多端运行"

目录
概述
本文档介绍如何在 Kotlin Multiplatform (KMP) 鸿蒙跨端开发中实现一个掷骰子游戏。这个案例展示了如何使用 Kotlin 的数学运算、集合操作和统计功能来创建一个完整的游戏系统。通过 KMP,这个游戏可以无缝编译到 JavaScript,在 OpenHarmony 应用中运行。
游戏的特点
- 数学运算:涉及骰子点数的比较和统计
- 多轮游戏:支持多轮对战和总分统计
- 数据分析:计算平均值、总分和胜负统计
- 跨端兼容:一份 Kotlin 代码可同时服务多个平台
- 实时反馈:即时显示每轮结果和详细统计
游戏规则
基本规则
- 骰子范围:1-6 点
- 游戏流程:
- 玩家掷骰子
- 电脑掷骰子
- 比较点数大小
- 记录胜负
- 胜负判定:
- 点数大者获胜
- 点数相同为平局
- 计分方式:
- 单轮:直接比较点数
- 总分:累计所有轮次的点数
游戏流程图
开始游戏
↓
玩家掷骰子 (1-6)
↓
电脑掷骰子 (1-6)
↓
比较点数
├→ 玩家点数大 → 玩家赢
├→ 电脑点数大 → 电脑赢
└→ 点数相同 → 平局
↓
记录结果和总分
↓
继续下一轮或结束游戏
↓
统计总分和平均值
核心功能
1. 骰子点数定义
val diceValues = listOf(1, 2, 3, 4, 5, 6)
代码说明:
这段代码定义了骰子的所有可能点数。使用 listOf() 创建一个不可变列表,包含 1 到 6 的所有点数。这个列表用于验证掷骰子的结果是否有效,或者在需要时随机选择一个点数。
2. 比较函数
val compareRolls: (Int, Int) -> String = { player, computer ->
when {
player > computer -> "玩家赢"
computer > player -> "电脑赢"
else -> "平局"
}
}
代码说明:
这是一个 Lambda 函数,用于比较玩家和电脑的掷骰子结果。函数接收两个整数参数(玩家点数和电脑点数),返回一个字符串表示结果。使用 when 表达式进行条件判断:如果玩家点数大于电脑,返回"玩家赢";如果电脑点数大于玩家,返回"电脑赢";否则返回"平局"。这个函数是游戏的核心逻辑。
3. 格式化函数
val formatRound: (Int, Int, Int, String) -> String = { round, player, computer, result ->
val icon = when (result) {
"玩家赢" -> "🎯"
"电脑赢" -> "🎲"
else -> "⚖️"
}
"$icon 第${round}轮: 玩家[$player] vs 电脑[$computer] → $result"
}
代码说明:
这是一个 Lambda 函数,用于格式化每轮游戏结果为易读的字符串。接收四个参数:轮数、玩家点数、电脑点数和比较结果。首先根据结果选择相应的图标:玩家赢显示"🎯",电脑赢显示"🎲",平局显示"⚖️"。然后返回一个格式化的字符串,包含图标、轮数、双方点数和结果。这个函数用于生成游戏过程的日志。
4. 统计函数
val playerScore = playerRolls.sum() // 总分
val playerAverage = playerScore / playerRolls.size // 平均值
val playerWins = rounds.count { it.contains("玩家赢") } // 胜数
代码说明:
这段代码展示了如何使用集合操作进行游戏统计。playerRolls.sum() 计算玩家所有轮次的总分。playerScore / playerRolls.size 计算平均每轮的得分。rounds.count { it.contains("玩家赢") } 统计玩家赢的轮数,通过计数包含"玩家赢"的字符串。这些统计数据用于生成游戏报告。
实战案例
案例:完整的掷骰子游戏
Kotlin 源代码
@OptIn(ExperimentalJsExport::class)
@JsExport
fun diceTossingGame(): String {
// 定义骰子点数
val diceValues = listOf(1, 2, 3, 4, 5, 6)
// 模拟多次掷骰子
val playerRolls = listOf(4, 2, 6, 3, 5, 1, 4, 6)
val computerRolls = listOf(3, 5, 2, 6, 4, 2, 5, 3)
// 定义比较函数
val compareRolls: (Int, Int) -> String = { player, computer ->
when {
player > computer -> "玩家赢"
computer > player -> "电脑赢"
else -> "平局"
}
}
// 定义格式化函数
val formatRound: (Int, Int, Int, String) -> String = { round, player, computer, result ->
val icon = when (result) {
"玩家赢" -> "🎯"
"电脑赢" -> "🎲"
else -> "⚖️"
}
"$icon 第${round}轮: 玩家[$player] vs 电脑[$computer] → $result"
}
// 计算每轮结果
val rounds = playerRolls.zip(computerRolls).mapIndexed { index, (player, computer) ->
val result = compareRolls(player, computer)
formatRound(index + 1, player, computer, result)
}
// 统计总分
val playerScore = playerRolls.sum()
val computerScore = computerRolls.sum()
val playerWins = rounds.count { it.contains("玩家赢") }
val computerWins = rounds.count { it.contains("电脑赢") }
val draws = rounds.count { it.contains("平局") }
// 计算平均值
val playerAverage = playerScore / playerRolls.size
val computerAverage = computerScore / computerRolls.size
return "🎲 掷骰子游戏\n" +
"━━━━━━━━━━━━━━━━━━━━━\n" +
"总轮数: ${playerRolls.size}\n\n" +
"游戏过程:\n" +
rounds.joinToString("\n") + "\n\n" +
"━━━━━━━━━━━━━━━━━━━━━\n" +
"统计数据:\n" +
"玩家总分: $playerScore (平均: $playerAverage)\n" +
"电脑总分: $computerScore (平均: $computerAverage)\n\n" +
"轮数统计:\n" +
"玩家胜: $playerWins 轮\n" +
"电脑胜: $computerWins 轮\n" +
"平局: $draws 轮\n\n" +
"最终结果: " + when {
playerScore > computerScore -> "🏆 玩家获胜!总分更高"
computerScore > playerScore -> "🤖 电脑获胜!总分更高"
else -> "🤝 平手!总分相同"
}
}
代码说明:
这是掷骰子游戏的完整 Kotlin 实现。函数使用 @JsExport 装饰器将其导出为 JavaScript 可调用的函数。首先定义骰子点数列表和模拟的掷骰子结果。定义比较函数和格式化函数。使用 zip() 配对玩家和电脑的掷骰子结果,然后使用 mapIndexed 遍历每一对,调用比较函数获取结果,格式化输出。统计总分、平均值和胜负数。最后根据总分确定最终胜者,返回完整的游戏过程和统计数据。
编译后的 JavaScript 代码
function diceTossingGame() {
// 定义骰子点数
var diceValues = [1, 2, 3, 4, 5, 6];
// 模拟多次掷骰子
var playerRolls = [4, 2, 6, 3, 5, 1, 4, 6];
var computerRolls = [3, 5, 2, 6, 4, 2, 5, 3];
// 定义比较函数
var compareRolls = function(player, computer) {
if (player > computer) {
return '玩家赢';
} else if (computer > player) {
return '电脑赢';
} else {
return '平局';
}
};
// 定义格式化函数
var formatRound = function(round, player, computer, result) {
var icon;
if (result === '玩家赢') {
icon = '🎯';
} else if (result === '电脑赢') {
icon = '🎲';
} else {
icon = '⚖️';
}
return icon + ' 第' + round + '轮: 玩家[' + player + '] vs 电脑[' + computer + '] → ' + result;
};
// 计算每轮结果
var rounds = [];
for (var i = 0; i < playerRolls.length; i++) {
var player = playerRolls[i];
var computer = computerRolls[i];
var result = compareRolls(player, computer);
rounds.push(formatRound(i + 1, player, computer, result));
}
// 统计总分
var playerScore = 0, computerScore = 0;
for (var i = 0; i < playerRolls.length; i++) {
playerScore += playerRolls[i];
computerScore += computerRolls[i];
}
var playerWins = 0, computerWins = 0, draws = 0;
for (var i = 0; i < rounds.length; i++) {
if (rounds[i].indexOf('玩家赢') !== -1) playerWins++;
else if (rounds[i].indexOf('电脑赢') !== -1) computerWins++;
else draws++;
}
// 计算平均值
var playerAverage = Math.floor(playerScore / playerRolls.length);
var computerAverage = Math.floor(computerScore / computerRolls.length);
// 确定最终结果
var finalResult;
if (playerScore > computerScore) {
finalResult = '🏆 玩家获胜!总分更高';
} else if (computerScore > playerScore) {
finalResult = '🤖 电脑获胜!总分更高';
} else {
finalResult = '🤝 平手!总分相同';
}
return '🎲 掷骰子游戏\n' +
'━━━━━━━━━━━━━━━━━━━━━\n' +
'总轮数: ' + playerRolls.length + '\n\n' +
'游戏过程:\n' +
rounds.join('\n') + '\n\n' +
'━━━━━━━━━━━━━━━━━━━━━\n' +
'统计数据:\n' +
'玩家总分: ' + playerScore + ' (平均: ' + playerAverage + ')\n' +
'电脑总分: ' + computerScore + ' (平均: ' + computerAverage + ')\n\n' +
'轮数统计:\n' +
'玩家胜: ' + playerWins + ' 轮\n' +
'电脑胜: ' + computerWins + ' 轮\n' +
'平局: ' + draws + ' 轮\n\n' +
'最终结果: ' + finalResult;
}
代码说明:
这是 Kotlin 代码编译到 JavaScript 后的结果。可以看到 Kotlin 的语言特性被转换为 JavaScript 等价物:listOf() 变成数组字面量,sum() 变成循环求和,count() 变成条件计数循环,when 表达式变成 if-else 语句。虽然编译后的代码看起来不同,但它保留了原始 Kotlin 代码的逻辑。使用 ES Module 格式,可以被其他模块导入。包含完整的类型定义(.d.ts 文件),提供 TypeScript 支持。可以直接在浏览器或 Node.js 中运行。
ArkTS 调用代码
import { diceTossingGame } 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 = diceTossingGame();
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 模块导入 diceTossingGame 函数。页面使用 @Entry 和 @Component 装饰器定义为可入口的组件。定义了三个响应式状态变量:message 显示操作状态,results 存储游戏结果,caseTitle 显示标题。aboutToAppear() 生命周期钩子在页面加载时调用 loadResults() 进行初始化。loadResults() 方法调用 Kotlin 函数进行游戏,将结果存储在 results 数组中,并更新 message 显示状态。使用 try-catch 块捕获异常。build() 方法定义了完整的 UI 布局,包括顶部标题栏、游戏标题、结果显示区域和底部按钮区域。使用 monospace 字体显示游戏过程,保持格式对齐。
编译过程详解
Kotlin 到 JavaScript 的转换
| Kotlin 特性 | JavaScript 等价物 |
|---|---|
| List.sum() | 数组求和循环 |
| List.average() | 数组平均值计算 |
| mapIndexed() | 带索引的数组映射 |
| count() | 条件计数 |
| when 表达式 | if-else 语句 |
关键转换点
- 数学运算:直接转换为 JavaScript 运算
- 集合操作:转换为数组操作
- Lambda 表达式:转换为 JavaScript 函数
- 字符串处理:保持功能一致
游戏扩展
扩展 1:添加多个骰子
val multiDice = listOf(
listOf(4, 2, 6), // 玩家掷3个骰子
listOf(3, 5, 2) // 电脑掷3个骰子
)
val playerTotal = multiDice[0].sum()
val computerTotal = multiDice[1].sum()
代码说明:
这段代码展示了如何支持多个骰子。使用嵌套列表存储多个骰子的结果:外层列表包含玩家和电脑的结果,内层列表包含每个玩家掷的多个骰子的点数。使用 sum() 计算每个玩家的总分。这允许实现更复杂的游戏规则。
扩展 2:添加特殊规则
// 豹子(三个相同数字)
val isPair = playerRolls.distinct().size == 1
// 顺子(连续数字)
val isStraight = playerRolls.sorted() == (playerRolls.min()..playerRolls.max()).toList()
代码说明:
这段代码展示了如何添加特殊规则。豹子检测:使用 distinct() 去重,如果去重后大小为 1,说明所有数字相同。顺子检测:先排序,然后与从最小值到最大值的范围比较,如果相等则是顺子。这些特殊规则可以增加游戏的复杂性和趣味性。
扩展 3:添加赌注系统
data class Bet(val amount: Int, val odds: Double)
var playerBalance = 1000
val currentBet = Bet(100, 2.0)
代码说明:
这段代码展示了如何添加赌注系统。定义一个数据类 Bet 存储赌注信息:赌注金额和赔率。维护玩家的余额。创建当前赌注对象,指定赌注金额为 100,赔率为 2.0。这允许实现更真实的赌博游戏体验。
扩展 4:添加排行榜
data class PlayerRecord(val name: String, val totalScore: Int, val wins: Int)
val leaderboard = mutableListOf<PlayerRecord>()
代码说明:
这段代码展示了如何添加排行榜系统。定义一个数据类 PlayerRecord 存储玩家记录:玩家名字、总分和胜数。创建一个可变列表存储所有玩家的记录。这允许跟踪多个玩家的成绩,实现排行榜功能。
最佳实践
1. 使用 sum() 计算总分
// ✅ 好:使用 sum() 函数
val total = playerRolls.sum()
// ❌ 不好:使用 for 循环
var total = 0
for (roll in playerRolls) {
total += roll
}
代码说明:
这个示例对比了两种计算总分的方法。第一种方法使用 sum() 函数,简洁高效,一行代码完成。第二种方法使用 for 循环手动累加,代码冗长且容易出错。最佳实践是:使用 sum() 进行求和操作。
2. 使用 mapIndexed() 获取索引
// ✅ 好:使用 mapIndexed()
val rounds = playerRolls.zip(computerRolls).mapIndexed { index, (p, c) -> /* ... */ }
// ❌ 不好:使用 indices
for (i in playerRolls.indices) {
val p = playerRolls[i]
val c = computerRolls[i]
}
代码说明:
这个示例对比了两种获取索引和元素的方法。第一种方法使用 zip() 配对两个列表,然后使用 mapIndexed() 获得索引和配对元素,代码简洁。第二种方法使用 for 循环和索引访问,代码冗长。最佳实践是:当需要索引和元素时,使用 mapIndexed()。
3. 使用 count() 统计
// ✅ 好:使用 count()
val wins = rounds.count { it.contains("玩家赢") }
// ❌ 不好:使用 filter().size
val wins = rounds.filter { it.contains("玩家赢") }.size
代码说明:
这个示例对比了两种统计满足条件元素的方法。第一种方法使用 count() 直接计数,高效且简洁。第二种方法先用 filter() 过滤,再获取大小,需要创建中间列表,效率较低。最佳实践是:使用 count() 进行条件计数。
4. 使用整数除法计算平均值
// ✅ 好:使用整数除法
val average = total / count
// ❌ 不好:使用浮点数
val average = (total.toDouble() / count).toInt()
代码说明:
这个示例对比了两种计算平均值的方法。第一种方法直接使用整数除法,简洁高效。第二种方法先转换为浮点数,进行浮点除法,再转换回整数,过程复杂且效率低。最佳实践是:当只需要整数平均值时,直接使用整数除法。
常见问题
Q1: 如何实现真正的随机掷骰子?
A: 在 Kotlin/JS 中,可以使用 JavaScript 的 Math.random():
external fun jsRandom(): Double = definedExternally
fun rollDice(): Int {
return (jsRandom() * 6).toInt() + 1
}
代码说明:
这段代码展示了如何实现真正的随机掷骰子。使用 external 关键字声明一个外部函数 jsRandom(),它调用 JavaScript 的 Math.random() 方法。rollDice() 函数生成 0 到 1 之间的随机数,乘以 6 得到 0 到 6 之间的数,转换为整数后加 1,得到 1 到 6 之间的随机数。
Q2: 如何处理多个骰子的情况?
A: 使用嵌套列表或数据类:
data class DiceRoll(val rolls: List<Int>) {
val total: Int get() = rolls.sum()
val average: Int get() = total / rolls.size
}
代码说明:
这段代码展示了如何处理多个骰子。定义一个数据类 DiceRoll 存储多个骰子的结果列表。使用计算属性 total 计算总分,average 计算平均值。这个设计使代码更清晰,易于扩展。
Q3: 如何实现概率分析?
A: 计算每个结果出现的频率:
val frequency = rounds.groupingBy { it }.eachCount()
val probability = frequency.mapValues { (_, count) -> count.toDouble() / rounds.size }
代码说明:
这段代码展示了如何进行概率分析。使用 groupingBy() 按结果分组,然后使用 eachCount() 计算每个结果出现的次数。使用 mapValues() 将计数转换为概率,即每个结果出现的次数除以总轮数。这允许分析游戏的概率分布。
Q4: 如何保存游戏历史?
A: 使用本地存储:
external object localStorage {
fun setItem(key: String, value: String)
fun getItem(key: String): String?
}
localStorage.setItem("gameHistory", rounds.toString())
代码说明:
这段代码展示了如何保存游戏历史。定义一个 external object 来访问浏览器的 localStorage API。使用 setItem() 方法将游戏轮次信息转换为字符串并保存。使用 getItem() 方法可以读取保存的游戏历史。这允许玩家在刷新页面后查看历史记录。
Q5: 如何实现网络对战?
A: 使用 WebSocket:
external class WebSocket(url: String) {
fun send(data: String)
var onmessage: ((String) -> Unit)?
}
val ws = WebSocket("ws://game-server.com")
ws.send("roll:${rollDice()}")
代码说明:
这段代码展示了如何实现网络对战。定义一个 external class 来访问浏览器的 WebSocket API。创建一个 WebSocket 连接到游戏服务器。使用 send() 方法发送掷骰子结果到服务器。使用 onmessage 回调处理来自服务器的消息。这允许实现实时的网络对战功能。
总结
关键要点
- ✅ 使用 sum() 计算总分
- ✅ 使用 mapIndexed() 处理索引
- ✅ 使用 count() 统计结果
- ✅ 使用 zip() 配对数据
- ✅ KMP 能无缝编译到 JavaScript
下一步
- 实现真正的随机掷骰子
- 添加多骰子支持
- 实现概率分析
- 添加游戏历史记录
- 实现网络对战
参考资源
更多推荐




所有评论(0)