HarmonyOS 小游戏《对战五子棋》开发第12篇 - 简单AI实现:规则引擎与优先级策略

最朴素的AI------用if-else模拟人类下棋直觉

设计截图如下:

代码如下:

typescript 复制代码
/**
 * AIPlayer.ets - 五子棋AI对手
 * 支持三档难度:简单、普通、困难
 */
import { BOARD_SIZE, EMPTY, BLACK, WHITE, Difficulty, Move, getOpponent } from './GameConstants';

/** 评分常量 */
const SCORE_FIVE: number = 1000000;     // 五连
const SCORE_OPEN_FOUR: number = 100000; // 活四
const SCORE_FOUR: number = 10000;       // 冲四
const SCORE_OPEN_THREE: number = 8000;  // 活三
const SCORE_THREE: number = 500;        // 眠三
const SCORE_OPEN_TWO: number = 400;     // 活二
const SCORE_TWO: number = 50;           // 眠二
const SCORE_ONE: number = 10;           // 单子

/** 带分数的落子候选 */
class ScoredMove {
  move: Move;
  score: number;

  constructor(move: Move, score: number) {
    this.move = move;
    this.score = score;
  }
}

export class AIPlayer {
  private aiColor: number;
  private humanColor: number;
  private difficulty: Difficulty;

  constructor(aiColor: number, difficulty: Difficulty) {
    this.aiColor = aiColor;
    this.humanColor = getOpponent(aiColor);
    this.difficulty = difficulty;
  }

  /**
   * 获取AI下一步落子位置
   */
  getMove(board: number[][]): Move {
    // 空棋盘直接下天元
    let isEmpty = true;
    for (let i = 0; i < BOARD_SIZE; i++) {
      for (let j = 0; j < BOARD_SIZE; j++) {
        if (board[i][j] !== EMPTY) {
          isEmpty = false;
          break;
        }
      }
      if (!isEmpty) break;
    }
    if (isEmpty) {
      return new Move(7, 7);
    }

    switch (this.difficulty) {
      case Difficulty.EASY:
        return this.getEasyMove(board);
      case Difficulty.NORMAL:
        return this.getNormalMove(board);
      case Difficulty.HARD:
        return this.getHardMove(board);
      default:
        return this.getNormalMove(board);
    }
  }

  // ==================== 简单模式 ====================
  private getEasyMove(board: number[][]): Move {
    const candidates = this.getCandidates(board, 2);

    // 1. AI能赢就赢
    for (const move of candidates) {
      board[move.row][move.col] = this.aiColor;
      if (this.checkWin(board, move.row, move.col, this.aiColor)) {
        board[move.row][move.col] = EMPTY;
        return move;
      }
      board[move.row][move.col] = EMPTY;
    }

    // 2. 堵对手四连
    for (const move of candidates) {
      board[move.row][move.col] = this.humanColor;
      if (this.checkWin(board, move.row, move.col, this.humanColor)) {
        board[move.row][move.col] = EMPTY;
        return move;
      }
      board[move.row][move.col] = EMPTY;
    }

    // 3. 堵对手活三
    for (const move of candidates) {
      board[move.row][move.col] = this.humanColor;
      const score = this.evaluatePosition(board, move.row, move.col, this.humanColor);
      board[move.row][move.col] = EMPTY;
      if (score >= SCORE_OPEN_THREE) {
        return move;
      }
    }

    // 4. 随机选择(偏中心)
    const centerDist = (m: Move): number => Math.abs(m.row - 7) + Math.abs(m.col - 7);
    candidates.sort((a: Move, b: Move) => {
      const diff = centerDist(a) - centerDist(b);
      if (diff !== 0) return diff;
      return Math.random() - 0.5;
    });
    const topN = Math.min(5, candidates.length);
    const idx = Math.floor(Math.random() * topN);
    return candidates[idx];
  }

  // ==================== 普通模式 ====================
  private getNormalMove(board: number[][]): Move {
    const candidates = this.getCandidates(board, 2);
    let bestMove: Move = candidates[0];
    let bestScore: number = -1;

    for (const move of candidates) {
      board[move.row][move.col] = this.aiColor;
      const attackScore = this.evaluatePosition(board, move.row, move.col, this.aiColor);
      board[move.row][move.col] = EMPTY;

      board[move.row][move.col] = this.humanColor;
      const defendScore = this.evaluatePosition(board, move.row, move.col, this.humanColor);
      board[move.row][move.col] = EMPTY;

      const totalScore = attackScore * 1.1 + defendScore;
      if (totalScore > bestScore) {
        bestScore = totalScore;
        bestMove = move;
      }
    }
    return bestMove;
  }

