鸿蒙开发案例:区字棋_i++

因棋盘酷似简体中文的“区”字,故称“区字棋”;也因为每人只有两枚棋子,所以也叫“二子棋”或“两子棋”,还有说法叫憋死牛、跳井棋等。交战一方只要将另一方困住就算取胜。该游戏玩法简单,一般用于儿童启蒙教育用。

【算法分析】

1. 动画执行

动画执行算法通过设置动画参数来模拟棋子从当前位置平滑移动到目标位置的过程。该算法包括两个阶段的动画:首先是棋子的移动动画,然后是一个瞬时动画用于交换棋子的位置信息。

实现原理:

• 设置动画的持续时间、迭代次数、曲线类型。

• 在动画结束后,重置动画偏移量,并交换当前棋子与目标棋子的位置信息。

moveAnimation(animationTime: number, toCell: Cell, callback?: () => void) {
  animateToImmediately({
    duration: animationTime,
    iterations: 1,
    curve: Curve.Linear,
    onFinish: () => {
      animateToImmediately({
        duration: 0,
        iterations: 1,
        curve: Curve.Linear,
        onFinish: () => {
          if (callback) {
            callback();
          }
        }
      }, () => {
        this.animX = 0;
        this.animY = 0;
        let temp = this.user;
        this.user = toCell.user;
        toCell.user = temp;
      });
    }
  }, () => {
    this.animX = toCell.x - this.x;
    this.animY = toCell.y - this.y;
  });
}

2. 棋子有效性检查

这个算法检查当前选定的棋子是否可以由当前玩家移动。它通过比较棋子的用户标识与当前玩家标识来判断棋子是否有效。

实现原理:

• 检查棋子的用户标识是否与当前玩家标识相匹配。

• 只有当棋子属于当前玩家时,才返回 true 表示棋子有效。

isCellValid(cell: Cell): boolean {
  return (cell.user === 1 && this.currentPlayer === 1) || (cell.user === 2 && this.currentPlayer === 2);
}

3. 游戏是否结束

该算法检查游戏是否已结束,通过遍历所有棋子来确定是否有任何棋子可以移动。

实现原理:

• 遍历所有棋子,检查是否存在属于当前玩家并且有合法移动路径的棋子。

• 如果没有这样的棋子存在,则表示游戏结束。

isGameOver(): boolean {
  for (let i = 0; i < this.cells.length; i++) {
    if (this.currentPlayer == this.cells[i].user && this.checkValidMove(this.cells[i]) != -1) {
      return false;
    }
  }
  return true;
}

4. 检查合法走法

这个算法检查棋子是否可以移动到棋盘上的其他位置。它通过查找棋子之间的连接关系来确定哪些位置是合法的。

实现原理:

• 遍历所有的连接关系,查找与当前棋子相关的连接。

• 对于每一个连接,检查连接的另一端是否为空闲位置。

• 如果找到空闲位置,则返回该位置的索引;否则返回 -1 表示没有合法的走法。

checkValidMove(cell: Cell): number {
  for (let i = 0; i < this.connections.length; i++) {
    if (cell.name === this.connections[i].startName) {
      for (let j = 0; j < this.cells.length; j++) {
        if (this.cells[j].name === this.connections[i].endName && this.cells[j].user === 0) {
          return j;
        }
      }
    } else if (cell.name === this.connections[i].endName) {
      for (let j = 0; j < this.cells.length; j++) {
        if (this.cells[j].name === this.connections[i].startName && this.cells[j].user === 0) {
          return j;
        }
      }
    }
  }
  return -1;
}

5. AI自动走法

此算法实现了白方(AI)的自动走法逻辑,它通过筛选出所有可以移动的白棋,并从中选择一个最佳的走法。

实现原理:

• 过滤出所有可以移动的白棋。

• 如果只有一个可移动的白棋,则直接移动。

• 如果有两个或更多个可移动的白棋,则调用 chooseBestMove 函数来选择最佳的走法。

aiMove() {
  let whiteCells = this.cells.filter(cell => cell.user === 2 && this.checkValidMove(cell) !== -1);
  if (whiteCells.length === 1) {
    this.move(whiteCells[0]);
  } else if (whiteCells.length === 2) {
    let moveIndex = this.chooseBestMove(whiteCells);
    this.move(whiteCells[moveIndex]);
  }
}

6. 选择最佳走法

此算法用于选择AI的最佳走法。它通过预测每一步棋的结果来选择能够导致游戏结束的最佳走法。

实现原理:

• 遍历所有可以移动的白棋,并对每一步棋都模拟其结果。

• 模拟后检查游戏是否结束,如果是,则选择这步棋作为最佳走法。

• 如果没有找到导致游戏结束的走法,则随机选择一步。

