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. 用count和openCount查表得分

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

相关推荐
新*25 分钟前
从一段 WebSocket 代码聊开:bytes 不是 zlib、binaryType 决定一切,15MB 大消息的零拷贝优化
javascript·python·websocket·性能优化·blob·zlib·arraybuffer
SL_staff27 分钟前
JVS-BI 实战:业务人员如何用零代码 ELT + 可视化配置构建实时经营分析闭环
java·spring·全栈
SL_staff1 小时前
企业AI落地难?JVS-AI渐进式实践:安全可控、小切口验证、标准化沉淀与规模化复制
java·开源·aigc
OpsEye1 小时前
别再挨个充会员了:多模型编码,最麻烦的从来不是选模型
javascript·ai编程
SL_staff2 小时前
JVS数字底座实践:构建企业级统一内容管理平台的技术路径
java·产品
nice_lcj5202 小时前
【从零理解 Spring AI · 12】MCP 的两种角色:做客户端还是做服务端
java·后端·spring·spring ai·ai应用开发
学以智用3 小时前
jsencrypt.js 完整详解
javascript
桦说编程3 小时前
TransmittableThreadLocal 解读:从 CRR 机制到上下文所有权
java·后端·源码
xys_6784 小时前
JavaEE通关手册:初识Java
java·开发语言
文丑颜不良啊4 小时前
LeetCode2461~2480题目解析
java·leetcode