在这里插入图片描述

目录

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

概述

本文档介绍如何在 Kotlin Multiplatform (KMP) 鸿蒙跨端开发中实现一个翻牌记忆游戏。这个案例展示了如何使用 Kotlin 的集合操作、Pair 数据结构和逻辑判断来创建一个完整的记忆力游戏系统。通过 KMP,这个游戏可以无缝编译到 JavaScript,在 OpenHarmony 应用中运行。

游戏的特点

  • 记忆挑战:考验玩家的记忆力和观察力
  • 数据结构应用:使用 Pair 存储卡牌位置
  • 逻辑判断:实现卡牌匹配检查
  • 跨端兼容:一份 Kotlin 代码可同时服务多个平台
  • 详细统计:显示成功率、得分和评价

游戏规则

基本规则

  1. 卡牌设置
    • 12 张卡牌(6 对)
    • 每对卡牌相同
    • 随机排列
  2. 游戏流程
    • 玩家翻开两张卡牌
    • 如果相同,配对成功
    • 如果不同,翻回并继续
    • 重复直到全部配对
  3. 计分规则
    • 成功配对:+100 分
    • 失败尝试:-10 分
  4. 评价标准
    • 完美:全部配对成功
    • 不错:配对成功一半以上
    • 继续加油:其他情况

游戏流程图

开始游戏
  ↓
显示所有卡牌
  ↓
玩家选择两张卡牌翻开
  ↓
检查是否匹配
  ├→ 匹配 → 配对成功,记录
  └→ 不匹配 → 配对失败,翻回
  ↓
继续翻牌或游戏结束
  ↓
统计成功率和得分
  ↓
显示最终评价

核心功能

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 语句

关键转换点

  1. Pair 转换:Kotlin Pair 转换为 JavaScript 数组
  2. 集合操作:转换为数组操作
  3. Lambda 表达式:转换为 JavaScript 函数
  4. 逻辑判断:保持功能一致

游戏扩展

扩展 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
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