棋盘是五子棋的"世界"——理解数据结构就理解了游戏的一半

代码如下:

/**
 * GomokuEngine.ets - 五子棋核心引擎
 * 负责棋盘管理、落子、胜负判定
 */
import { BOARD_SIZE, EMPTY, BLACK, WHITE, GameResult, Move } from './GameConstants';

export class GomokuEngine {
  /** 棋盘二维数组 */
  board: number[][];
  /** 当前玩家 */
  currentPlayer: number;
  /** 游戏结果 */
  result: GameResult;
  /** 落子历史 */
  history: Move[];
  /** 最后一手位置 */
  lastMove: Move | null;

  constructor() {
    this.board = this.createEmptyBoard();
    this.currentPlayer = BLACK;
    this.result = GameResult.PLAYING;
    this.history = [];
    this.lastMove = null;
  }

  /** 创建空棋盘 */
  createEmptyBoard(): number[][] {
    const board: number[][] = [];
    for (let i = 0; i < BOARD_SIZE; i++) {
      const row: number[] = [];
      for (let j = 0; j < BOARD_SIZE; j++) {
        row.push(EMPTY);
      }
      board.push(row);
    }
    return board;
  }

  /** 重置游戏 */
  reset(): void {
    this.board = this.createEmptyBoard();
    this.currentPlayer = BLACK;
    this.result = GameResult.PLAYING;
    this.history = [];
    this.lastMove = null;
  }

  /** 在指定位置落子,返回是否成功 */
  placePiece(row: number, col: number, player: number): boolean {
    if (this.result !== GameResult.PLAYING) {
      return false;
    }
    if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) {
      return false;
    }
    if (this.board[row][col] !== EMPTY) {
      return false;
    }

    this.board[row][col] = player;
    this.lastMove = new Move(row, col);
    this.history.push(new Move(row, col));

    // 检查胜负
    if (this.checkWin(row, col, player)) {
      this.result = player === BLACK ? GameResult.BLACK_WIN : GameResult.WHITE_WIN;
    } else if (this.isBoardFull()) {
      this.result = GameResult.DRAW;
    } else {
      this.currentPlayer = player === BLACK ? WHITE : BLACK;
    }
    return true;
  }

  /** 检查指定位置是否获胜(五子连珠) */
  checkWin(row: number, col: number, player: number): boolean {
    const directions: number[][] = [
      [0, 1],  // 水平
      [1, 0],  // 垂直
      [1, 1],  // 主对角线
      [1, -1]  // 副对角线
    ];

    for (const dir of directions) {
      let count = 1;
      // 正方向
      for (let i = 1; i < 5; i++) {
        const r = row + dir[0] * i;
        const c = col + dir[1] * i;
        if (r < 0 || r >= BOARD_SIZE || c < 0 || c >= BOARD_SIZE) {
          break;
        }
        if (this.board[r][c] === player) {
          count++;
        } else {
          break;
        }
      }
      // 反方向
      for (let i = 1; i < 5; i++) {
        const r = row - dir[0] * i;
        const c = col - dir[1] * i;
        if (r < 0 || r >= BOARD_SIZE || c < 0 || c >= BOARD_SIZE) {
          break;
        }
        if (this.board[r][c] === player) {
          count++;
        } else {
          break;
        }
      }
      if (count >= 5) {
        return true;
      }
    }
    return false;
  }

  /** 检查棋盘是否已满 */
  isBoardFull(): boolean {
    for (let i = 0; i < BOARD_SIZE; i++) {
      for (let j = 0; j < BOARD_SIZE; j++) {
        if (this.board[i][j] === EMPTY) {
          return false;
        }
      }
    }
    return true;
  }

  /** 撤销上一步 */
  undo(): boolean {
    if (this.history.length === 0) {
      return false;
    }
    const lastMove = this.history.pop()!;
    this.board[lastMove.row][lastMove.col] = EMPTY;
    this.currentPlayer = this.currentPlayer === BLACK ? WHITE : BLACK;
    this.result = GameResult.PLAYING;
    if (this.history.length > 0) {
      const prev = this.history[this.history.length - 1];
      this.lastMove = new Move(prev.row, prev.col);
    } else {
      this.lastMove = null;
    }
    return true;
  }

  /** 获取所有空位 */
  getEmptyPositions(): Move[] {
    const positions: Move[] = [];
    for (let i = 0; i < BOARD_SIZE; i++) {
      for (let j = 0; j < BOARD_SIZE; j++) {
        if (this.board[i][j] === EMPTY) {
          positions.push(new Move(i, j));
        }
      }
    }
    return positions;
  }

  /** 获取已有棋子周围的候选位置 */
  getCandidateMoves(range: number = 2): Move[] {
    const candidates: Move[] = [];
    const seen: Set<string> = new Set();

    for (let i = 0; i < BOARD_SIZE; i++) {
      for (let j = 0; j < BOARD_SIZE; j++) {
        if (this.board[i][j] !== EMPTY) {
          for (let dr = -range; dr <= range; dr++) {
            for (let dc = -range; dc <= range; dc++) {
              const r = i + dr;
              const c = j + dc;
              if (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE) {
                if (this.board[r][c] === EMPTY) {
                  const key = `${r},${c}`;
                  if (!seen.has(key)) {
                    seen.add(key);
                    candidates.push(new Move(r, c));
                  }
                }
              }
            }
          }
        }
      }
    }
    return candidates;
  }

  /** 检查棋盘是否为空 */
  isBoardEmpty(): boolean {
    return this.history.length === 0;
  }

  /** 将棋盘转为扁平数组(用于UI传递) */
  toFlatArray(): number[] {
    const flat: number[] = [];
    for (let i = 0; i < BOARD_SIZE; i++) {
      for (let j = 0; j < BOARD_SIZE; j++) {
        flat.push(this.board[i][j]);
      }
    }
    return flat;
  }
}

