如何修复井字棋 Negamax 算法中的错误输出问题

阿敏大大_8974

阿敏大大_8974

2026-09-08

844人浏览

原创

如何修复井字棋 Negamax 算法中的错误输出问题

本文深入分析井字棋中 negamax 算法常见实现错误,重点指出基础分值翻转逻辑、状态回滚一致性、胜负判定健壮性等关键缺陷,并提供修正后的完整代码与调试建议。

本文深入分析井字棋中 negamax 算法常见实现错误,重点指出基础分值翻转逻辑、状态回滚一致性、胜负判定健壮性等关键缺陷,并提供修正后的完整代码与调试建议。

Negamax 算法是 Minimax 的简洁变体,其核心思想是:所有节点均以当前玩家视角评估,子节点得分需统一取负后再递归。这意味着它不区分“极大层”或“极小层”,而是通过符号翻转统一建模双方利益对立性。你的代码已具备基本骨架,但存在几处关键偏差,导致决策错误。

? 核心错误:基础情形(Base Case)的得分未正确翻转

在原始代码中:

if (this.isGameOver(board) || emptyCells.length === 0) {
  const score = this.evaluate(board);
  return { score: maxmizingPlayer ? score : -score }; // ❌ 错误!
}

这是对 Negamax 原理的根本误解。Negamax 要求:无论当前是谁的回合,返回的 score 必须是“从当前玩家视角”看到的估值;而递归调用时已通过 -negamax(...).score 实现视角切换,因此基础情形必须无条件返回 -score(即当前玩家视角下的真实效用)

✅ 正确写法应为:

if (this.isGameOver(board) || emptyCells.length === 0) {
  const score = this.evaluate(board); // evaluate 返回:己方赢 → +∞,对方赢 → −∞,平局 → 0
  return { score: -score }; // ✅ 统一取负:使返回值始终表示「当前递归层玩家」的收益
}

? 为什么?因为 evaluate() 是面向“游戏终局状态”定义的(例如:mySymbol 赢 → +Infinity),但它描述的是静态局面价值,而非“当前调用者”的收益。Negamax 要求每层返回值都代表「该层所代表玩家」的收益。由于递归入口总是由 AI(最大化方)发起,中间层会交替切换视角,因此基础层必须做一次符号对齐——即 -(终局价值) 才能保证:当轮到对手走且局面已胜时,返回负无穷(对当前玩家是灾难)。

⚠️ 其他易忽略但致命的问题

1. evaluate() 的设计必须严格对称且完备

确保 isWinning()(注意拼写:isWiningisWinning)覆盖全部 8 种获胜模式(3 行 + 3 列 + 2 对角线),且逻辑无歧义:

isWinning(board, symbol) {
  const wins = [
    [[0,0],[0,1],[0,2]], [[1,0],[1,1],[1,2]], [[2,0],[2,1],[2,2]], // rows
    [[0,0],[1,0],[2,0]], [[0,1],[1,1],[2,1]], [[0,2],[1,2],[2,2]], // cols
    [[0,0],[1,1],[2,2]], [[0,2],[1,1],[2,0]]                       // diags
  ];
  return wins.some(triple => 
    triple.every(([y,x]) => board[y][x] === symbol)
  );
}

2. 状态回滚必须 100% 可靠

你使用 board[y][x] = this.emptySymbol 恢复状态,这仅在 board 是深拷贝或每次递归前已克隆时才安全。若 board 是共享引用(典型错误),多个分支将互相污染。✅ 强烈建议在递归前克隆棋盘:

const newBoard = board.map(row => [...row]); // 浅拷贝二维数组
newBoard[y][x] = maxmizingPlayer ? this.mySymbol : this.opSymbol;
const move = {
  score: -this.negamax(newBoard, -beta, -alpha, !maxmizingPlayer).score
};
// 无需手动恢复 —— newBoard 是局部副本

3. Alpha-Beta 剪枝逻辑需匹配 Negamax 约定

你的剪枝部分基本正确,但注意变量命名应统一(maxmizingPlayermaximizingPlayer),并确保 alpha/beta 初始化合理:

// 外部首次调用推荐:
this.negamax(board, -Infinity, +Infinity, true);

// 内部循环中:
let bestMove = { score: -Infinity };
for (/* ... */) {
  // ...
  if (move.score > bestMove.score) {
    bestMove = move;
  }
  alpha = Math.max(alpha, bestMove.score); // 注意:此处用 > 而非 >=,避免平局误剪
  if (alpha >= beta) break; // 更标准的写法(语义等价于 beta <h3>✅ 修正后的完整 <code>negamax</code> 方法</h3><pre class="brush:php;toolbar:false;">static negamax(board, alpha, beta, maximizingPlayer) {
  const emptyCells = this.getEmptyCells(board);
  if (this.isGameOver(board) || emptyCells.length === 0) {
    const score = this.evaluate(board);
    return { score: -score }; // ✅ 关键修复:无条件取负
  }

  let bestMove = { score: -Infinity };
  for (let i = 0; i  [...row]);
    newBoard[y][x] = maximizingPlayer ? this.mySymbol : this.opSymbol;

    const childScore = -this.negamax(newBoard, -beta, -alpha, !maximizingPlayer).score;
    const move = { score: childScore, x, y };

    if (move.score > bestMove.score) {
      bestMove = move;
    }

    alpha = Math.max(alpha, bestMove.score);
    if (alpha >= beta) break; // Beta 剪枝
  }
  return bestMove;
}

