实现四子棋Minimax AI时遭遇Uncaught TypeError错误求助
问题描述
开发四子棋AI时出现以下错误:
Uncaught TypeError: Cannot create property '35' on string '' at Board.insert (board.js:394:26) at player.js:29:15 at Array.forEach (<anonymous>) at Player.getBestMove (player.js:27:33) at script.js:8:20
已完成棋盘、胜负/平局检测及AI逻辑实现,getLowestEmptyCell()为自行实现,代码参考井字棋AI教程。
相关代码
board.js
export default class Board { constructor(state = ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""]) { this.state = state; } printFormattedBoard() { let formattedString = ''; this.state.forEach((cell, index) => { formattedString += cell ? ` ${cell} |` : ` |`; if ((index + 1) % 7 === 0) { formattedString = formattedString.slice(0, -1); if (index < 41) formattedString += '\n――― ――― ――― ――― ――― ――― ―――\n' } }); console.log('%c' + formattedString, 'color: #c11dd4; font-size: 16px;'); } isEmpty() { return this.state.every(cell => !cell); } isFull() { return this.state.every(cell => cell); } isTerminal() { if (this.isEmpty()) return false; /* 320 lines of winning combinations */ if (this.isFull()) { return { 'winner': 'draw' }; } return false; } getLowestEmptyCell(index) { if (index > 41 || index < 0 || this.state[index]) return NaN; let i = 0; if (index >= 0) i = 35; if (index >= 7) i = 28; if (index >= 14) i = 21; if (index >= 21) i = 14; if (index >= 28) i = 7; if (index >= 35) i = 0; for (i; i > -1; i -= 7) { if (!this.state[index + i]) return index + i; } } insert(symbol, position) { if (![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41].includes(position)) throw new Error('Cell index does not exist or is not possible!'); if(!['r', 'y'].includes(symbol)) throw new Error('The symbol can only be an r or a y!'); if (this.state[position]) return false; position = this.getLowestEmptyCell(position); this.state[position] = symbol; // error thrown here return true; } getAvailableMoves() { let moves = []; for (let i = 0; i < 7; i++) { if (!this.state[i]) moves.push(this.getLowestEmptyCell(i)); } return moves; } }
player.js
import Board from './board.js'; export default class Player { constructor(maxDepth = -1) { this.maxDepth = maxDepth; this.nodesMap = new Map(); } getBestMove(board, maximising = true, callback = () => {}, depth = 0) { if (depth === 0) this.nodesMap.clear(); if (board.isTerminal() || depth === this.maxDepth) { if (board.isTerminal().winner === 'r') { return 100 - depth; } else if (board.isTerminal().winner === 'y') { return -100 + depth; } return 0; } if (maximising) { let best = -100; board.getAvailableMoves().forEach(index => { const child = new Board([...board.state]); child.insert('r', index); const nodeValue = this.getBestMove(child, false, callback, depth + 1); best = Math.max(best, nodeValue); if (depth === 0) { const moves = this.nodesMap.has(nodeValue) ? `${this.nodesMap.get(nodeValue)},${index}` : index; this.nodesMap.set(nodeValue, moves); } }); if (depth === 0) { let returnValue; if (typeof this.nodesMap.get(best) === 'string') { const arr = this.nodesMap.get(best).split(','); returnValue = arr[Math.floor(Math.random() * arr.length)]; } else { returnValue = this.nodesMap.get(best); } callback(returnValue); return returnValue; } return best; } if (!maximising) { let best = 100; board.getAvailableMoves().forEach(index => { const child = new Board([...board.state]); child.insert('y', index); const nodeValue = this.getBestMove(child, false, callback, depth + 1); best = Math.max(best, nodeValue); if (depth === 0) { const moves = this.nodesMap.has(nodeValue) ? `${this.nodesMap.get(nodeValue)},${index}` : index; this.nodesMap.set(nodeValue, moves); } }); if (depth === 0) { let returnValue; if (typeof this.nodesMap.get(best) === 'string') { const arr = this.nodesMap.get(best).split(','); returnValue = arr[Math.floor(Math.random() * arr.length)]; } else { returnValue = this.nodesMap.get(best); } callback(returnValue); return returnValue; } return best; } } }
script.js
import Board from './classes/board.js'; import Player from './classes/player.js'; const board = new Board(["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""]); const player = new Player(); console.log(player.getBestMove(board)); board.printFormattedBoard(); //console.log(player.nodesMap);
问题排查与修复
核心问题分析
- Minimax逻辑错误:非最大化玩家(y)的分数比较使用了
Math.max,应改为Math.min;递归调用时未切换玩家状态,始终传入false,导致逻辑混乱。 - 边界情况未处理:
getLowestEmptyCell函数在列已满时无返回值(返回undefined),导致insert方法中对字符串类型的this.state进行赋值操作(递归过程中因逻辑错误导致board.state变为字符串)。 - Move存储类型错误:使用字符串拼接存储可行Move,导致返回的最佳Move为字符串类型,后续处理时引发索引错误。
修复步骤
1. 修复Player.js中的Minimax逻辑
- 非最大化分支使用
Math.min获取最低分数 - 递归调用时切换玩家状态(传入
true而非false) - 使用数组存储可行Move,避免字符串类型问题
修改后的player.js关键部分:
// ... if (!maximising) { let best = 100; board.getAvailableMoves().forEach(index => { const child = new Board([...board.state]); child.insert('y', index); // 修复:递归调用时切换为maximising玩家 const nodeValue = this.getBestMove(child, true, callback, depth + 1); // 修复:minimising玩家取最小分数 best = Math.min(best, nodeValue); if (depth === 0) { // 修复:用数组存储move,避免字符串拼接 const moves = this.nodesMap.has(nodeValue) ? [...this.nodesMap.get(nodeValue), index] : [index]; this.nodesMap.set(nodeValue, moves); } }); if (depth === 0) { let returnValue; const bestMoves = this.nodesMap.get(best); // 直接从数组中随机选择 returnValue = bestMoves[Math.floor(Math.random() * bestMoves.length)]; callback(returnValue); return returnValue; } return best; } // ...
2. 修复Board.js中的边界处理
getLowestEmptyCell函数在列已满时返回nullinsert方法检查position是否为null,避免无效赋值getAvailableMoves过滤掉无效的null值
修改后的board.js关键部分:
// ... getLowestEmptyCell(index) { if (index > 41 || index < 0 || this.state[index]) return NaN; let i = 0; if (index >= 0) i = 35; if (index >= 7) i = 28; if (index >= 14) i = 21; if (index >= 21) i = 14; if (index >= 28) i = 7; if (index >= 35) i = 0; for (i; i > -1; i -= 7) { if (!this.state[index + i]) return index + i; } // 修复:列已满时返回null return null; } insert(symbol, position) { if (![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41].includes(position)) throw new Error('Cell index does not exist or is not possible!'); if(!['r', 'y'].includes(symbol)) throw new Error('The symbol can only be an r or a y!'); if (this.state[position]) return false; position = this.getLowestEmptyCell(position); // 修复:检查position是否有效 if (position === null || isNaN(position)) return false; this.state[position] = symbol; return true; } getAvailableMoves() { let moves = []; for (let i = 0; i < 7; i++) { if (!this.state[i]) { const pos = this.getLowestEmptyCell(i); // 修复:过滤无效的null/NaN值 if (pos !== null && !isNaN(pos)) moves.push(pos); } } return moves; } // ...
3. 验证修复
运行script.js后,AI将正确计算最佳Move,不再出现类型错误。
内容的提问来源于stack exchange,提问作者Archigan
相关产品推荐
相关产品推荐