chooseBestMove(whiteCells: Cell[]): number {
  let emptyIndex = this.cells.findIndex(cell => cell.user === 0);
  let bestMoveIndex = -1;
  for (let i = 0; i < whiteCells.length; i++) {
    let tempUser = whiteCells[i].user;
    whiteCells[i].user = this.cells[emptyIndex].user;
    this.cells[emptyIndex].user = tempUser;
    this.currentPlayer = 1;
    let isGameOver = this.isGameOver();
    tempUser = whiteCells[i].user;
    whiteCells[i].user = this.cells[emptyIndex].user;
    this.cells[emptyIndex].user = tempUser;
    this.currentPlayer = 2;
    if (isGameOver) {
      bestMoveIndex = i;
      break;
    }
  }
  if (bestMoveIndex === -1) {
    bestMoveIndex = Math.floor(Math.random() * 2);
  }
  return bestMoveIndex;
}

【完整代码】

// 导入提示框操作模块
import { promptAction } from '@kit.ArkUI';

// 使用框架提供的特性,如属性追踪等
@ObservedV2
class Cell {
  // 定义棋子类型,0为空,1为黑方,2为白方
  @Trace user: number = 0;
  // 棋子的名字,例如"A"、"B"
  name: string = "";
  // 棋子的位置坐标
  x: number = 0;
  y: number = 0;
  // 棋子的尺寸
  width: number = 100;
  height: number = 100;

  // 构造函数初始化棋子的状态
  constructor(name: string, x: number, y: number, user: number) {
    this.user = user;
    this.name = name;
    this.x = x;
    this.y = y;
  }

  // 动画中的X轴偏移量
  @Trace animX: number = 0;
  // 动画中的Y轴偏移量
  @Trace animY: number = 0;

  // 获取棋子中心点的X坐标
  getCenterX() {
    return this.x - this.width / 2;
  }

  // 获取棋子中心点的Y坐标
  getCenterY() {
    return this.y - this.height / 2;
  }

  // 执行棋子移动动画
  moveAnimation(animationTime: number, toCell: Cell, callback?: () => void) {
    // 设置动画参数
    animateToImmediately({
      duration: animationTime,
      iterations: 1,
      curve: Curve.Linear,
      // 在动画完成后的回调函数
      onFinish: () => {
        animateToImmediately({
          duration: 0,
          iterations: 1,
          curve: Curve.Linear,
          // 在动画完成后的内部回调函数
          onFinish: () => {
            if (callback) {
              callback();
            }
          }
        }, () => {
          // 重置动画偏移量
          this.animX = 0;
          this.animY = 0;
          // 交换棋子的位置信息
          let temp = this.user;
          this.user = toCell.user;
          toCell.user = temp;
        });
      }
    }, () => {
      // 设置动画的目标偏移量
      this.animX = toCell.x - this.x;
      this.animY = toCell.y - this.y;
    });
  }
}

// 定义棋子之间的连接关系
class Connection {
  // 开始和结束节点的名字
  startName: string;
  endName: string;
  // 开始和结束节点的坐标
  startX: number;
  startY: number;
  endX: number;
  endY: number;

  // 构造函数初始化连接关系
  constructor(start: Cell, end: Cell) {
    this.startName = start.name;
    this.endName = end.name;
    this.startX = start.x;
    this.startY = start.y;
    this.endX = end.x;
    this.endY = end.y;
  }
}

// 定义游戏结构
@Entry
@Component
struct TwoSonChessGame {
  // 游戏状态标志,用于控制动画执行
  @State isAnimationRunning: boolean = false;
  // 当前棋盘上的所有棋子
  @State cells: Cell[] = [];
  // 当前棋盘上的所有连接关系
  @State connections: Connection[] = [];
  // 当前玩家,1代表黑方,2代表白方
  @State currentPlayer: number = 1;

  // 游戏加载时初始化棋盘
  aboutToAppear(): void {
    // 创建五个棋子
    const cellA = new Cell("A", 180, 180, 2);
    const cellB = new Cell("B", 540, 180, 1);
    const cellC = new Cell("C", 360, 360, 0);
    const cellD = new Cell("D", 180, 540, 1);
    const cellE = new Cell("E", 540, 540, 2);
    // 将创建的棋子添加到棋盘上
    this.cells.push(cellA, cellB, cellC, cellD, cellE);
    // 初始化棋子间的连接关系
    this.connections.push(new Connection(cellA, cellB));
    this.connections.push(new Connection(cellA, cellC));
    this.connections.push(new Connection(cellA, cellD));
    this.connections.push(new Connection(cellB, cellC));
    this.connections.push(new Connection(cellC, cellD));
    this.connections.push(new Connection(cellC, cellE));
    this.connections.push(new Connection(cellD, cellE));
  }

  // 重置游戏状态
  resetGame() {
    this.currentPlayer = 1;
    this.cells[0].user = 2;
    this.cells[1].user = 1;
    this.cells[2].user = 0;
    this.cells[3].user = 1;
    this.cells[4].user = 2;
  }

