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.