单点评估 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);
}
评估流程:
- 以落子点为中心
- 正方向延伸:统计连续同色棋子,检查端点是否开放
- 反方向延伸:同上
- 用
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;
}
逐线扫描逻辑:
- 沿着方向遍历每个格子
- 遇到同色棋子:
consecutive++ - 遇到不同色或空位:结算之前的连续段
- 结算时检查两端是否开放
- 用
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在不同难度下都有合理的评估能力。