You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

实现四子棋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);
问题排查与修复

核心问题分析

  1. Minimax逻辑错误:非最大化玩家(y)的分数比较使用了Math.max,应改为Math.min;递归调用时未切换玩家状态,始终传入false,导致逻辑混乱。
  2. 边界情况未处理:getLowestEmptyCell函数在列已满时无返回值(返回undefined),导致insert方法中对字符串类型的this.state进行赋值操作(递归过程中因逻辑错误导致board.state变为字符串)。
  3. 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函数在列已满时返回null
  • insert方法检查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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 16:45:44