  // ==================== 困难模式 ====================
  private getHardMove(board: number[][]): Move {
    const candidates = this.getSortedCandidates(board);
    const maxCandidates = Math.min(12, candidates.length);
    let bestMove: Move = candidates[0];
    let bestScore: number = -Infinity;
    let alpha: number = -Infinity;
    const beta: number = Infinity;

    for (let i = 0; i < maxCandidates; i++) {
      const move = candidates[i];
      board[move.row][move.col] = this.aiColor;

      if (this.checkWin(board, move.row, move.col, this.aiColor)) {
        board[move.row][move.col] = EMPTY;
        return move;
      }

      const score = this.minimax(board, 2, alpha, beta, false);
      board[move.row][move.col] = EMPTY;

      if (score > bestScore) {
        bestScore = score;
        bestMove = move;
      }
      alpha = Math.max(alpha, score);
    }
    return bestMove;
  }

  private minimax(board: number[][], depth: number, alpha: number, beta: number,
    isMaximizing: boolean): number {
    if (depth === 0) {
      return this.evaluateBoard(board);
    }

    const candidates = this.getSortedCandidates(board);
    const maxCandidates = Math.min(8, candidates.length);

    if (isMaximizing) {
      let maxEval: number = -Infinity;
      for (let i = 0; i < maxCandidates; i++) {
        const move = candidates[i];
        board[move.row][move.col] = this.aiColor;

        if (this.checkWin(board, move.row, move.col, this.aiColor)) {
          board[move.row][move.col] = EMPTY;
          return SCORE_FIVE;
        }

        const evalScore = this.minimax(board, depth - 1, alpha, beta, false);
        board[move.row][move.col] = EMPTY;

        maxEval = Math.max(maxEval, evalScore);
        alpha = Math.max(alpha, evalScore);
        if (beta <= alpha) break;
      }
      return maxEval;
    } else {
      let minEval: number = Infinity;
      for (let i = 0; i < maxCandidates; i++) {
        const move = candidates[i];
        board[move.row][move.col] = this.humanColor;

        if (this.checkWin(board, move.row, move.col, this.humanColor)) {
          board[move.row][move.col] = EMPTY;
          return -SCORE_FIVE;
        }

        const evalScore = this.minimax(board, depth - 1, alpha, beta, true);
        board[move.row][move.col] = EMPTY;

        minEval = Math.min(minEval, evalScore);
        beta = Math.min(beta, evalScore);
        if (beta <= alpha) break;
      }
      return minEval;
    }
  }

  // ==================== 评估函数 ====================

  private evaluateBoard(board: number[][]): number {
    let aiScore: number = 0;
    let humanScore: number = 0;
    aiScore += this.evaluateLines(board, this.aiColor);
    humanScore += this.evaluateLines(board, this.humanColor);
    return aiScore - humanScore * 1.1;
  }

  private evaluateLines(board: number[][], player: number): number {
    let total: number = 0;
    const directions: number[][] = [[0, 1], [1, 0], [1, 1], [1, -1]];

    for (const dir of directions) {
      if (dir[0] === 0 && dir[1] === 1) {
        for (let row = 0; row < BOARD_SIZE; row++) {
          total += this.evaluateLine(board, row, 0, dir[0], dir[1], player);
        }
      } else if (dir[0] === 1 && dir[1] === 0) {
        for (let col = 0; col < BOARD_SIZE; col++) {
          total += this.evaluateLine(board, 0, col, dir[0], dir[1], player);
        }
      } else if (dir[0] === 1 && dir[1] === 1) {
        for (let row = 0; row < BOARD_SIZE; row++) {
          total += this.evaluateLine(board, row, 0, dir[0], dir[1], player);
        }
        for (let col = 1; col < BOARD_SIZE; col++) {
          total += this.evaluateLine(board, 0, col, dir[0], dir[1], player);
        }
      } else {
        for (let row = 0; row < BOARD_SIZE; row++) {
          total += this.evaluateLine(board, row, 0, dir[0], dir[1], player);
        }
        for (let col = 1; col < BOARD_SIZE; col++) {
          total += this.evaluateLine(board, 0, col, dir[0], dir[1], player);
        }
      }
    }
    return total;
  }

  private evaluateLine(board: number[][], startRow: number, startCol: number,
    dr: number, dc: number, player: number): number {
    let score: number = 0;
    let row = startRow;
    let col = startCol;
    let consecutive: number = 0;

    while (row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE) {
      if (board[row][col] === player) {
        consecutive++;
      } else {
        if (consecutive > 0) {
          let openEnds: number = 0;
          const prevR = row - dr * (consecutive + 1);
          const prevC = col - dc * (consecutive + 1);
          if (prevR >= 0 && prevR < BOARD_SIZE && prevC >= 0 && prevC < BOARD_SIZE &&
            board[prevR][prevC] === EMPTY) {
            openEnds++;
          }
          if (board[row][col] === EMPTY) {
            openEnds++;
          }
          score += this.scoreForCount(consecutive, openEnds);
          consecutive = 0;
        }
      }
      row += dr;
      col += dc;
    }

    if (consecutive > 0) {
      let openEnds: number = 0;
      const prevR = row - dr * (consecutive + 1);
      const prevC = col - dc * (consecutive + 1);
      if (prevR >= 0 && prevR < BOARD_SIZE && prevC >= 0 && prevC < BOARD_SIZE &&
        board[prevR][prevC] === EMPTY) {
        openEnds++;
      }
      score += this.scoreForCount(consecutive, openEnds);
    }

    return score;
  }

