目录

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

概述

本文档介绍如何在 Kotlin Multiplatform (KMP) 鸿蒙跨端开发中实现一个经典的扫雷游戏。这个案例展示了如何使用 Kotlin 的二维网格处理、邻域计算和游戏逻辑来创建一个完整的策略游戏系统。通过 KMP,这个游戏可以无缝编译到 JavaScript,在 OpenHarmony 应用中运行。

游戏的特点

  • 经典玩法:传统的扫雷游戏规则
  • 网格处理:实现二维网格的坐标转换
  • 邻域计算:计算周围地雷数量
  • 跨端兼容:一份 Kotlin 代码可同时服务多个平台
  • 详细统计:显示安全率、得分和评价

游戏规则

基本规则

  1. 游戏网格
    • 5×5 的网格(25 个格子)
    • 其中 5 个格子有地雷
    • 其他 20 个格子是安全的
  2. 游戏流程
    • 玩家点击格子
    • 如果是地雷,游戏结束
    • 如果是安全格子,显示周围地雷数
    • 继续点击直到游戏结束
  3. 计分规则
    • 安全点击:+10 分
    • 踩中地雷:-50 分
  4. 评价标准
    • 完美:没有踩中任何地雷
    • 不错:只踩中 1-2 个地雷
    • 继续加油:踩中 3 个或以上地雷

游戏流程图

开始游戏
  ↓
显示 5×5 网格
  ↓
玩家点击格子
  ↓
检查格子内容
  ├→ 地雷 → 游戏结束,显示结果
  └→ 安全 → 显示周围地雷数
  ↓
继续点击或游戏结束
  ↓
统计安全率和得分
  ↓
显示最终评价

核心功能

1. 网格初始化

val gridSize = 5
val totalCells = gridSize * gridSize
val mines = MutableList(totalCells) { false }
// 放置地雷
for (i in 0 until mineCount) {
    mines[i] = true
}
mines.shuffle()

创建游戏网格并随机放置地雷。

2. 邻域计算函数

fun countAdjacentMines(index: Int): Int {
    val row = index / gridSize
    val col = index % gridSize
    var count = 0
    
    for (dr in -1..1) {
        for (dc in -1..1) {
            if (dr == 0 && dc == 0) continue
            val newRow = row + dr
            val newCol = col + dc
            if (newRow in 0 until gridSize && newCol in 0 until gridSize) {
                val newIndex = newRow * gridSize + newCol
                if (mines[newIndex]) count++
            }
        }
    }
    return count
}

计算指定格子周围的地雷数量。

3. 点击检查函数

val checkCell: (Int) -> String = { index ->
    if (mines[index]) {
        "💣 地雷"
    } else {
        val adjacent = countAdjacentMines(index)
        if (adjacent == 0) "⬜ 空" else "🔢 $adjacent"
    }
}

检查点击的格子内容。

4. 格式化函数

val formatClick: (Int, Int, String) -> String = { clickNum, index, result ->
    val row = index / gridSize
    val col = index % gridSize
    val icon = when {
        result.contains("💣") -> "❌"
        result.contains("空") -> "✅"
        else -> "ℹ️"
    }
    "$icon${clickNum}步: 点击[$row,$col] → $result"
}

将点击操作格式化为易读的字符串。


实战案例

案例:完整的扫雷游戏