static evaluate(board) {
  if (this.isWinning(board, this.mySymbol)) return Infinity;
  if (this.isWinning(board, this.opSymbol)) return -Infinity;
  return 0;
}

? 总结与调试建议

  • 永远记住:Negamax 的 evaluate() 输出是“客观局面价值”,而 negamax() 函数返回值必须是“当前玩家视角收益”,二者通过 return { score: -score } 对齐。
  • 杜绝共享状态:递归中修改原始 board 是多数 Bug 的根源,务必使用副本。
  • 验证胜负判定:添加单元测试,穷举所有 3×3 胜局组合。
  • 启用日志追踪:在 negamax 开头打印 depthalphabetaemptyCells.length,观察剪枝是否过早触发。
  • 平局行为:若多个走法得分相同(如均为 0),算法会返回首个最优解——这并非错误,但可通过打乱空位顺序实现更自然的随机性。

遵循以上原则,你的 Negamax 将稳定输出最优落子,真正体现博弈算法的优雅与力量。

PHP速学视频免费教程(入门到精通)
PHP速学视频免费教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

相关标签:

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

相关专题

更多
js获取数组长度的方法
js获取数组长度的方法

在js中,可以利用array对象的length属性来获取数组长度,该属性可设置或返回数组中元素的数目,只需要使用“array.length”语句即可返回表示数组对象的元素个数的数值,也就是长度值。php中文网还提供JavaScript数组的相关下载、相关课程等内容,供大家免费下载使用。

2023.06.20

4126

5

js刷新当前页面
js刷新当前页面

js刷新当前页面的方法:1、reload方法,该方法强迫浏览器刷新当前页面,语法为“location.reload([bForceGet]) ”;2、replace方法,该方法通过指定URL替换当前缓存在历史里(客户端)的项目,因此当使用replace方法之后,不能通过“前进”和“后退”来访问已经被替换的URL,语法为“location.replace(URL) ”。php中文网为大家带来了js刷新当前页面的相关知识、以及相关文章等内容

2023.07.04

1049

3

js四舍五入
js四舍五入

js四舍五入的方法:1、tofixed方法,可把 Number 四舍五入为指定小数位数的数字;2、round() 方法,可把一个数字舍入为最接近的整数。php中文网为大家带来了js四舍五入的相关知识、以及相关文章等内容

2023.07.04

4024

6

js删除节点的方法
js删除节点的方法

js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

2023.09.01

820

4

JavaScript转义字符
JavaScript转义字符

JavaScript中的转义字符是反斜杠和引号,可以在字符串中表示特殊字符或改变字符的含义。本专题为大家提供转义字符相关的文章、下载、课程内容,供大家免费下载体验。

2023.09.04

1716

5

js生成随机数的方法
js生成随机数的方法

js生成随机数的方法有:1、使用random函数生成0-1之间的随机数;2、使用random函数和特定范围来生成随机整数;3、使用random函数和round函数生成0-99之间的随机整数;4、使用random函数和其他函数生成更复杂的随机数;5、使用random函数和其他函数生成范围内的随机小数;6、使用random函数和其他函数生成范围内的随机整数或小数。

2023.09.04

3025

4

如何启用JavaScript
如何启用JavaScript

JavaScript启用方法有内联脚本、内部脚本、外部脚本和异步加载。详细介绍:1、内联脚本是将JavaScript代码直接嵌入到HTML标签中;2、内部脚本是将JavaScript代码放置在HTML文件的`<script>`标签中;3、外部脚本是将JavaScript代码放置在一个独立的文件;4、外部脚本是将JavaScript代码放置在一个独立的文件。

2023.09.12

3933

6

Js中Symbol类详解
Js中Symbol类详解

javascript中的Symbol数据类型是一种基本数据类型,用于表示独一无二的值。Symbol的特点:1、独一无二,每个Symbol值都是唯一的,不会与其他任何值相等;2、不可变性,Symbol值一旦创建,就不能修改或者重新赋值;3、隐藏性,Symbol值不会被隐式转换为其他类型;4、无法枚举,Symbol值作为对象的属性名时,默认是不可枚举的。

2023.09.20

2500

5

java访问控制修饰符介绍
java访问控制修饰符介绍

java访问控制修饰符有四种,分别是public、protected、private、默认访问修饰符。详细介绍:1、public,public是最宽松的访问控制修饰符,被修饰的类、方法和变量可以被任何其他类访问,当一个类、方法或变量被声明为public时,它们可以在任何地方被访问,无论是同一个包中的类还是不同包中的类;2、protected修饰符等等。

2023.09.20

828

7

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
热门推荐
/
最新课程
phpStudy极速入门视频教程
phpStudy极速入门视频教程

共6课时 | 54.6万人学习

独孤九贱(4)_PHP视频教程
独孤九贱(4)_PHP视频教程

共89课时 | 133.1万人学习