鸿蒙开发案例:围住神经猫

时间:2024-10-22 14:49:41

一个基于网格的游戏环境,其中包含了一个名为“猫咪”的角色。游戏中使用了一个9x9的网格,每个单元格可以是空闲的(值为0)或者被设置为墙壁(值为1)。游戏的目标是让“猫咪”在一个充满墙壁的迷宫中移动,避免被墙壁围困。

【主要功能】

• 初始化棋盘并设置每个单元格的邻居关系。

• 开始游戏时随机放置墙壁,并将猫咪放置在指定位置。

• 当猫咪尝试移动时,寻找所有可移动的空邻居,并根据一定的策略选择下一步移动的方向。

• 计算启发式值(使用曼哈顿距离)来帮助决定移动方向。

• 构建用户界面,显示背景和猫咪的位置,并允许玩家通过点击放置墙壁并触发猫咪的移动。

【开发环境】

开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.814

工程API版本:12

【算法分析】

1. 广度优先搜索(BFS):在 findNeighbors 方法中,通过遍历当前单元格的邻居来获取周围非墙壁且可以移动的单元格集合。这类似于广度优先搜索的思想,逐层遍历邻居单元格。

findNeighbors(cell: Cell): Cell[] {
    let neighbors: Cell[] = [];
    // 检查当前单元格的六个方向邻居,将非墙壁且可以移动的单元格加入集合
    // ...
    return neighbors;
}

2. 启发式搜索:在 selectNextMove 方法中,根据一定的启发式函数选择下一个移动位置,以确保小猫朝着离边界最近的方向移动。这种启发式搜索可以帮助小猫更智能地选择下一步的移动位置。

selectNextMove(emptyNeighbors: Cell[]): Cell {
    // 根据启发式函数选择最优的移动位置
    // ...
    return closestToEdge || emptyNeighbors[0];
}

3. 曼哈顿距离计算:在 computeHeuristic 方法中,使用曼哈顿距离计算启发式函数的值,以评估当前单元格到边界的距离。曼哈顿距离是在网格上两点之间的距离,沿着网格的边缘移动。

computeHeuristic(cell: Cell): number {
    // 计算曼哈顿距离作为启发式函数的值
    // ...
    return minDistanceX + minDistanceY;
}

【完整代码】

import { promptAction } from '@kit.ArkUI'

@ObservedV2
class Cell {
  @Trace value: number = 0; // 0 表示空位,1 表示墙壁
  x: number = 0;
  y: number = 0;
  leftNeighborIndex: number | undefined = undefined; // 左邻居索引
  rightNeighborIndex: number | undefined = undefined; // 右邻居索引
  leftTopNeighborIndex: number | undefined = undefined; // 左上邻居索引
  rightTopNeighborIndex: number | undefined = undefined; // 右上邻居索引
  leftBottomNeighborIndex: number | undefined = undefined; // 左下邻居索引
  rightBottomNeighborIndex: number | undefined = undefined; // 右下邻居索引

  constructor(value: number, x: number, y: number) {
    this.value = value;
    this.x = x;
    this.y = y;
  }
}

@Entry
@Component
struct Index {
  @State gridCells: Cell[] = []; // 保存所有单元格的数组
  cellWidth: number = 70; // 单元格宽度
  borderPieceWidth: number = -10; // 边缘件宽度
  pieceSize: number = 65; // 件大小
  @State catPositionIndex: number = 40; // 小猫位置索引

  aboutToAppear(): void {
    this.initializeBoard();
    this.startGame();
  }

  findNeighbors(cell: Cell): Cell[] { // 获取当前单元格周围非 undefined 且可以移动的集合
    let neighbors: Cell[] = [];
    if (cell.leftNeighborIndex !== undefined && this.gridCells[cell.leftNeighborIndex].value === 0) {
      neighbors.push(this.gridCells[cell.leftNeighborIndex]);
    }
    if (cell.rightNeighborIndex !== undefined && this.gridCells[cell.rightNeighborIndex].value === 0) {
      neighbors.push(this.gridCells[cell.rightNeighborIndex]);
    }
    if (cell.leftTopNeighborIndex !== undefined && this.gridCells[cell.leftTopNeighborIndex].value === 0) {
      neighbors.push(this.gridCells[cell.leftTopNeighborIndex]);
    }
    if (cell.rightTopNeighborIndex !== undefined && this.gridCells[cell.rightTopNeighborIndex].value === 0) {
      neighbors.push(this.gridCells[cell.rightTopNeighborIndex]);
    }
    if (cell.leftBottomNeighborIndex !== undefined && this.gridCells[cell.leftBottomNeighborIndex].value === 0) {
      neighbors.push(this.gridCells[cell.leftBottomNeighborIndex]);
    }
    if (cell.rightBottomNeighborIndex !== undefined && this.gridCells[cell.rightBottomNeighborIndex].value === 0) {
      neighbors.push(this.gridCells[cell.rightBottomNeighborIndex]);
    }
    return neighbors;
  }

