在这里插入图片描述

目录

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

概述

本文档介绍如何在 Kotlin Multiplatform (KMP) 鸿蒙跨端开发中实现一个完整的迷宫生成和求解系统。这个案例展示了如何使用 Kotlin 的递归算法、图论和搜索算法来创建一个功能丰富的迷宫工具。通过 KMP,这个工具可以无缝编译到 JavaScript,在 OpenHarmony 应用中运行,并支持用户输入进行实时处理。

工具的特点

  • 迷宫生成:使用递归回溯算法生成随机迷宫
  • 路径求解:使用 BFS 算法找到最短路径
  • 可视化展示:用 ASCII 字符展示迷宫和路径
  • 统计分析:分析迷宫的结构特性
  • 跨端兼容:一份 Kotlin 代码可同时服务多个平台

工具功能

1. 迷宫生成

  • 大小范围:支持 3×3 到 10×10 的迷宫
  • 递归回溯:使用高效的递归回溯算法
  • 随机性:每次生成不同的迷宫

2. 迷宫统计

  • 总格子数:计算迷宫的总格子数
  • 墙数量:统计墙的个数和百分比
  • 通路数:统计通路的个数和百分比

3. 路径求解

  • BFS 搜索:使用广度优先搜索找最短路径
  • 路径长度:显示从起点到终点的步数
  • 可解性:判断迷宫是否有解

4. 可视化展示

  • 迷宫图:用 ASCII 字符展示迷宫
  • 解决方案:用不同符号标记求解路径
  • 清晰标记:S 表示起点,E 表示终点

核心实现

1. 迷宫网格初始化

val maze = Array(size) { IntArray(size) { 1 } }  // 1=墙, 0=通路
val visited = Array(size) { BooleanArray(size) { false } }

这段代码初始化了迷宫的基础数据结构。创建一个 size × size 的二维整数数组 maze,所有元素初始化为 1(表示墙)。创建一个同样大小的布尔数组 visited,用于追踪在递归回溯过程中已访问的单元格。这种初始化方式确保了迷宫最初是完全被墙包围的,然后通过递归回溯算法逐步打通通路。

2. 递归回溯生成迷宫

fun carvePath(x: Int, y: Int) {
    visited[x][y] = true
    maze[x][y] = 0
    
    val directions = listOf(
        Pair(-2, 0),  // 上
        Pair(0, 2),   // 右
        Pair(2, 0),   // 下
        Pair(0, -2)   // 左
    )
    
    val shuffled = directions.shuffled()
    
    for ((dx, dy) in shuffled) {
        val nx = x + dx
        val ny = y + dy
        
        if (nx in 0 until size && ny in 0 until size && !visited[nx][ny]) {
            maze[x + dx / 2][y + dy / 2] = 0  // 打通中间的墙
            carvePath(nx, ny)
        }
    }
}

这段代码实现了递归回溯算法来生成迷宫。首先标记当前单元格为已访问,并将其设置为通路(0)。定义四个方向的移动向量,每次移动 2 个单位以保留墙。使用 shuffled() 随机打乱方向顺序,确保生成的迷宫具有随机性。对于每个方向,检查目标单元格是否在边界内且未被访问。如果满足条件,打通当前单元格和目标单元格之间的中间墙,然后递归地对目标单元格调用 carvePath()。这个算法生成的迷宫具有完美的连通性,即每个单元格都可以从任何其他单元格到达。

3. 迷宫统计

val totalCells = size * size
val wallCount = maze.sumOf { row -> row.count { it == 1 } }
val pathCount = totalCells - wallCount
val wallPercent = (wallCount * 100) / totalCells
val pathPercent = (pathCount * 100) / totalCells

这段代码计算迷宫的统计信息。计算总格子数为 size × size。使用 sumOf()count() 统计所有值为 1 的单元格(墙)的数量。计算通路数为总格子数减去墙数。计算墙和通路的百分比。这些统计数据可以用来评估迷宫的难度和结构特性。

4. BFS 路径求解

