bestMove函数遇死循环及不可迭代报错:无法找到有效最优移动位
问题分析与修复方案
问题描述
以下是井字棋AI最佳落子函数的代码及运行异常问题:
代码与变量说明
// winningCombinationsState是包含8个0-3整数的数组,记录各获胜组合的权重 // Game.winningCombinations是包含8个数组的数组,每个子数组存3个0-8的棋盘索引(对应获胜组合) // Game.state是含9个字符串的数组,记录棋盘上"X"/"O"/空位状态 function bestMove () { let index; while (index == undefined) { // 找到权重数组中第一个最大值的索引 let max = winningCombinationsState.indexOf(Math.max(...winningCombinationsState)); // 检查该获胜组合对应的棋盘位置里的首个空位 for (let slot in Game.winningCombinations[max]) { if (Game.state[slot] == "") { index = slot; break; } } // 若当前获胜组合无空位,移除该权重项 if (index == undefined) { delete winningCombinationsState[max]; } } return index; }
该函数多数情况正常,但偶尔会卡住,甚至抛出错误:Game.winningCombinations[max] is not iterable。改用for...in循环后错误消失,但仍会陷入死循环。console.log(typeof Game.winningCombinations[max])显示类型为object,但该变量定义为数组的数组。
需求示例
// 棋盘状态(示例为4格简化版) Game.state = ["X","","O",""] // 获胜组合列表 Game.winningCombinations = [[0,1],[0,2],[0,3],[1,2],[1,3],[2,3]] // 权重计算规则:"X"→1,"O"→-1,空位→0,取组合内数值的绝对值求和 winningCombinationsState = [1, 0, 1, 1, 0, 1] // 找到权重最大值的索引 max = winningCombinationsState.indexOf(Math.max(...winningCombinationsState)) // 返回0
此示例中应返回index = 1,因为Game.state[Game.winningCombinations[max][1]]是空位。核心需求是:优先选权重最高的获胜组合里的空位,若无则找次优组合的空位。
问题根源
delete操作制造数组空洞:用delete删除数组元素会保留索引,导致数组变成稀疏数组。后续Math.max(...winningCombinationsState)会包含undefined,indexOf可能返回-1,此时Game.winningCombinations[-1]不存在,抛出“不可迭代”错误。for...in遍历数组的缺陷:for...in会遍历数组的所有可枚举属性(包括原型链属性),且遍历顺序不固定,容易出错。- 无终止的死循环风险:当所有获胜组合都没有空位时,
index始终为undefined,循环会无限执行。
修复方案
方案1:修复循环逻辑,避免稀疏数组
function bestMove() { // 复制权重数组,避免修改原数据 let availableWeights = [...winningCombinationsState]; let index; // 循环终止条件:找到空位 或 所有组合都已排查 while (index === undefined && availableWeights.length > 0) { const maxValue = Math.max(...availableWeights); const maxIndex = availableWeights.indexOf(maxValue); // 获取当前权重对应的获胜组合 const combo = Game.winningCombinations[maxIndex]; // 用for...of遍历数组(专门针对可迭代对象,顺序固定) for (const slot of combo) { if (Game.state[slot] === "") { index = slot; break; } } // 若无空位,从数组中彻底移除该组合(用splice代替delete) if (index === undefined) { availableWeights.splice(maxIndex, 1); // 若允许修改原获胜组合数组,可同步移除对应项 // Game.winningCombinations.splice(maxIndex, 1); } } // 处理棋盘已满的情况 return index === undefined ? -1 : index; }
关键改进点
- 用
splice替代delete:直接移除数组元素,避免稀疏数组,保证后续Math.max和indexOf正常工作。 - 用
for...of遍历数组:遍历顺序固定,只处理数组元素,不会遍历额外属性。 - 添加循环终止条件:当
availableWeights为空时停止循环,防止死循环。 - 复制原数组:避免修改原始权重数组,不影响其他逻辑。
方案2:预筛选空位,按权重排序(更高效)
另一种逻辑更清晰的实现方式,先收集所有空位,计算每个空位对应的最高权重,再选最优:
function bestMove() { // 收集所有棋盘空位 const emptySlots = Game.state.map((val, idx) => val === "" ? idx : null).filter(idx => idx !== null); if (emptySlots.length === 0) return -1; // 计算每个空位的最高关联权重 const slotScores = emptySlots.map(slot => { // 找到包含该空位的所有获胜组合 const relevantCombos = Game.winningCombinations.filter(combo => combo.includes(slot)); // 获取这些组合的权重最大值 const maxScore = Math.max(...relevantCombos.map(combo => { const comboIndex = Game.winningCombinations.indexOf(combo); return winningCombinationsState[comboIndex]; })); return { slot, score: maxScore }; }); // 按权重降序排序,取第一个最优空位 slotScores.sort((a, b) => b.score - a.score); return slotScores[0].slot; }
这种方式避免了循环重试,逻辑更直观,性能也更稳定。
内容的提问来源于stack exchange,提问作者Capeya
相关产品推荐
相关产品推荐