  private scoreForCount(count: number, openEnds: number): number {
    if (count >= 5) return SCORE_FIVE;
    if (count === 4) {
      if (openEnds === 2) return SCORE_OPEN_FOUR;
      if (openEnds === 1) return SCORE_FOUR;
      return 0;
    }
    if (count === 3) {
      if (openEnds === 2) return SCORE_OPEN_THREE;
      if (openEnds === 1) return SCORE_THREE;
      return 0;
    }
    if (count === 2) {
      if (openEnds === 2) return SCORE_OPEN_TWO;
      if (openEnds === 1) return SCORE_TWO;
      return 0;
    }
    if (count === 1) {
      if (openEnds === 2) return SCORE_ONE;
      return 0;
    }
    return 0;
  }

  private evaluatePosition(board: number[][], row: number, col: number,
    player: number): number {
    const directions: number[][] = [[0, 1], [1, 0], [1, 1], [1, -1]];
    let totalScore: number = 0;
    for (const dir of directions) {
      totalScore += this.evaluateDirection(board, row, col, dir[0], dir[1], player);
    }
    return totalScore;
  }

  private evaluateDirection(board: number[][], row: number, col: number,
    dr: number, dc: number, player: number): number {
    let count: number = 1;
    let leftOpen: boolean = false;
    let rightOpen: boolean = false;

    let r = row + dr;
    let c = col + dc;
    while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && board[r][c] === player) {
      count++;
      r += dr;
      c += dc;
    }
    if (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && board[r][c] === EMPTY) {
      rightOpen = true;
    }

    r = row - dr;
    c = col - dc;
    while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && board[r][c] === player) {
      count++;
      r -= dr;
      c -= dc;
    }
    if (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && board[r][c] === EMPTY) {
      leftOpen = true;
    }

    const openCount = (leftOpen ? 1 : 0) + (rightOpen ? 1 : 0);
    return this.scoreForCount(count, openCount);
  }

  // ==================== 辅助方法 ====================

  private checkWin(board: number[][], 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 (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 (board[r][c] === player) count++;
        else break;
      }
      if (count >= 5) return true;
    }
    return false;
  }

  private getCandidates(board: number[][], 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 (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 &&
                board[r][c] === EMPTY) {
                const key = `${r},${c}`;
                if (!seen.has(key)) {
                  seen.add(key);
                  candidates.push(new Move(r, c));
                }
              }
            }
          }
        }
      }
    }
    if (candidates.length === 0) {
      candidates.push(new Move(7, 7));
    }
    return candidates;
  }

  private getSortedCandidates(board: number[][]): Move[] {
    const candidates = this.getCandidates(board, 2);
    const scored: ScoredMove[] = [];

    for (const move of candidates) {
      board[move.row][move.col] = this.aiColor;
      const attack = this.evaluatePosition(board, move.row, move.col, this.aiColor);
      board[move.row][move.col] = EMPTY;

      board[move.row][move.col] = this.humanColor;
      const defend = this.evaluatePosition(board, move.row, move.col, this.humanColor);
      board[move.row][move.col] = EMPTY;

      scored.push(new ScoredMove(move, attack + defend));
    }

    scored.sort((a: ScoredMove, b: ScoredMove) => b.score - a.score);
    return scored.map((s: ScoredMove) => s.move);
  }
}

简单AI的决策链

typescript 复制代码
private getEasyMove(board: number[][]): Move {
  const candidates = this.getCandidates(board, 2);

  // 优先级1:AI能赢就赢
  for (const move of candidates) {
    board[move.row][move.col] = this.aiColor;
    if (this.checkWin(board, move.row, move.col, this.aiColor)) {
      board[move.row][move.col] = EMPTY;
      return move;
    }
    board[move.row][move.col] = EMPTY;
  }

  // 优先级2:堵对手四连
  for (const move of candidates) {
    board[move.row][move.col] = this.humanColor;
    if (this.checkWin(board, move.row, move.col, this.humanColor)) {
      board[move.row][move.col] = EMPTY;
      return move;
    }
    board[move.row][move.col] = EMPTY;
  }

  // 优先级3:堵对手活三
  for (const move of candidates) {
    board[move.row][move.col] = this.humanColor;
    const score = this.evaluatePosition(board, move.row, move.col, this.humanColor);
    board[move.row][move.col] = EMPTY;
    if (score >= SCORE_OPEN_THREE) {
      return move;
    }
  }

  // 优先级4:随机选择(偏中心)
  const centerDist = (m: Move): number => Math.abs(m.row - 7) + Math.abs(m.col - 7);
  candidates.sort((a: Move, b: Move) => {
    const diff = centerDist(a) - centerDist(b);
    if (diff !== 0) return diff;
    return Math.random() - 0.5;
  });
  const topN = Math.min(5, candidates.length);
  const idx = Math.floor(Math.random() * topN);
  return candidates[idx];
}

