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在不同难度下都有合理的评估能力。

相关推荐
奋斗的小方1 小时前
Java进阶篇1-3:集合框架(单列集合)
java·windows·python
甜味弥漫1 小时前
后端开发入门:业务能力、数据库与程序员的核心竞争力
javascript·node.js·全栈
湮w1 小时前
JavaWeb(三)JavaScript详细讲解
java·javascript·web
gcw10242 小时前
Cron专业工具:定时任务与开发办公的一站式在线工具箱
java·前端·数据库·工具·crontab·cron·spring cron
whaledown2 小时前
互联网大厂Java求职面试三轮提问详解(涵盖Spring Boot、微服务、Kafka等核心技术)
java·jvm·数据库·spring boot·微服务·面试·kafka
xixingzhe22 小时前
Spring 依赖注入
java·后端·spring
私人珍藏库2 小时前
[Android] Npatch-免Root框架+可内置模块
android·人工智能·智能手机·工具·软件
带刺的坐椅2 小时前
多 Agent 协作实战:任务编排与子代理系统
java·ai·llm·agent·solon·harness
黄林晴2 小时前
KMP 正式支持 SwiftPM:CocoaPods 迁移配置指南
android·ios·kotlin