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

相关推荐
wno7042 分钟前
Spring Boot JdbcTemplate配置Druid多数据源
java·spring boot·后端
聊浮游33 分钟前
JAVA2026最新全套学习资料、学习路线
java·开发语言·jvm·mysql·spring·maven·idea
MacroZheng1 小时前
完美替代 Navicat!这款内置 AI 的数据库工具,太香了!
java·后端·mysql
程序员老赵1 小时前
Docker 部署 ZLMediaKit:轻松搭建高性能流媒体服务平台
前端·javascript·后端
SamDeepThinking1 小时前
从REST到gRPC,一个API选型的思考框架
java·后端·程序员
Zane19941 小时前
接口都能写默认实现了,为什么还需要抽象类
java·后端
未秃头的程序猿1 小时前
读Spring AI源码的方法——不是硬啃,是带着问题去翻
java·后端·spring
hunterandroid1 小时前
[Android 从零到一] ViewPager2 与 Fragment 生命周期协同:从预加载到状态一致性
android·前端
拾年2751 小时前
前后端分离开发,别再"傻等"后端接口了——聊聊前端接口工程
前端·javascript·react.js
SendTomo1 小时前
SendTomo稳定传输三大核心策略
javascript·网络协议·webrtc·html5·p2p