KMP 实现鸿蒙跨端:Kotlin 小游戏 - 扫雷游戏
·
目录
概述
本文档介绍如何在 Kotlin Multiplatform (KMP) 鸿蒙跨端开发中实现一个经典的扫雷游戏。这个案例展示了如何使用 Kotlin 的二维网格处理、邻域计算和游戏逻辑来创建一个完整的策略游戏系统。通过 KMP,这个游戏可以无缝编译到 JavaScript,在 OpenHarmony 应用中运行。
游戏的特点
- 经典玩法:传统的扫雷游戏规则
- 网格处理:实现二维网格的坐标转换
- 邻域计算:计算周围地雷数量
- 跨端兼容:一份 Kotlin 代码可同时服务多个平台
- 详细统计:显示安全率、得分和评价
游戏规则
基本规则
- 游戏网格:
- 5×5 的网格(25 个格子)
- 其中 5 个格子有地雷
- 其他 20 个格子是安全的
- 游戏流程:
- 玩家点击格子
- 如果是地雷,游戏结束
- 如果是安全格子,显示周围地雷数
- 继续点击直到游戏结束
- 计分规则:
- 安全点击:+10 分
- 踩中地雷:-50 分
- 评价标准:
- 完美:没有踩中任何地雷
- 不错:只踩中 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() | 带索引的数组循环 |
关键转换点
- 网格表示:一维数组表示二维网格
- 坐标转换:索引与行列的相互转换
- 邻域遍历:嵌套循环遍历周围 8 个格子
- 集合操作:转换为数组操作
游戏扩展
扩展 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:
- 预计算邻域地雷数
- 使用位操作表示网格状态
- 缓存计算结果
Q5: 如何实现网络对战?
A: 使用 WebSocket 同步游戏状态:
external class WebSocket(url: String) {
fun send(data: String)
var onmessage: ((String) -> Unit)?
}
总结
关键要点
- ✅ 使用一维数组表示二维网格
- ✅ 实现坐标转换函数
- ✅ 计算邻域地雷数量
- ✅ 使用 mapIndexed() 处理操作
- ✅ KMP 能无缝编译到 JavaScript
下一步
- 实现自动展开功能
- 添加标记/取消标记功能
- 实现难度选择
- 添加排行榜系统
- 实现网络对战
参考资源
- Kotlin 官方文档 - 集合操作
- Kotlin 官方文档 - 范围
- KMP 官方文档
- 鸿蒙 ArkTS 官方文档
- 经典扫雷游戏
- 欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)