棋盘的二维数组表示

export class GomokuEngine {
  board: number[][];
  
  createEmptyBoard(): number[][] {
    const board: number[][] = [];
    for (let i = 0; i < BOARD_SIZE; i++) {
      const row: number[] = [];
      for (let j = 0; j < BOARD_SIZE; j++) {
        row.push(EMPTY);
      }
      board.push(row);
    }
    return board;
  }
}

棋盘用number[][]二维数组表示:

  • board[0][0]是左上角
  • board[7][7]是天元(中心点)
  • board[14][14]是右下角
  • 值为0(EMPTY)、1(BLACK)或2(WHITE)
    0  1  2  3 ... 14
  ┌──┬──┬──┬──┬───┬──┐
0 │  │  │  │  │...│  │
  ├──┼──┼──┼──┼───┼──┤
1 │  │  │  │  │...│  │
  ├──┼──┼──┼──┼───┼──┤
...│  │  │  │  │...│  │
  ├──┼──┼──┼──┼───┼──┤
14│  │  │  │  │...│  │
  └──┴──┴──┴──┴───┴──┘

为什么用二维数组而不是一维数组

二维数组的优势:

  • 坐标直观:board[row][col]
  • 行列操作自然

一维数组的优势(本项目也用到了):

  • 序列化方便:toFlatArray()返回number[]
  • 传递给Canvas组件更简单

本项目两者兼用:内部逻辑用二维数组,UI传递用一维数组。

toFlatArray:二维到一维的转换

toFlatArray(): number[] {
  const flat: number[] = [];
  for (let i = 0; i < BOARD_SIZE; i++) {
    for (let j = 0; j < BOARD_SIZE; j++) {
      flat.push(this.board[i][j]);
    }
  }
  return flat;
}

转换公式:flatIndex = row * BOARD_SIZE + col

逆运算:row = Math.floor(flatIndex / BOARD_SIZE)col = flatIndex % BOARD_SIZE

引擎状态管理

export class GomokuEngine {
  board: number[][];          // 棋盘
  currentPlayer: number;      // 当前玩家
  result: GameResult;         // 游戏结果
  history: Move[];            // 落子历史
  lastMove: Move | null;      // 最后一手

  constructor() {
    this.board = this.createEmptyBoard();
    this.currentPlayer = BLACK;          // 黑方先行
    this.result = GameResult.PLAYING;
    this.history = [];
    this.lastMove = null;
  }
}

状态设计要点:

  1. currentPlayer:黑方先行的规则在构造函数中设定
  2. result:用枚举管理游戏状态,避免布尔标志的组合爆炸
  3. history:数组记录所有落子,支持悔棋功能
  4. lastMove:单独记录最后一手,UI高亮显示

reset方法

reset(): void {
  this.board = this.createEmptyBoard();
  this.currentPlayer = BLACK;
  this.result = GameResult.PLAYING;
  this.history = [];
  this.lastMove = null;
}

reset重新初始化所有状态,与构造函数逻辑一致。这种设计确保游戏可以反复重玩而不需要创建新的引擎实例。

isBoardEmpty和isBoardFull

isBoardEmpty(): boolean {
  return this.history.length === 0;
}

isBoardFull(): boolean {
  for (let i = 0; i < BOARD_SIZE; i++) {
    for (let j = 0; j < BOARD_SIZE; j++) {
      if (this.board[i][j] === EMPTY) {
        return false;
      }
    }
  }
  return true;
}

巧妙的设计:

  • isBoardEmpty通过检查history.length而非遍历棋盘——O(1)复杂度
  • isBoardFull必须遍历——O(n^2)复杂度,但只在落子后调用一次

总结

棋盘数据结构是游戏引擎的基石。本项目用简单的二维数组配合history数组,实现了棋盘表示、状态管理和历史记录。关键设计决策:

  1. 二维数组用于逻辑,一维数组用于UI传递
  2. 用枚举管理游戏状态
  3. history数组既是悔棋的基础,也是判断空棋盘的快捷方式

附:

在这里插入图片描述

Logo

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

更多推荐