-3

I am currently in the process of making a Connect Four AI using the minimax algorithm. I have made the board and win/draw checks, and have finished implementing the AI. However, when I go to test it, I get the following error:

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

I have looked through every similar question I could find, and Google has not been of any more help. I am basing most of these functions off of this Tic-Tac-Toe AI tutorial, but the getLowestEmptyCell() method is my own.

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\u2015\u2015\u2015 \u2015\u2015\u2015 \u2015\u2015\u2015 \u2015\u2015\u2015 \u2015\u2015\u2015 \u2015\u2015\u2015 \u2015\u2015\u2015\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);

I sense that this is not anything to do with the functionality itself, but rather my cluelessness and trying to implement a custom function in the wrong places.

UPDATE: After doing many console.logs (but probably not enough), I have determined that using an array to initialise a new Board class along with ...board.state actually allows the insert() function to see that there is still, in fact, a usable this.state value with 42 empty strings in an array.

Archigan
  • 62
  • 11
  • 1
    Please for the love of Brendan Eich and all JS that is holy use a playground for a minimal reproducible example, such as https://jsfiddle.net or https://codesandbox.io - it's *extremely* hard to debug code when we can't even run it or tinker with it ourselves; not to mention you've excluded 320 lines of code that could potentially be the cause of the bug. – kelsny Oct 28 '22 at 18:33
  • @caTS If I could do that with multiple files, I would. However, with these 320 lines, they are all really just copy and paste, and only the indices are different for each one. – Archigan Oct 29 '22 at 15:11
  • can you `console.log([...board.state])` in both 2 locations in player.js ? – A. Khaled Nov 01 '22 at 22:36
  • @A.Khaled i did, it's the same for both places – Archigan Nov 02 '22 at 13:34
  • Try to `console.log(this.state)` before inserting. We want to make sure state is array when you try to insert. – A. Khaled Nov 02 '22 at 14:16

1 Answers1

0

I guess that Board.state is a string (instead of the array that you may expect).

Since strings are immutable, the following assignment is illegal and may (depending on your js-engine) throw the error that you mentioned:

const state: any = "x";
state[35] = 1;

Playgrond Example

  • click Run to see the expected error

To test, if this is really the case you can set a breakpoint on the line that throws and check the state variable or log the type console.log(this.state, typeof this.state)

To avoid such issues, you should check the type of the state parameter in the constructor and throw an error if it's not of the expected type (i.e. array of string) - or use typescript which will help for such simple errors (note, that the Playgrond Example shows an error on the assignment line and shows a meaningful error when you hoover the mouse over the line: "Index signature in type 'String' only permits reading."

TmTron
  • 17,012
  • 10
  • 94
  • 142