fun solveMaze(): List<Pair<Int, Int>>? {
    val queue = mutableListOf<Pair<Int, Int>>()
    val parent = mutableMapOf<Pair<Int, Int>, Pair<Int, Int>?>()
    val visited2 = Array(size) { BooleanArray(size) { false } }
    
    queue.add(Pair(0, 0))
    visited2[0][0] = true
    parent[Pair(0, 0)] = null
    
    while (queue.isNotEmpty()) {
        val (x, y) = queue.removeAt(0)
        
        if (x == size - 1 && y == size - 1) {
            // 回溯路径
            val path = mutableListOf<Pair<Int, Int>>()
            var current: Pair<Int, Int>? = Pair(x, y)
            
            while (current != null) {
                path.add(0, current)
                current = parent[current]
            }
            return path
        }
        
        // 探索四个方向
        for ((dx, dy) in directions) {
            val nx = x + dx
            val ny = y + dy
            
            if (nx in 0 until size && ny in 0 until size && 
                !visited2[nx][ny] && maze[nx][ny] == 0) {
                visited2[nx][ny] = true
                parent[Pair(nx, ny)] = Pair(x, y)
                queue.add(Pair(nx, ny))
            }
        }
    }
    
    return null
}

这段代码实现了广度优先搜索(BFS)算法来求解迷宫。创建一个队列用于存储待探索的单元格,创建一个 Map 用于存储每个单元格的父节点(用于路径回溯),创建一个布尔数组追踪已访问的单元格。初始化起点 (0, 0) 到队列中。使用 while 循环处理队列中的每个单元格。当到达终点 (size-1, size-1) 时,通过父节点 Map 回溯路径。对于每个单元格,探索四个方向的相邻单元格。如果相邻单元格在边界内、未被访问且是通路,则标记为已访问、记录父节点、添加到队列。如果队列为空且未找到终点,则返回 null(迷宫无解)。BFS 算法保证找到的是最短路径。

5. ASCII 可视化

val mazeStr = maze.mapIndexed { i, row ->
    "  " + row.mapIndexed { j, cell ->
        when {
            i == 0 && j == 0 -> "S"      // 起点
            i == size - 1 && j == size - 1 -> "E"  // 终点
            cell == 1 -> "█"              // 墙
            else -> " "                   // 通路
        }
    }.joinToString("")
}.joinToString("\n")

这段代码将迷宫转换为 ASCII 字符串用于显示。使用 mapIndexed() 遍历迷宫的每一行和每一列。对于每个单元格,使用 when 表达式进行条件判断。起点 (0, 0) 显示为 “S”。终点 (size-1, size-1) 显示为 “E”。墙(值为 1)显示为 “█” 符号。通路(值为 0)显示为空格。使用 joinToString() 将每行的字符连接成字符串,然后将所有行用换行符连接。这种可视化方式使得迷宫结构清晰易读。


实战案例

案例:完整的迷宫生成和求解器

Kotlin 源代码
@OptIn(ExperimentalJsExport::class)
@JsExport
fun mazeGenerator(inputSize: String = "5"): String {
    // 解析输入
    val size = inputSize.trim().toIntOrNull() ?: 5
    
    if (size < 3) {
        return "❌ 错误: 迷宫大小必须至少为 3"
    }
    
    if (size > 10) {
        return "❌ 错误: 迷宫过大"
    }
    
    // 1. 创建迷宫网格
    val maze = Array(size) { IntArray(size) { 1 } }
    val visited = Array(size) { BooleanArray(size) { false } }
    
    // 2. 生成迷宫
    fun carvePath(x: Int, y: Int) {
        visited[x][y] = true
        maze[x][y] = 0
        
        val directions = listOf(
            Pair(-2, 0), Pair(0, 2), Pair(2, 0), Pair(0, -2)
        )
        
        for ((dx, dy) in directions.shuffled()) {
            val nx = x + dx
            val ny = y + dy
            
            if (nx in 0 until size && ny in 0 until size && !visited[nx][ny]) {
                maze[x + dx / 2][y + dy / 2] = 0
                carvePath(nx, ny)
            }
        }
    }
    
    carvePath(0, 0)
    maze[0][0] = 0
    maze[size - 1][size - 1] = 0
    
    // 3. 统计信息
    val totalCells = size * size
    val wallCount = maze.sumOf { row -> row.count { it == 1 } }
    val pathCount = totalCells - wallCount
    
    // 4. 求解迷宫
    fun solveMaze(): List<Pair<Int, Int>>? {
        val queue = mutableListOf<Pair<Int, Int>>()
        val parent = mutableMapOf<Pair<Int, Int>, Pair<Int, Int>?>()
        val visited2 = Array(size) { BooleanArray(size) { false } }
        
        queue.add(Pair(0, 0))
        visited2[0][0] = true
        parent[Pair(0, 0)] = null
        
        while (queue.isNotEmpty()) {
            val (x, y) = queue.removeAt(0)
            
            if (x == size - 1 && y == size - 1) {
                val path = mutableListOf<Pair<Int, Int>>()
                var current: Pair<Int, Int>? = Pair(x, y)
                while (current != null) {
                    path.add(0, current)
                    current = parent[current]
                }
                return path
            }
            
            for ((dx, dy) in listOf(Pair(-1, 0), Pair(0, 1), Pair(1, 0), Pair(0, -1))) {
                val nx = x + dx
                val ny = y + dy
                
                if (nx in 0 until size && ny in 0 until size && 
                    !visited2[nx][ny] && maze[nx][ny] == 0) {
                    visited2[nx][ny] = true
                    parent[Pair(nx, ny)] = Pair(x, y)
                    queue.add(Pair(nx, ny))
                }
            }
        }
        
        return null
    }
    
    val solution = solveMaze()
    val solutionLength = solution?.size ?: 0
    
    // 5. 生成输出
    return "🎮 迷宫生成和求解器\n" +
           "━━━━━━━━━━━━━━━━━━━━━\n" +
           "1️⃣ 迷宫信息:\n" +
           "  大小: ${size}×${size}\n" +
           "  总格子数: $totalCells\n" +
           "  墙数量: $wallCount\n" +
           "  通路数: $pathCount\n\n" +
           "2️⃣ 求解信息:\n" +
           "  是否可解: ${if (solution != null) "✅ 是" else "❌ 否"}\n" +
           "  路径长度: $solutionLength 步\n\n" +
           "✅ 生成完成!"
}

