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.
I guess that
Board.stateis 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:
Playgrond Example
Runto see the expected errorTo test, if this is really the case you can set a breakpoint on the line that throws and check the
statevariable or log the typeconsole.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."