Kotlin 源代码
@OptIn(ExperimentalJsExport::class)
@JsExport
fun minesweeperGame(): String {
    // 定义游戏网格大小
    val gridSize = 5
    val totalCells = gridSize * gridSize
    val mineCount = 5
    
    // 创建游戏网格(true表示有地雷)
    val mines = MutableList(totalCells) { false }
    // 放置地雷
    for (i in 0 until mineCount) {
        mines[i] = true
    }
    mines.shuffle()
    
    // 计算每个格子周围的地雷数
    fun countAdjacentMines(index: Int): Int {
        val row = index / gridSize
        val col = index % gridSize
        var count = 0
        
        for (dr in -1..1) {
            for (dc in -1..1) {
                if (dr == 0 && dc == 0) continue
                val newRow = row + dr
                val newCol = col + dc
                if (newRow in 0 until gridSize && newCol in 0 until gridSize) {
                    val newIndex = newRow * gridSize + newCol
                    if (mines[newIndex]) count++
                }
            }
        }
        return count
    }
    
    // 模拟玩家的点击操作
    val playerClicks = listOf(0, 1, 2, 5, 6, 7, 10, 11, 12, 15, 16, 17, 20, 21, 22)
    
    // 定义点击结果函数
    val checkCell: (Int) -> String = { index ->
        if (mines[index]) {
            "💣 地雷"
        } else {
            val adjacent = countAdjacentMines(index)
            if (adjacent == 0) "⬜ 空" else "🔢 $adjacent"
        }
    }
    
    // 定义格式化函数
    val formatClick: (Int, Int, String) -> String = { clickNum, index, result ->
        val row = index / gridSize
        val col = index % gridSize
        val icon = when {
            result.contains("💣") -> "❌"
            result.contains("空") -> "✅"
            else -> "ℹ️"
        }
        "$icon${clickNum}步: 点击[$row,$col] → $result"
    }
    
    // 计算游戏结果
    val clicks = playerClicks.mapIndexed { index, cellIndex ->
        val result = checkCell(cellIndex)
        formatClick(index + 1, cellIndex, result)
    }
    
    // 统计结果
    val safeClicks = clicks.count { it.contains("✅") || it.contains("ℹ️") }
    val mineHits = clicks.count { it.contains("❌") }
    val score = (safeClicks * 10) - (mineHits * 50)
    val winRate = (safeClicks * 100) / playerClicks.size
    
    return "🎮 扫雷游戏\n" +
           "━━━━━━━━━━━━━━━━━━━━━\n" +
           "网格大小: ${gridSize}×${gridSize}\n" +
           "总格子数: $totalCells\n" +
           "地雷数量: $mineCount\n\n" +
           "游戏过程:\n" +
           clicks.joinToString("\n") + "\n\n" +
           "━━━━━━━━━━━━━━━━━━━━━\n" +
           "统计数据:\n" +
           "安全点击: $safeClicks 次\n" +
           "踩中地雷: $mineHits 次\n" +
           "安全率: $winRate%\n\n" +
           "得分计算:\n" +
           "安全点击: $safeClicks × 10 = ${safeClicks * 10}\n" +
           "地雷扣分: $mineHits × 50 = ${mineHits * 50}\n" +
           "最终得分: $score 分\n\n" +
           "最终结果: " + when {
               mineHits == 0 -> "🏆 完美!没有踩中任何地雷!"
               mineHits <= 2 -> "🎉 不错!只踩中 $mineHits 个地雷"
               else -> "💪 继续加油!"
           }
}
编译后的 JavaScript 代码
function minesweeperGame() {
  // 定义游戏网格大小
  var gridSize = 5;
  var totalCells = gridSize * gridSize;
  var mineCount = 5;
  
  // 创建游戏网格
  var mines = [];
  for (var i = 0; i < totalCells; i++) {
    mines[i] = false;
  }
  for (var i = 0; i < mineCount; i++) {
    mines[i] = true;
  }
  // 随机打乱
  for (var i = mines.length - 1; i > 0; i--) {
    var j = Math.floor(Math.random() * (i + 1));
    var temp = mines[i];
    mines[i] = mines[j];
    mines[j] = temp;
  }
  
  // 计算每个格子周围的地雷数
  function countAdjacentMines(index) {
    var row = Math.floor(index / gridSize);
    var col = index % gridSize;
    var count = 0;
    
    for (var dr = -1; dr <= 1; dr++) {
      for (var dc = -1; dc <= 1; dc++) {
        if (dr === 0 && dc === 0) continue;
        var newRow = row + dr;
        var newCol = col + dc;
        if (newRow >= 0 && newRow < gridSize && newCol >= 0 && newCol < gridSize) {
          var newIndex = newRow * gridSize + newCol;
          if (mines[newIndex]) count++;
        }
      }
    }
    return count;
  }
  
  // 模拟玩家的点击操作
  var playerClicks = [0, 1, 2, 5, 6, 7, 10, 11, 12, 15, 16, 17, 20, 21, 22];
  
  // 定义点击结果函数
  var checkCell = function(index) {
    if (mines[index]) {
      return '💣 地雷';
    } else {
      var adjacent = countAdjacentMines(index);
      return adjacent === 0 ? '⬜ 空' : '🔢 ' + adjacent;
    }
  };
  
  // 定义格式化函数
  var formatClick = function(clickNum, index, result) {
    var row = Math.floor(index / gridSize);
    var col = index % gridSize;
    var icon;
    if (result.indexOf('💣') !== -1) {
      icon = '❌';
    } else if (result.indexOf('空') !== -1) {
      icon = '✅';
    } else {
      icon = 'ℹ️';
    }
    return icon + ' 第' + clickNum + '步: 点击[' + row + ',' + col + '] → ' + result;
  };
  
  // 计算游戏结果
  var clicks = [];
  for (var i = 0; i < playerClicks.length; i++) {
    var cellIndex = playerClicks[i];
    var result = checkCell(cellIndex);
    clicks.push(formatClick(i + 1, cellIndex, result));
  }
  
  // 统计结果
  var safeClicks = 0, mineHits = 0;
  for (var i = 0; i < clicks.length; i++) {
    if (clicks[i].indexOf('✅') !== -1 || clicks[i].indexOf('ℹ️') !== -1) {
      safeClicks++;
    } else {
      mineHits++;
    }
  }
  
  var score = (safeClicks * 10) - (mineHits * 50);
  var winRate = Math.floor((safeClicks * 100) / playerClicks.length);
  
  // 确定最终结果
  var finalResult;
  if (mineHits === 0) {
    finalResult = '🏆 完美!没有踩中任何地雷!';
  } else if (mineHits <= 2) {
    finalResult = '🎉 不错!只踩中 ' + mineHits + ' 个地雷';
  } else {
    finalResult = '💪 继续加油!';
  }
  
  return '🎮 扫雷游戏\n' +
         '━━━━━━━━━━━━━━━━━━━━━\n' +
         '网格大小: ' + gridSize + '×' + gridSize + '\n' +
         '总格子数: ' + totalCells + '\n' +
         '地雷数量: ' + mineCount + '\n\n' +
         '游戏过程:\n' +
         clicks.join('\n') + '\n\n' +
         '━━━━━━━━━━━━━━━━━━━━━\n' +
         '统计数据:\n' +
         '安全点击: ' + safeClicks + ' 次\n' +
         '踩中地雷: ' + mineHits + ' 次\n' +
         '安全率: ' + winRate + '%\n\n' +
         '得分计算:\n' +
         '安全点击: ' + safeClicks + ' × 10 = ' + (safeClicks * 10) + '\n' +
         '地雷扣分: ' + mineHits + ' × 50 = ' + (mineHits * 50) + '\n' +
         '最终得分: ' + score + ' 分\n\n' +
         '最终结果: ' + finalResult;
}
ArkTS 调用代码
import { minesweeperGame } 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 = minesweeperGame();
      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')
  }
}