这是迷宫生成和求解的核心实现函数,展示了完整的迷宫处理算法。函数使用 @OptIn(ExperimentalJsExport::class)@JsExport 注解,使其可以被编译成 JavaScript 并在 ArkTS 中调用。函数首先解析输入字符串,获取迷宫大小,并验证大小在 3 到 10 之间。创建迷宫网格和访问追踪数组,初始化所有单元格为墙。定义 carvePath() 内部函数实现递归回溯算法生成迷宫。调用 carvePath(0, 0) 从起点开始生成迷宫,然后确保起点和终点都是通路。计算迷宫的统计信息,包括总格子数、墙数和通路数。定义 solveMaze() 内部函数实现 BFS 算法求解迷宫。获取求解结果和路径长度。最后使用字符串模板格式化输出完整的分析报告,包括迷宫信息和求解信息。这个函数展示了如何将复杂的迷宫算法转换为用户友好的输出。

ArkTS 调用代码(带输入框)
import { mazeGenerator } from './hellokjs';

@Entry
@Component
struct Index {
  @State message: string = '加载中...';
  @State results: string[] = [];
  @State caseTitle: string = '迷宫生成和求解器';
  @State inputText: string = '5';

  aboutToAppear(): void {
    this.loadResults();
  }

  loadResults(): void {
    try {
      const results: string[] = [];
      const algorithmResult: string = mazeGenerator(this.inputText);
      results.push(algorithmResult);
      
      this.results = results;
      this.message = '✓ 生成完成';
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      this.message = `✗ 错误: ${errorMessage}`;
    }
  }

这段 ArkTS 代码是鸿蒙应用的用户界面实现,展示了如何在 ArkTS 中导入和调用编译后的 Kotlin 函数。首先使用 import 语句从编译后的 JavaScript 模块中导入 mazeGenerator 函数。使用 @State 装饰器定义四个响应式状态变量:message 用于显示状态信息,results 存储迷宫生成结果,caseTitle 存储案例标题,inputText 存储用户输入的迷宫大小。aboutToAppear() 生命周期函数在组件加载时自动调用 loadResults() 进行初始化。loadResults() 方法调用 Kotlin 编译的 JavaScript 函数 mazeGenerator(),将用户输入的迷宫大小传入,获取迷宫生成和求解的结果,然后更新 resultsmessage。使用 try-catch 块捕获异常,如果发生错误则显示错误信息。这个结构实现了清晰的前后端职责划分:ArkTS 只负责收集输入和展示结果,所有迷宫生成和求解逻辑都集中在 Kotlin 中。

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)

  // 输入框区域
  Column() {
    Text('输入迷宫大小 (3-10):')
      .fontSize(14)
      .fontWeight(FontWeight.Bold)
      .fontColor('#1f2937')
      .margin({ bottom: 8 })
    
    TextInput({ placeholder: '输入迷宫的大小...', text: this.inputText })
      .width('100%')
      .height(60)
      .padding(12)
      .border({ width: 1, color: '#d1d5db' })
      .borderRadius(6)
      .onChange((value: string) => {
        this.inputText = value
      })
    
    Button('生成')
      .width('100%')
      .height(40)
      .margin({ top: 12 })
      .backgroundColor('#3b82f6')
      .fontColor(Color.White)
      .onClick(() => {
        this.loadResults()
      })
  }
  .width('100%')
  .padding({ left: 16, right: 16, bottom: 16 })

  // 结果显示区域
  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('示例 (5)')
      .width('48%')
      .height(44)
      .backgroundColor('#10b981')
      .fontColor(Color.White)
      .fontSize(14)
      .onClick(() => {
        this.inputText = '5'
        this.loadResults()
      })

    Button('清空')
      .width('48%')
      .height(44)
      .backgroundColor('#6b7280')
      .fontColor(Color.White)
      .fontSize(14)
      .onClick(() => {
        this.inputText = ''
        this.results = []
      })
  }
  .width('100%')
  .padding({ left: 16, right: 16, bottom: 20 })
}
.width('100%')
.height('100%')
.backgroundColor('#f9fafb')

}
}