  initializeBoard() {
    this.gridCells = [];
    for (let rowIndex = 0; rowIndex < 9; rowIndex++) {
      for (let columnIndex = 0; columnIndex < 9; columnIndex++) {
        this.gridCells.push(new Cell(0, rowIndex, columnIndex));
      }
    }
    // 设置每个单元格的邻居
    for (let rowIndex = 0; rowIndex < 9; rowIndex++) {
      for (let columnIndex = 0; columnIndex < 9; columnIndex++) {
        let cellIndex: number = rowIndex * 9 + columnIndex;
        const row = rowIndex;
        const column = columnIndex;
        let cell = this.gridCells[cellIndex];
        // 检查六个方向的邻居
        if (column > 0) {
          cell.leftNeighborIndex = cellIndex - 1; // 左
        }
        if (column < 8) {
          cell.rightNeighborIndex = cellIndex + 1; // 右
        }
        // 根据行数的不同,选择不同的邻居位置
        if (row % 2 === 1) {
          let leftTopIndex = cellIndex - 9;
          if (leftTopIndex >= 0) {
            cell.leftTopNeighborIndex = leftTopIndex; // 左上
          }
          let leftBottomIndex = cellIndex + 9;
          if (leftBottomIndex < this.gridCells.length) {
            cell.leftBottomNeighborIndex = leftBottomIndex; // 左下
          }
          let rightTopIndex = cellIndex - 8;
          if (rightTopIndex >= 0) {
            cell.rightTopNeighborIndex = rightTopIndex; // 右上
          }
          let rightBottomIndex = cellIndex + 10;
          if (rightBottomIndex < this.gridCells.length) {
            cell.rightBottomNeighborIndex = rightBottomIndex; // 右下
          }
        } else {
          let leftTopIndex = cellIndex - 10;
          if (leftTopIndex >= 0) {
            cell.leftTopNeighborIndex = leftTopIndex; // 左上
          }
          let leftBottomIndex = cellIndex + 8;
          if (leftBottomIndex < this.gridCells.length) {
            cell.leftBottomNeighborIndex = leftBottomIndex; // 左下
          }
          let rightTopIndex = cellIndex - 9;
          if (rightTopIndex >= 0) {
            cell.rightTopNeighborIndex = rightTopIndex; // 右上
          }
          let rightBottomIndex = cellIndex + 9;
          if (rightBottomIndex < this.gridCells.length) {
            cell.rightBottomNeighborIndex = rightBottomIndex; // 右下
          }
        }
      }
    }
  }

  startGame() {
    let availableIndices: number[] = [];
    for (let i = 0; i < 81; i++) {
      this.gridCells[i].value = 0;
      if (i === 39 || i === 40 || i === 41) { // 排除中心点及左右两点,避免一下子就被围住直接游戏结束
        continue;
      }
      availableIndices.push(i);
    }
    // 随机生成墙壁
    for (let i = 0; i < 8; i++) {
      let randomIndex = Math.floor(Math.random() * availableIndices.length);
      let randomNeighbor = availableIndices[randomIndex];
      this.gridCells[randomNeighbor].value = 1;
      availableIndices.splice(randomIndex, 1); // 移除已使用的索引
    }
    this.catPositionIndex = 40;
  }