编译过程详解

Kotlin 到 JavaScript 的转换

Kotlin 特性 JavaScript 等价物
MutableList 数组 []
shuffle() 随机排序
嵌套循环 嵌套 for 循环
范围检查 (in) 比较操作符
mapIndexed() 带索引的数组循环

关键转换点

  1. 网格表示:一维数组表示二维网格
  2. 坐标转换:索引与行列的相互转换
  3. 邻域遍历:嵌套循环遍历周围 8 个格子
  4. 集合操作:转换为数组操作

游戏扩展

扩展 1:增加难度级别

// 简单:3×3 网格,2 个地雷
val easyGrid = Triple(3, 9, 2)

// 普通:5×5 网格,5 个地雷
val normalGrid = Triple(5, 25, 5)

// 困难:7×7 网格,10 个地雷
val hardGrid = Triple(7, 49, 10)

扩展 2:添加标记功能

val flaggedCells = mutableSetOf<Int>()

fun toggleFlag(index: Int) {
    if (flaggedCells.contains(index)) {
        flaggedCells.remove(index)
    } else {
        flaggedCells.add(index)
    }
}

扩展 3:添加计时功能

val startTime = System.currentTimeMillis()
// ... 游戏逻辑 ...
val endTime = System.currentTimeMillis()
val timeUsed = (endTime - startTime) / 1000