---

## 编译过程详解

### Kotlin 到 JavaScript 的转换

| Kotlin 特性 | JavaScript 等价物 |
|-----------|-----------------|
| Array | 数组 [] |
| IntArray | 嵌套数组 |
| BooleanArray | 嵌套布尔数组 |
| shuffled() | 数组排序 |
| mutableListOf | 数组 [] |
| Pair | 对象或数组 |

### 关键转换点

1. **多维数组**:转换为嵌套数组
2. **集合操作**:转换为数组方法
3. **递归算法**:直接转换为 JavaScript 递归
4. **BFS 搜索**:转换为队列实现

---

## 工具扩展

### 扩展 1:添加 DFS 求解

```kotlin
fun solveMazeDFS(): List<Pair<Int, Int>>? {
    val visited = Array(size) { BooleanArray(size) { false } }
    val path = mutableListOf<Pair<Int, Int>>()
    
    fun dfs(x: Int, y: Int): Boolean {
        if (x == size - 1 && y == size - 1) {
            path.add(Pair(x, y))
            return true
        }
        
        if (visited[x][y] || maze[x][y] == 1) return false
        
        visited[x][y] = true
        path.add(Pair(x, y))
        
        for ((dx, dy) in directions) {
            if (dfs(x + dx, y + dy)) return true
        }
        
        path.removeAt(path.size - 1)
        return false
    }
    
    return if (dfs(0, 0)) path else null
}

这段代码实现了深度优先搜索(DFS)算法来求解迷宫。创建一个访问追踪数组和一个路径列表。定义 dfs() 递归函数用于深度优先搜索。当到达终点时,将其添加到路径并返回 true。如果当前单元格已访问或是墙,则返回 false。标记当前单元格为已访问,将其添加到路径。递归地探索四个方向,如果任何方向找到解决方案则返回 true。如果所有方向都失败,则从路径中移除当前单元格(回溯)并返回 false。与 BFS 不同,DFS 找到的不一定是最短路径,但代码更简洁。这个扩展展示了如何实现不同的搜索算法。

扩展 2:添加迷宫难度评分

fun calculateDifficulty(): Int {
    val wallRatio = (wallCount * 100) / totalCells
    val pathLength = solution?.size ?: 0
    
    return when {
        wallRatio > 70 && pathLength > size * 2 -> 5  // 困难
        wallRatio > 60 && pathLength > size -> 4      // 中等偏难
        wallRatio > 50 -> 3                            // 中等
        wallRatio > 40 -> 2                            // 简单
        else -> 1                                      // 非常简单
    }
}

这段代码实现了迷宫难度评分函数。计算墙的比例,即墙的数量占总格子数的百分比。获取求解路径的长度。使用 when 表达式根据墙比例和路径长度进行难度评分。墙比例越高、路径越长,迷宫难度越高。返回 1 到 5 的难度等级。这个评分函数可以帮助用户了解迷宫的复杂程度,从而选择合适的难度。

扩展 3:添加多个出口

fun generateMultipleExits(exitCount: Int): List<Pair<Int, Int>> {
    val exits = mutableListOf<Pair<Int, Int>>()
    exits.add(Pair(size - 1, size - 1))
    
    for (i in 1 until exitCount) {
        val x = (Math.random() * size).toInt()
        val y = (Math.random() * size).toInt()
        if (maze[x][y] == 0) {
            exits.add(Pair(x, y))
        }
    }
    
    return exits
}

这段代码实现了生成多个出口的功能。创建一个出口列表,首先添加默认的终点出口 (size-1, size-1)。循环生成指定数量的额外出口。对于每个额外出口,随机选择一个坐标。检查该坐标是否是通路(值为 0),如果是则添加到出口列表。这个功能允许迷宫有多个可能的终点,增加了迷宫的复杂性和趣味性。

扩展 4:添加迷宫导出

fun exportMazeAsString(): String {
    return maze.joinToString("\n") { row ->
        row.joinToString("") { if (it == 1) "1" else "0" }
    }
}

这段代码实现了迷宫导出功能,将迷宫转换为字符串格式。使用 joinToString() 遍历迷宫的每一行。对于每一行,再次使用 joinToString() 将每个单元格转换为字符串。如果单元格值为 1(墙),则转换为 “1”;如果为 0(通路),则转换为 “0”。使用换行符连接所有行。这种格式便于保存、传输或在其他系统中导入迷宫数据。


最佳实践

1. 使用递归回溯生成迷宫

// ✅ 好:递归回溯
fun carvePath(x: Int, y: Int) {
    visited[x][y] = true
    maze[x][y] = 0
    for ((dx, dy) in directions.shuffled()) {
        // ...
    }
}

// ❌ 不好:简单随机
val maze = Array(size) { IntArray(size) { if (Math.random() > 0.5) 1 else 0 } }

这段代码对比了两种迷宫生成方法。第一种方法使用递归回溯算法,这是生成完美迷宫的标准方法。递归回溯确保迷宫的每个单元格都可以从任何其他单元格到达,生成的迷宫具有良好的连通性和复杂性。第二种方法使用简单随机,随机决定每个单元格是墙还是通路。这种方法会产生不连通的迷宫,可能有多个独立的区域,不适合作为真正的迷宫。递归回溯是生成高质量迷宫的最佳实践。

2. 使用 BFS 找最短路径

// ✅ 好:BFS 找最短路径
fun solveMaze(): List<Pair<Int, Int>>? {
    val queue = mutableListOf<Pair<Int, Int>>()
    // ...
}

// ❌ 不好:DFS 可能不是最短
fun solveMazeDFS(): List<Pair<Int, Int>>? {
    // ...
}

这段代码对比了两种迷宫求解方法。第一种方法使用 BFS(广度优先搜索),这是找最短路径的标准方法。BFS 按层级探索,保证找到的第一条路径是最短的。第二种方法使用 DFS(深度优先搜索),它会深入探索一条路径直到死胡同,然后回溯。DFS 找到的路径可能很长,不一定是最短的。对于迷宫求解,BFS 是最佳实践,因为用户通常希望找到最短的出路。

3. 检查边界条件

// ✅ 好:检查大小范围
if (size < 3 || size > 10) {
    return "错误: 大小必须在 3-10 之间"
}

// ❌ 不好:不检查
val maze = Array(size) { IntArray(size) { 1 } }

这段代码对比了有无边界检查的差异。第一种方法在创建迷宫前检查大小是否在有效范围内(3-10)。如果大小无效,则返回错误信息。这样可以防止创建过小(无法形成有意义的迷宫)或过大(导致性能问题)的迷宫。第二种方法直接创建迷宫而不检查,可能导致异常或性能问题。边界检查是健壮代码的必要部分。

4. 使用合适的数据结构

// ✅ 好:使用 Pair 表示坐标
val path = mutableListOf<Pair<Int, Int>>()

// ❌ 不好:使用数组
val path = mutableListOf<IntArray>()

这段代码对比了两种表示坐标的方式。第一种方法使用 Pair<Int, Int> 表示坐标,这是 Kotlin 标准库提供的数据结构,语义清晰,易于理解和使用。可以直接访问 pair.firstpair.second 获取 x 和 y 坐标。第二种方法使用 IntArray 表示坐标,虽然也能工作,但语义不清晰,需要通过索引访问,容易出错。使用合适的数据结构可以提高代码的可读性和可维护性。


常见问题

Q1: 为什么使用递归回溯而不是其他算法?

A: 递归回溯能生成完美迷宫(每个格子都可达),且算法简单高效。

Q2: BFS 和 DFS 的区别是什么?

A:

  • BFS:找最短路径,使用队列
  • DFS:找任意路径,使用栈或递归

Q3: 迷宫大小为什么限制在 10×10?

A: 为了性能考虑。更大的迷宫会导致计算时间过长。

Q4: 如何优化迷宫生成性能?

A:

  • 使用迭代而不是递归
  • 减少随机数生成
  • 使用位操作代替数组

Q5: 如何处理无解的迷宫?

A: 使用 BFS 搜索,如果队列为空且未找到出口,则迷宫无解。


总结

关键要点

  • ✅ 使用递归回溯生成迷宫
  • ✅ 使用 BFS 找最短路径
  • ✅ 检查边界条件和特殊情况
  • ✅ 使用合适的数据结构
  • ✅ KMP 能无缝编译到 JavaScript

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