  // 处理棋子移动
  move(cell: Cell) {
    // 判断棋子是否可移动
    if (this.isCellValid(cell)) {
      let targetIndex = this.checkValidMove(cell);
      // 如果目标位置合法,则启动动画
      if (targetIndex !== -1) {
        this.isAnimationRunning = true;
        cell.moveAnimation(300, this.cells[targetIndex], () => {
          this.isAnimationRunning = false;
          this.moveCompleted();
        });
      } else {
        console.info(`当前位置无法移动`);
      }
    }
  }

  // 移动完成后处理
  moveCompleted() {
    // 切换玩家
    this.currentPlayer = this.currentPlayer === 1 ? 2 : 1;
    // 检查游戏是否结束
    if (this.isGameOver()) {
      let winner = this.currentPlayer === 1 ? '白棋赢了' : '黑棋赢了';
      console.info(`${winner}`);
      // 显示游戏结束提示
      promptAction.showDialog({
        title: '游戏结束',
        message: `${winner}`,
        buttons: [{ text: '重新开始', color: '#ffa500' }]
      }).then(() => {
        this.resetGame();
      });
    } else {
      // 如果是白方回合,则进行AI自动走法
      if (this.currentPlayer === 2) {
        this.aiMove();
      }
    }
  }

  // AI走法
  aiMove() {
    let whiteCells = this.cells.filter(cell => cell.user === 2 && this.checkValidMove(cell) !== -1);
    // 根据当前情况选择最优走法
    if (whiteCells.length === 1) {
      this.move(whiteCells[0]);
    } else if (whiteCells.length === 2) {
      let moveIndex = this.chooseBestMove(whiteCells);
      this.move(whiteCells[moveIndex]);
    }
  }

  // 选择最佳走法
  chooseBestMove(whiteCells: Cell[]): number {
    let emptyIndex = this.cells.findIndex(cell => cell.user === 0);
    let bestMoveIndex = -1;
    for (let i = 0; i < whiteCells.length; i++) {
      let tempUser = whiteCells[i].user;
      whiteCells[i].user = this.cells[emptyIndex].user;
      this.cells[emptyIndex].user = tempUser;
      this.currentPlayer = 1;
      let isGameOver = this.isGameOver();
      tempUser = whiteCells[i].user;
      whiteCells[i].user = this.cells[emptyIndex].user;
      this.cells[emptyIndex].user = tempUser;
      this.currentPlayer = 2;
      if (isGameOver) {
        bestMoveIndex = i;
        break;
      }
    }
    if (bestMoveIndex === -1) {
      bestMoveIndex = Math.floor(Math.random() * 2);
    }
    return bestMoveIndex;
  }

  // 判断棋子是否有效
  isCellValid(cell: Cell): boolean {
    return (cell.user === 1 && this.currentPlayer === 1) || (cell.user === 2 && this.currentPlayer === 2);
  }

  // 判断游戏是否结束
  isGameOver(): boolean {
    for (let i = 0; i < this.cells.length; i++) {
      if (this.currentPlayer == this.cells[i].user && this.checkValidMove(this.cells[i]) != -1) {
        return false;
      }
    }
    return true;
  }

  // 检查是否为有效走法
  checkValidMove(cell: Cell): number {
    for (let i = 0; i < this.connections.length; i++) {
      if (cell.name === this.connections[i].startName) {
        for (let j = 0; j < this.cells.length; j++) {
          if (this.cells[j].name === this.connections[i].endName && this.cells[j].user === 0) {
            return j;
          }
        }
      } else if (cell.name === this.connections[i].endName) {
        for (let j = 0; j < this.cells.length; j++) {
          if (this.cells[j].name === this.connections[i].startName && this.cells[j].user === 0) {
            return j;
          }
        }
      }
    }
    return -1;
  }

  // 构建棋盘界面
  build() {
    Column({ space: 10 }) {
      Stack() {
        ForEach(this.connections, (connection: Connection, _index) => {
          Line()
            .width(5)
            .height(5)
            .startPoint([`${connection.startX}lpx`, `${connection.startY}lpx`])
            .endPoint([`${connection.endX}lpx`, `${connection.endY}lpx`])
            .stroke(Color.Black)
            .fill(Color.Green);
        });

        ForEach(this.cells, (cell: Cell, _index) => {
          Text()
            .width(`${cell.width}lpx`)
            .height(`${cell.height}lpx`)
            .margin({ left: `${cell.getCenterX()}lpx`, top: `${cell.getCenterY()}lpx` })
            .translate({ x: `${cell.animX}lpx`, y: `${cell.animY}lpx` })
            .backgroundColor(cell.user === 0 ? Color.Transparent :
              (cell.user === 1 ? Color.Black : Color.White))
            .borderRadius('50%')
            .onClick(() => {
              if (this.isAnimationRunning) {
                console.info(`动画执行中`);
                return;
              }
              this.move(cell);
            });
        });
      }
      .align(Alignment.TopStart)
      .width('720lpx')
      .height('720lpx')
      .backgroundColor(Color.Orange);

      Button('重新开始').onClick(() => {
        if (this.isAnimationRunning) {
          console.info(`动画执行中`);
          return;
        }
        this.resetGame();
      });
    }
  }
}