扩展 4:添加自动展开

fun revealEmpty(index: Int, revealed: MutableSet<Int>) {
    if (revealed.contains(index)) return
    revealed.add(index)
    
    if (countAdjacentMines(index) == 0) {
        // 递归展开周围格子
        for (neighbor in getNeighbors(index)) {
            revealEmpty(neighbor, revealed)
        }
    }
}

最佳实践

1. 使用坐标转换函数

// ✅ 好:提取为函数
fun indexToCoord(index: Int, gridSize: Int): Pair<Int, Int> {
    return Pair(index / gridSize, index % gridSize)
}

// ❌ 不好:重复计算
val row = index / gridSize
val col = index % gridSize

2. 使用范围检查

// ✅ 好:使用 in 操作符
if (newRow in 0 until gridSize && newCol in 0 until gridSize)

// ❌ 不好:多个比较
if (newRow >= 0 && newRow < gridSize && newCol >= 0 && newCol < gridSize)

3. 使用 mapIndexed()

// ✅ 好:使用 mapIndexed()
val clicks = playerClicks.mapIndexed { index, cellIndex -> /* ... */ }

// ❌ 不好:使用 for 循环
for (i in playerClicks.indices) {
    val cellIndex = playerClicks[i]
}

4. 提取复杂逻辑

// ✅ 好:提取为函数
fun countAdjacentMines(index: Int): Int { /* ... */ }

// ❌ 不好:内联复杂逻辑
val adjacent = /* 复杂的计算 */

常见问题

Q1: 如何实现自动展开空白区域?

A: 使用递归或队列实现:

fun revealEmpty(index: Int, revealed: MutableSet<Int>) {
    if (revealed.contains(index) || mines[index]) return
    revealed.add(index)
    
    if (countAdjacentMines(index) == 0) {
        for (neighbor in getNeighbors(index)) {
            revealEmpty(neighbor, revealed)
        }
    }
}

Q2: 如何处理游戏结束条件?

A: 检查是否踩中地雷或全部安全格子都被揭示:

val gameOver = mineHits > 0 || safeClicks == totalCells - mineCount

Q3: 如何实现难度选择?

A: 使用参数化函数:

fun minesweeperGame(difficulty: String): String {
    val (gridSize, mineCount) = when (difficulty) {
        "easy" -> Pair(3, 2)
        "normal" -> Pair(5, 5)
        "hard" -> Pair(7, 10)
        else -> Pair(5, 5)
    }
    // ... 游戏逻辑 ...
}

Q4: 如何优化性能?

A:

  1. 预计算邻域地雷数
  2. 使用位操作表示网格状态
  3. 缓存计算结果

Q5: 如何实现网络对战?

A: 使用 WebSocket 同步游戏状态:

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

总结

关键要点

  • ✅ 使用一维数组表示二维网格
  • ✅ 实现坐标转换函数
  • ✅ 计算邻域地雷数量
  • ✅ 使用 mapIndexed() 处理操作
  • ✅ KMP 能无缝编译到 JavaScript

下一步

  1. 实现自动展开功能
  2. 添加标记/取消标记功能
  3. 实现难度选择
  4. 添加排行榜系统
  5. 实现网络对战

参考资源

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