KMP 实现鸿蒙跨端:Kotlin 小游戏 - 翻牌记忆游戏
本文介绍了基于Kotlin Multiplatform(KMP)的鸿蒙跨端翻牌记忆游戏实现方案。游戏包含12张卡牌(6对),通过随机排列考验玩家记忆力。核心功能包括:卡牌对定义与随机排列、匹配检查逻辑、游戏过程格式化输出以及详细的统计计算(成功率、得分和评价)。该方案展示了Kotlin集合操作、Pair数据结构和逻辑判断的实战应用,通过KMP技术可编译为JavaScript在OpenHarmony

目录
概述
本文档介绍如何在 Kotlin Multiplatform (KMP) 鸿蒙跨端开发中实现一个翻牌记忆游戏。这个案例展示了如何使用 Kotlin 的集合操作、Pair 数据结构和逻辑判断来创建一个完整的记忆力游戏系统。通过 KMP,这个游戏可以无缝编译到 JavaScript,在 OpenHarmony 应用中运行。
游戏的特点
- 记忆挑战:考验玩家的记忆力和观察力
- 数据结构应用:使用 Pair 存储卡牌位置
- 逻辑判断:实现卡牌匹配检查
- 跨端兼容:一份 Kotlin 代码可同时服务多个平台
- 详细统计:显示成功率、得分和评价
游戏规则
基本规则
- 卡牌设置:
- 12 张卡牌(6 对)
- 每对卡牌相同
- 随机排列
- 游戏流程:
- 玩家翻开两张卡牌
- 如果相同,配对成功
- 如果不同,翻回并继续
- 重复直到全部配对
- 计分规则:
- 成功配对:+100 分
- 失败尝试:-10 分
- 评价标准:
- 完美:全部配对成功
- 不错:配对成功一半以上
- 继续加油:其他情况
游戏流程图
开始游戏
↓
显示所有卡牌
↓
玩家选择两张卡牌翻开
↓
检查是否匹配
├→ 匹配 → 配对成功,记录
└→ 不匹配 → 配对失败,翻回
↓
继续翻牌或游戏结束
↓
统计成功率和得分
↓
显示最终评价
核心功能
1. 卡牌对定义
val cardPairs = listOf("🌟", "🎨", "🎭", "🎪", "🎯", "🎲")
代码说明:
这段代码定义了游戏中的卡牌对。使用 listOf() 创建一个不可变列表,包含 6 个不同的 emoji 符号,每个符号代表一对卡牌。这些符号将被复制两次以创建完整的卡牌组。
2. 卡牌组创建
val cards = (cardPairs + cardPairs).shuffled()
代码说明:
这段代码创建了完整的卡牌组并随机打乱。首先使用 + 操作符将 cardPairs 与自身连接,创建一个包含 12 张卡牌的列表(每个符号出现两次)。然后使用 shuffled() 函数随机打乱卡牌顺序。这确保了每次游戏的卡牌排列都不同。
3. 匹配检查函数
val checkMatch: (String, String) -> Boolean = { card1, card2 ->
card1 == card2
}
代码说明:
这是一个 Lambda 函数,用于检查两张卡牌是否匹配。函数接收两个字符串参数(两张卡牌的符号),返回一个布尔值。如果两张卡牌相同返回 true,否则返回 false。这个函数是游戏的核心逻辑。
4. 格式化函数
val formatMove: (Int, Int, Int, String, String, Boolean) -> String = { moveNum, pos1, pos2, card1, card2, matched ->
val icon = if (matched) "✅" else "❌"
val status = if (matched) "配对成功" else "配对失败"
"$icon 第${moveNum}步: 翻开[$card1] 和 [$card2] → $status"
}
代码说明:
这是一个格式化函数,用于将翻牌操作转换为易读的字符串。接收六个参数:步数、两个位置、两张卡牌和匹配结果。根据是否匹配选择相应的 emoji 图标和状态文本。返回一个格式化的字符串,包含图标、步数、卡牌符号和结果。这个函数增强了游戏的可读性。
5. 统计函数
val matchedCount = moves.count { it.contains("配对成功") }
val failedCount = moves.count { it.contains("配对失败") }
val accuracy = (matchedCount * 100) / playerMoves.size
代码说明:
这段代码使用集合操作进行游戏统计。count() 函数接收一个 Lambda 表达式,计算满足条件的元素个数。第一行统计成功配对的次数,第二行统计失败配对的次数。第三行计算成功率百分比。这种方法简洁高效,避免了手动循环计数。
实战案例
案例:完整的翻牌记忆游戏
Kotlin 源代码
@OptIn(ExperimentalJsExport::class)
@JsExport
fun memoryCardGame(): String {
// 定义卡牌对
val cardPairs = listOf("🌟", "🎨", "🎭", "🎪", "🎯", "🎲")
// 创建卡牌组(每个卡牌出现两次)
val cards = (cardPairs + cardPairs).shuffled()
// 模拟玩家的翻牌操作
val playerMoves = listOf(
Pair(0, 1), // 翻开第1和2张
Pair(2, 3), // 翻开第3和4张
Pair(4, 5), // 翻开第5和6张
Pair(0, 6), // 翻开第1和7张
Pair(7, 8), // 翻开第8和9张
Pair(1, 9) // 翻开第2和10张
)
// 定义匹配检查函数
val checkMatch: (String, String) -> Boolean = { card1, card2 ->
card1 == card2
}
// 定义格式化函数
val formatMove: (Int, Int, Int, String, String, Boolean) -> String = { moveNum, pos1, pos2, card1, card2, matched ->
val icon = if (matched) "✅" else "❌"
val status = if (matched) "配对成功" else "配对失败"
"$icon 第${moveNum}步: 翻开[$card1] 和 [$card2] → $status"
}
// 计算游戏结果
val moves = playerMoves.mapIndexed { index, (pos1, pos2) ->
val card1 = cards[pos1]
val card2 = cards[pos2]
val matched = checkMatch(card1, card2)
formatMove(index + 1, pos1, pos2, card1, card2, matched)
}
// 统计匹配情况
val matchedCount = moves.count { it.contains("配对成功") }
val failedCount = moves.count { it.contains("配对失败") }
val totalPairs = cardPairs.size
// 计算得分
val score = (matchedCount * 100) - (failedCount * 10)
val accuracy = (matchedCount * 100) / playerMoves.size
return "🎮 翻牌记忆游戏\n" +
"━━━━━━━━━━━━━━━━━━━━━\n" +
"卡牌总数: ${cards.size}\n" +
"卡牌对数: $totalPairs\n\n" +
"游戏过程:\n" +
moves.joinToString("\n") + "\n\n" +
"━━━━━━━━━━━━━━━━━━━━━\n" +
"统计数据:\n" +
"配对成功: $matchedCount 对\n" +
"配对失败: $failedCount 次\n" +
"成功率: $accuracy%\n\n" +
"得分计算:\n" +
"成功配对: $matchedCount × 100 = ${matchedCount * 100}\n" +
"失败扣分: $failedCount × 10 = ${failedCount * 10}\n" +
"最终得分: $score 分\n\n" +
"最终结果: " + when {
matchedCount == totalPairs -> "🏆 完美!全部配对成功!"
matchedCount >= totalPairs / 2 -> "🎉 不错!配对成功一半以上"
else -> "💪 继续加油!"
}
}
代码说明:
这是翻牌记忆游戏的完整 Kotlin 实现。函数使用 @JsExport 装饰器将其导出为 JavaScript 可调用的函数。首先定义卡牌对和创建随机打乱的卡牌组。定义匹配检查函数和格式化函数。模拟玩家的翻牌操作序列,每次翻开两张卡牌。使用 mapIndexed 遍历每个操作,获取卡牌,检查是否匹配,格式化输出。统计成功配对数、失败配对数和成功率。计算得分(成功配对加分,失败配对扣分)。最后根据成功配对数确定游戏评价,返回完整的游戏过程和统计数据。
编译后的 JavaScript 代码
function memoryCardGame() {
// 定义卡牌对
var cardPairs = ['🌟', '🎨', '🎭', '🎪', '🎯', '🎲'];
// 创建卡牌组
var allCards = cardPairs.concat(cardPairs);
var cards = allCards.slice().sort(function() { return Math.random() - 0.5; });
// 模拟玩家的翻牌操作
var playerMoves = [
[0, 1], [2, 3], [4, 5], [0, 6], [7, 8], [1, 9]
];
// 定义匹配检查函数
var checkMatch = function(card1, card2) {
return card1 === card2;
};
// 定义格式化函数
var formatMove = function(moveNum, pos1, pos2, card1, card2, matched) {
var icon = matched ? '✅' : '❌';
var status = matched ? '配对成功' : '配对失败';
return icon + ' 第' + moveNum + '步: 翻开[' + card1 + '] 和 [' + card2 + '] → ' + status;
};
// 计算游戏结果
var moves = [];
for (var i = 0; i < playerMoves.length; i++) {
var pos1 = playerMoves[i][0];
var pos2 = playerMoves[i][1];
var card1 = cards[pos1];
var card2 = cards[pos2];
var matched = checkMatch(card1, card2);
moves.push(formatMove(i + 1, pos1, pos2, card1, card2, matched));
}
// 统计匹配情况
var matchedCount = 0, failedCount = 0;
for (var i = 0; i < moves.length; i++) {
if (moves[i].indexOf('配对成功') !== -1) matchedCount++;
else failedCount++;
}
var totalPairs = cardPairs.length;
var score = (matchedCount * 100) - (failedCount * 10);
var accuracy = Math.floor((matchedCount * 100) / playerMoves.length);
// 确定最终结果
var finalResult;
if (matchedCount === totalPairs) {
finalResult = '🏆 完美!全部配对成功!';
} else if (matchedCount >= totalPairs / 2) {
finalResult = '🎉 不错!配对成功一半以上';
} else {
finalResult = '💪 继续加油!';
}
return '🎮 翻牌记忆游戏\n' +
'━━━━━━━━━━━━━━━━━━━━━\n' +
'卡牌总数: ' + cards.length + '\n' +
'卡牌对数: ' + totalPairs + '\n\n' +
'游戏过程:\n' +
moves.join('\n') + '\n\n' +
'━━━━━━━━━━━━━━━━━━━━━\n' +
'统计数据:\n' +
'配对成功: ' + matchedCount + ' 对\n' +
'配对失败: ' + failedCount + ' 次\n' +
'成功率: ' + accuracy + '%\n\n' +
'得分计算:\n' +
'成功配对: ' + matchedCount + ' × 100 = ' + (matchedCount * 100) + '\n' +
'失败扣分: ' + failedCount + ' × 10 = ' + (failedCount * 10) + '\n' +
'最终得分: ' + score + ' 分\n\n' +
'最终结果: ' + finalResult;
}
代码说明:
这是 Kotlin 代码编译到 JavaScript 后的结果。可以看到 Kotlin 的语言特性被转换为 JavaScript 等价物:Pair 变成数组,shuffled() 变成 sort() 随机排序,Lambda 函数变成匿名函数,mapIndexed 变成 for 循环,count() 变成条件计数循环,when 表达式变成 if-else 语句。虽然编译后的代码看起来不同,但它保留了原始 Kotlin 代码的逻辑。使用 ES Module 格式,可以被其他模块导入。包含完整的类型定义(.d.ts 文件),提供 TypeScript 支持。可以直接在浏览器或 Node.js 中运行。
ArkTS 调用代码
import { memoryCardGame } 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 = memoryCardGame();
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 模块导入 memoryCardGame 函数。页面使用 @Entry 和 @Component 装饰器定义为可入口的组件。定义了三个响应式状态变量:message 显示操作状态,results 存储游戏结果,caseTitle 显示标题。aboutToAppear() 生命周期钩子在页面加载时调用 loadResults() 进行初始化。loadResults() 方法调用 Kotlin 函数进行游戏,将结果存储在 results 数组中,并更新 message 显示状态。使用 try-catch 块捕获异常。build() 方法定义了完整的 UI 布局,包括顶部标题栏、游戏标题、结果显示区域和底部按钮区域。使用 monospace 字体显示游戏过程,保持格式对齐。
编译过程详解
Kotlin 到 JavaScript 的转换
| Kotlin 特性 | JavaScript 等价物 |
|---|---|
| Pair 数据结构 | 数组 [pos1, pos2] |
| shuffled() 函数 | sort() 随机排序 |
| mapIndexed() | 带索引的数组循环 |
| count() 函数 | 条件计数循环 |
| when 表达式 | if-else 语句 |
关键转换点
- Pair 转换:Kotlin Pair 转换为 JavaScript 数组
- 集合操作:转换为数组操作
- Lambda 表达式:转换为 JavaScript 函数
- 逻辑判断:保持功能一致
游戏扩展
扩展 1:增加难度级别
// 简单模式:4对卡牌
val easyCards = listOf("🌟", "🎨", "🎭", "🎪")
// 普通模式:6对卡牌
val normalCards = listOf("🌟", "🎨", "🎭", "🎪", "🎯", "🎲")
// 困难模式:8对卡牌
val hardCards = listOf("🌟", "🎨", "🎭", "🎪", "🎯", "🎲", "🎸", "🎺")
代码说明:
这段代码演示了如何添加不同难度级别。简单模式使用 4 对卡牌,对初学者友好。普通模式使用 6 对卡牌,是标准难度。困难模式使用 8 对卡牌,对有经验的玩家挑战。这个设计允许玩家根据自己的技能水平选择合适的难度。
扩展 2:添加时间限制
val startTime = System.currentTimeMillis()
// ... 游戏逻辑 ...
val endTime = System.currentTimeMillis()
val timeUsed = (endTime - startTime) / 1000
代码说明:
这段代码演示了如何添加时间限制。使用 System.currentTimeMillis() 获取当前时间戳(毫秒)。在游戏开始时记录开始时间,在游戏结束时记录结束时间。计算时间差并除以 1000 转换为秒。这个设计允许统计玩家完成游戏所用的时间。
扩展 3:添加连续成功计数
var consecutiveMatches = 0
var maxConsecutiveMatches = 0
for (move in moves) {
if (move.contains("配对成功")) {
consecutiveMatches++
maxConsecutiveMatches = maxOf(maxConsecutiveMatches, consecutiveMatches)
} else {
consecutiveMatches = 0
}
}
代码说明:
这段代码演示了如何添加连续成功计数。定义两个变量:consecutiveMatches 跟踪当前的连续成功次数,maxConsecutiveMatches 跟踪最高连续成功次数。遵历每个游戏操作,如果配对成功则连续数加 1,并更新最高连续数;否则重置连续数。这个设计允许跟踪玩家的最佳表现。
扩展 4:添加排行榜系统
data class GameRecord(val playerName: String, val score: Int, val accuracy: Int)
val leaderboard = mutableListOf<GameRecord>()
代码说明:
这段代码演示了如何添加排行榜系统。定义一个数据类 GameRecord 存储游戏记录:玩家名字、得分和成功率。创建一个可变列表存储所有玩家的成绩记录。这个设计允许跟踪多个玩家的成绩,实现排行榜功能。
最佳实践
1. 使用 Pair 存储坐标
// ✅ 好:使用 Pair 存储两个位置
val move = Pair(0, 1)
val (pos1, pos2) = move
// ❌ 不好:使用两个变量
var pos1 = 0
var pos2 = 1
代码说明:
这个示例对比了两种存储坐标的方法。第一种方法使用 Pair 数据结构,可以将两个相关的值作为一个整体,支持解构赋值,代码简洁。第二种方法使用两个独立的变量,容易混淆且不够优雅。最佳实践是:使用 Pair 存储相关的坐标对。
2. 使用 shuffled() 随机打乱
// ✅ 好:使用 shuffled() 函数
val cards = (cardPairs + cardPairs).shuffled()
// ❌ 不好:手动实现随机
val cards = mutableListOf<String>()
// ... 复杂的随机逻辑 ...
代码说明:
这个示例对比了两种随机打乱列表的方法。第一种方法使用 shuffled() 函数,简洁高效,一行代码完成。第二种方法手动实现随机逻辑,代码复杂且容易出错。最佳实践是:使用 shuffled() 函数进行随机打乱。
3. 使用 mapIndexed() 处理索引
// ✅ 好:使用 mapIndexed()
val moves = playerMoves.mapIndexed { index, (pos1, pos2) -> /* ... */ }
// ❌ 不好:使用 for 循环
for (i in playerMoves.indices) {
val (pos1, pos2) = playerMoves[i]
}
代码说明:
这个示例对比了两种处理带索引列表的方法。第一种方法使用 mapIndexed() 函数,直接在 Lambda 中获得索引和元素,支持解构赋值,代码简洁。第二种方法使用 for 循环和索引访问,代码冗长。最佳实践是:使用 mapIndexed() 处理索引。
4. 使用 count() 统计
// ✅ 好:使用 count()
val matchedCount = moves.count { it.contains("配对成功") }
// ❌ 不好:使用 filter().size
val matchedCount = moves.filter { it.contains("配对成功") }.size
代码说明:
这个示例对比了两种统计满足条件元素的方法。第一种方法使用 count() 函数,直接计数满足条件的元素,高效且简洁。第二种方法先用 filter() 过滤,再获取大小,需要创建中间列表,效率较低。最佳实践是:使用 count() 进行条件计数。
常见问题
Q1: 如何实现真正的随机打乱?
A: 使用 Kotlin 的 shuffled() 函数或 JavaScript 的 Math.random():
val cards = (cardPairs + cardPairs).shuffled()
代码说明:
这段代码展示了如何实现真正的随机打乱。使用 shuffled() 函数对列表进行随机打乱。这个函数使用 Fisher-Yates 算法确保均匀的随机分布。编译到 JavaScript 后,会使用 Math.random() 实现相同的功能。这确保了每次游戏的卡牌排列都是随机的。
Q2: 如何实现卡牌翻回动画?
A: 在 ArkTS 中使用动画:
Text(cardFlipped ? card : '?')
.transition(TransitionEffect.OPACITY.animation({ duration: 300 }))
代码说明:
这段代码展示了如何实现卡牌翻回动画。使用条件表达式根据 cardFlipped 状态显示卡牌或问号。使用 transition() 方法添加过渡效果。TransitionEffect.OPACITY 创建透明度变化效果,animation() 指定动画持续时间为 300 毫秒。这创建了流畅的卡牌翻转动画效果。
Q3: 如何保存游戏进度?
A: 使用本地存储:
external object localStorage {
fun setItem(key: String, value: String)
fun getItem(key: String): String?
}
localStorage.setItem("gameProgress", moves.toString())
代码说明:
这段代码展示了如何保存游戏进度。定义一个 external object 来访问浏览器的 localStorage API。使用 setItem() 方法将游戏操作转换为字符串并保存到本地存储。使用 getItem() 方法可以读取保存的游戏进度。这允许玩家在刷新页面后继续游戏。
Q4: 如何实现多人游戏?
A: 使用网络通信:
external class WebSocket(url: String) {
fun send(data: String)
var onmessage: ((String) -> Unit)?
}
代码说明:
这段代码展示了如何实现多人游戏。定义一个 external class 来访问浏览器的 WebSocket API。创建一个 WebSocket 连接到游戏服务器。使用 send() 方法发送玩家的翻牌操作到服务器。使用 onmessage 回调处理来自服务器的消息(对手的操作)。这允许实现实时的网络多人游戏功能。
Q5: 如何计算最优步数?
A: 使用最少翻牌次数作为参考:
val optimalMoves = cardPairs.size // 最少需要翻 cardPairs.size 次
val efficiency = (optimalMoves * 100) / playerMoves.size
代码说明:
这段代码展示了如何计算游戏效率。最优步数等于卡牌对数,因为理想情况下玩家可以一次配对所有卡牌。效率计算为最优步数与实际步数的比值乘以 100。例如,如果有 6 对卡牌,最优步数是 6,如果玩家用 12 步完成,效率就是 50%。这允许评估玩家的游戏表现。
总结
关键要点
- ✅ 使用 Pair 存储卡牌位置
- ✅ 使用 shuffled() 随机打乱
- ✅ 使用 mapIndexed() 处理索引
- ✅ 使用 count() 统计结果
- ✅ KMP 能无缝编译到 JavaScript
更多推荐


所有评论(0)