HarmonyOS 小游戏《对战五子棋》开发第17篇 - AI的两种评估策略

单点评估 vs 全盘评估------AI如何看待棋盘

设计截图如下:

两种评估函数的对比

本项目实现了两种评估策略:

维度 evaluatePosition evaluateLines
用途 普通模式、候选排序 困难模式Minimax叶节点
范围 评估单个位置的四方向 评估全盘所有线
复杂度 O(1) O(n)
精度 局部精确 全局视角

evaluatePosition:单点评估

typescript 复制代码
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;
}

核心思路 :假设在(row, col)落子,计算该位置在四个方向上的棋型分数之和。

evaluateDirection:单方向扫描

typescript 复制代码
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);
}

评估流程

  1. 以落子点为中心
  2. 正方向延伸:统计连续同色棋子,检查端点是否开放
  3. 反方向延伸:同上
  4. countopenCount查表得分

evaluateLines:全盘评估

typescript 复制代码
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) {
      // 水平:每行从(0,0)开始
      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) {
      // 垂直:每列从(0,col)开始
      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;
}

扫描线的起点

不同方向的扫描起点不同:

复制代码
水平方向(0,1):从每行的第0列开始 → 15条线
垂直方向(1,0):从第0行的每列开始 → 15条线
主对角(1,1):从左边+上边出发 → 29条线
副对角(1,-1):从左边+上边出发 → 29条线
总计:88条扫描线

evaluateLine:逐线扫描

typescript 复制代码
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;
}

逐线扫描逻辑

  1. 沿着方向遍历每个格子
  2. 遇到同色棋子:consecutive++
  3. 遇到不同色或空位:结算之前的连续段
  4. 结算时检查两端是否开放
  5. scoreForCount查表得分

evaluateBoard:全盘综合评估

typescript 复制代码
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;
}

防守权重1.1:与普通模式的攻防权重一致------AI评估时对人类的棋型稍加重视。

两种评估的使用场景

evaluatePosition(单点)

  • 普通模式:评估每个候选位置的攻防价值
  • 候选排序:为困难模式排序候选

evaluateBoard(全盘)

  • Minimax叶节点:评估搜索到底时的整体局面

为什么叶节点用全盘评估? 因为Minimax的叶节点是"未来的局面",需要整体评估而非单点评估------单点评估只考虑了最后一手的影响,可能遗漏其他方向的威胁。

总结

两种评估函数代表了不同的思考方式:

  • evaluatePosition:聚焦一点,四方向扫描------快速但不全面
  • evaluateLines:扫描全盘,逐线统计------全面但较慢

普通AI用单点评估实现快速决策,困难AI用全盘评估实现深度搜索。两者的配合让AI在不同难度下都有合理的评估能力。

相关推荐
solo_9914 分钟前
用 LSPosed 模拟录屏音频编码器卡顿:从思路到落地
android
Larcher22 分钟前
从“加载模型”界面到端侧推理:拆解一个 React + WebGPU 大模型 Demo
javascript·后端
极客先躯24 分钟前
高级java每日一道面试题-2026年05月03日-实战篇[Docker]-如何实现容器化环境的数据加密?
java·运维·docker·容器·金融·加解密·高级面试
zzq779736 分钟前
别把大模型 API Key 写进 APK:移动 AI 应用接口防盗刷实践
android·人工智能·安全·app加固·御盾安全·安卓加固
xqqxqxxq1 小时前
Java Socket 多人聊天室(私聊+群聊)技术笔记(V4版本)
java·网络·笔记
Larcher1 小时前
从状态快照到惰性初始化:读懂 React useState 的三个关键场景
javascript·人工智能·后端
wang09071 小时前
自己动手写一个spring之aop_1
java·后端·spring
国科安芯2 小时前
FreeRTOS RISC-V 浮点上下文切换移植:在 IAR 工程中完整保存 FPU 寄存器
java·开发语言·单片机·嵌入式硬件·算法·系统架构·risc-v
deng12042 小时前
【Thread类的用法(1)-- Thread类的基本用法】
java·java-ee
Hilaku2 小时前
工作 5 年后,决定你薪资上限的究竟是什么?
前端·javascript·程序员