  moveCat(): void {
    let neighbors = this.findNeighbors(this.gridCells[this.catPositionIndex]);
    let emptyNeighbors: Cell[] = neighbors.filter(neighbor => neighbor.value === 0); // 仅保留空位邻居
    if (emptyNeighbors.length === 0) {
      console.log('神经猫被围住了,游戏结束!');
      promptAction.showDialog({
        title: '游戏胜利!', // 对话框标题
        buttons: [{ text: '重新开始', color: '#ffa500' }] // 对话框按钮
      }).then(() => { // 对话框关闭后执行
        this.startGame(); // 重新开始游戏
      });
    } else {
      // 根据一定策略选择下一个移动位置
      let nextMove = this.selectNextMove(emptyNeighbors);
      // 清除原来的位置,并且更新猫的位置和状态
      this.catPositionIndex = nextMove.x * 9 + nextMove.y;
      // 检查小猫是否移动到边界
      if (nextMove.x === 0 || nextMove.x === 8 || nextMove.y === 0 || nextMove.y === 8) {
        console.log('小猫移动到了边界,游戏结束!');
        // 在此处添加游戏结束的逻辑,例如重置游戏或显示游戏结束提示
        promptAction.showDialog({
          title: '游戏失败!', // 对话框标题
          buttons: [{ text: '重新开始', color: '#ffa500' }] // 对话框按钮
        }).then(() => { // 对话框关闭后执行
          this.startGame(); // 重新开始游戏
        });
      }
    }
  }

  selectNextMove(emptyNeighbors: Cell[]): Cell {
    let closestToEdge: Cell | null = null;
    let minDistanceToEdge: number = Number.MAX_VALUE;
    for (let neighbor of emptyNeighbors) {
      let distanceToEdge = Math.min(neighbor.x, 8 - neighbor.x, neighbor.y, 8 - neighbor.y);
      if (distanceToEdge < minDistanceToEdge) {
        minDistanceToEdge = distanceToEdge;
        closestToEdge = neighbor;
      } else if (distanceToEdge === minDistanceToEdge) {
        // 如果距离相同,根据启发式函数选择更靠近边界的邻居
        if (this.computeHeuristic(neighbor) < this.computeHeuristic(closestToEdge as Cell)) {
          closestToEdge = neighbor;
        }
      }
    }
    return closestToEdge || emptyNeighbors[0]; // 返回最靠近边界的一个空位邻居,如果没有则返回第一个空位邻居
  }

  computeHeuristic(cell: Cell): number {
    // 曼哈顿距离
    let minDistanceX = Math.min(...[0, 8].map(x => Math.abs(cell.x - x)));
    let minDistanceY = Math.min(...[0, 8].map(y => Math.abs(cell.y - y)));
    return minDistanceX + minDistanceY;
  }

  build() {
    Column({ space: 20 }) {
      Stack() {
        Flex({ wrap: FlexWrap.Wrap }) {
          //背景
          ForEach(this.gridCells, (item: Cell, index: number) => {
            Stack() {
              Text().width(`${this.pieceSize}lpx`).height(`${this.pieceSize}lpx`)
                .backgroundColor(item.value == 0 ? "#b4b4b4" : "#ff8d5a").borderRadius('50%')
            }.width(`${this.cellWidth}lpx`).height(`${this.cellWidth}lpx`)
            .margin({
              left: `${(index + 9) % 18 == 0 ? this.cellWidth / 2 : 0}lpx`,
              top: `${this.borderPieceWidth}lpx`
            })
          })
        }.width(`${this.cellWidth * 9 + this.cellWidth / 2}lpx`)

        Flex({ wrap: FlexWrap.Wrap }) {
          //小猫
          ForEach(this.gridCells, (item: Cell, index: number) => {
            Stack() {
              Text('猫')
                .width(`${this.pieceSize}lpx`)
                .height(`${this.pieceSize}lpx`)
                .fontColor(Color.White)
                .fontSize(`${this.cellWidth / 2}lpx`)
                .textAlign(TextAlign.Center)
                .backgroundColor(Color.Black)
                .borderRadius('50%')
                .visibility(this.catPositionIndex == index ? Visibility.Visible : Visibility.None)
            }
            .width(`${this.cellWidth}lpx`)
            .height(`${this.cellWidth}lpx`)
            .margin({
              left: `${(index + 9) % 18 == 0 ? this.cellWidth / 2 : 0}lpx`,
              top: `${this.borderPieceWidth}lpx`
            })
            .onClick(() => {
              if (item.value == 0 && index != this.catPositionIndex) {
                item.value = 1; // 放置墙壁
                this.moveCat(); // 移动神经猫
              }
            })
            .animation({ duration: 150 })

          })
        }.width(`${this.cellWidth * 9 + this.cellWidth / 2}lpx`)
      }

      Button('重新开始').clickEffect({ scale: 0.5, level: ClickEffectLevel.LIGHT }).onClick(() => {
        this.startGame()
      })
    }.height('100%').width('100%')
    .backgroundColor("#666666")
    .padding({ top: 20 })
  }
}