HarmonyOS 小游戏《对战五子棋》开发第6篇 - GomokuEngine核心引擎设计(一):棋盘数据结构
·
棋盘是五子棋的"世界"——理解数据结构就理解了游戏的一半
代码如下:
/**
* 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;
}
}
状态设计要点:
currentPlayer:黑方先行的规则在构造函数中设定result:用枚举管理游戏状态,避免布尔标志的组合爆炸history:数组记录所有落子,支持悔棋功能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数组,实现了棋盘表示、状态管理和历史记录。关键设计决策:
- 二维数组用于逻辑,一维数组用于UI传递
- 用枚举管理游戏状态
- history数组既是悔棋的基础,也是判断空棋盘的快捷方式
附:

更多推荐


所有评论(0)