四级优先级详解

优先级1:AI能赢就赢

typescript 复制代码
board[move.row][move.col] = this.aiColor;
if (this.checkWin(board, move.row, move.col, this.aiColor)) {
  board[move.row][move.col] = EMPTY;
  return move;
}
board[move.row][move.col] = EMPTY;

模拟落子→检查胜负→撤销:这是AI中最常用的"试探"模式。对每个候选位置,假设AI落子在此,检查是否形成五连。如果赢,立即返回。

优先级2:堵对手四连

同样的试探模式,但这次模拟的是人类落子。如果人类在此处落子能赢,AI必须堵住。

优先级3:堵对手活三

typescript 复制代码
const score = this.evaluatePosition(board, move.row, move.col, this.humanColor);
if (score >= SCORE_OPEN_THREE) {
  return move;
}

活三是威胁性很强的棋型(下一步可以变成活四),必须提前防御。

优先级4:随机偏中心

typescript 复制代码
candidates.sort((a: Move, b: Move) => {
  const diff = centerDist(a) - centerDist(b);
  if (diff !== 0) return diff;
  return Math.random() - 0.5;  // 距离相同时随机
});
const topN = Math.min(5, candidates.length);
const idx = Math.floor(Math.random() * topN);
return candidates[idx];

中心偏置centerDist计算曼哈顿距离,距离中心近的排前面。

随机性:从前5个候选中随机选一个,增加AI的不可预测性。

"试探-撤销"模式

这是简单AI最核心的技术:

typescript 复制代码
// 1. 在空位上模拟落子
board[move.row][move.col] = player;
// 2. 检查效果
const wins = this.checkWin(board, move.row, move.col, player);
// 3. 撤销模拟
board[move.row][move.col] = EMPTY;

为什么直接修改board而不创建副本?

  1. 创建15x15数组副本有内存开销
  2. 试探后立即撤销,board状态不变
  3. JavaScript是单线程的,不存在并发问题

简单AI的局限性

  1. 只看一步:不考虑对手的后续反应
  2. 不会主动进攻:除了"能赢就赢",没有进攻策略
  3. 可能被双活三击败:只堵第一个发现的活三
  4. 随机性可能下出臭棋:优先级4完全随机

但这些局限正是"简单"的含义------它是一个合格的入门级AI。

对比普通AI

typescript 复制代码
// 简单AI:规则优先级,找到就返回
if (能赢) return 赢;
if (要堵) return 堵;

// 普通AI:评估所有候选,选最优
for (每个候选) {
  score = 攻击分 × 1.1 + 防守分
}
return 最高分;

简单AI是"找到就做",普通AI是"比较后选最优"。

总结

简单AI展示了最基础的AI设计模式:

  1. 优先级链:从高到低依次检查
  2. 试探-撤销:模拟落子评估效果
  3. 中心偏置:符合棋类开局直觉
  4. 适度随机:增加不可预测性

这种规则驱动的AI虽然简单,但在实际游戏中能提供合理的对手体验。

相关推荐
凡科建站1 小时前
2026年AI网页快速制作方法,AI自动生成响应式页面
人工智能
用户938515635071 小时前
从“问答机器”到“智能体”:AI Agent 是如何自主工作的?
javascript·人工智能·全栈
秦先生在广东1 小时前
Loop Engineering:用系统设计取代手动提示,构建 AI 编码代理的自动化循环
人工智能
知足常乐3201 小时前
【Agent失败复盘】从 ReAct 到白箱工作流:一个数据分析 Agent 被现实按着重构的过程
人工智能
Kel1 小时前
多 Tab 聊天不翻车:纯前端实现跨标签页消息同步的三层协同架构
前端·javascript·人工智能
林泽毅1 小时前
Qwen3.5 DPO 模型对齐实战(pytrio训练版)
人工智能·python·算法
Token炼金师1 小时前
四段式淬炼:预训练、持续预训练、中训练、冷启动 SFT —— 课程学习与训练阶段编排
人工智能·深度学习·机器学习
RisunJan1 小时前
AI 前沿速报 · 周报第28周
人工智能