To challenge myself, I set out to create a tic-tac-toe game, complete with a Minimax-driven AI. I had already attempted this challenge once when I was following the FreeCodeCamp curriculum.
For this review I would kindly ask everyone who is nice enough to give it a moment of their time to please advise me if my approach is the best way of interacting with the DOM? I am currently using objects with DOM elements as properties. This avoids having to use IDs, classes or data attributes. I am still very much a beginner so I am trying to use Vanilla JS as much as possible, figuring it would give me a good basis for any frameworks I might decide to learn.
I also have a question about performance. I know it is not the best, but my understanding of JS/CS in general is not deep enough to know how to better it. As it stands, the initial call to Minimax()
takes a whopping 1080 ms on average.
For a working example, see this code Pen.
Here is the JS for easy reference:
const BOARD = document.querySelector('.board');
const BUTTONS = document.querySelector('.buttons')
let GRID = [];
function Player () {
this.state = 0;
this.symbol;
this.moveCount = 0;
}
function PlayerAI () {
this.state = 1;
this.symbol;
this.moveCount;
this.moveCountClone;
}
function Game (state) {
this.running = state;
}
let game = new Game(true);
let player = new Player;
let playerAI = new PlayerAI;
function init(){
game.running = true;
BUTTONS.addEventListener('click', function(e){
player.symbol = e.target.textContent;
player.symbol == 'O' ? playerAI.symbol = 'X' : playerAI.symbol = 'O';
BUTTONS.classList.add('hidden');
GRID.forEach(function(el){
el.activate()
})
startGame(player, playerAI);
})
}
// CELL FILE
let count = 1;
let fakeBoard = [2,2,2,0,0,0,2,2,2];
function Cell (index) {
this.index = index;
this.state = 2;// default state;
this.parent = BOARD;
this.DOMElement = document.createElement("div");
}
Cell.prototype.render = function () {
this.parent.appendChild(this.DOMElement);
this.DOMElement.classList.add("cell");
}
Cell.prototype.activate = function() {
this.DOMElement.classList.add("active");
}
// draw grid
for(var i = 0; i < 9; i++){
GRID.push(new Cell(i));
GRID[i].render();
}
// END CELL FILE
function startGame (){
GRID.forEach(function(cell){
cell.DOMElement.addEventListener('click', function(){
if(cell.DOMElement.textContent == ''){
cell.DOMElement.classList.remove('active');
cell.DOMElement.textContent = player.symbol;
cell.state = player.state;
handleWin(checkForTheWin(GRID, winStateMatrix, player))
if(game.running === true){
AIMove();
}
}
});
})
}
function AIMove () {
let myMoves = possibleMoves(GRID);
if(myMoves.length > 0){
let myMove = myMoves[0].index;
let next = miniMax(GRID, 0);
makeMove(GRID, next.index, playerAI)
displayMoveOnBoard(next.index, playerAI)
handleWin(checkForTheWin(GRID, winStateMatrix, playerAI));
}
}
// ai 1, human 0
function score(int, depth){
if(int == 1){
return 100 - depth;
}
if(int == 0){
return -100 + depth;
}
if(int == 2){
return 0; // prefer draw, should be 0;
}
}
function miniMax(gameState, depth){
let moves = possibleMoves(gameState);
bestMove = moves[0];
bestScore = -1000;
moves.forEach(function(move){
let clone = JSON.parse(JSON.stringify(gameState));
makeMove(clone, move.index, playerAI);
let score = min(clone, depth+1);
if(score > bestScore){
bestMove = move;
bestScore = score;
}
})
return bestMove;
}
function max(gameState, depth){
let check = checkForTheWin(gameState, winStateMatrix, player);
if(check >= 0){
return score(check, depth);
}
let moves = possibleMoves(gameState);
let bestScore = -1000;
let bestMove;
moves.forEach(function(move){
let clone = JSON.parse(JSON.stringify(gameState));
makeMove(clone, move.index, playerAI);
let score_value = min(clone, depth+1);
if(score_value > bestScore){
bestScore = score_value;
bestMove = move;
}
})
return bestScore;
}
function min(gameState, depth){
let check = checkForTheWin(gameState, winStateMatrix, playerAI);
if(check){
return score(check, depth);
}
let moves = possibleMoves(gameState);
let bestScore = 1000;
let bestMove;
moves.forEach(function(move){
let clone = JSON.parse(JSON.stringify(gameState));
makeMove(clone, move.index, player);
let score_value = max(clone, depth+1);
if(score_value < bestScore){
bestScore = score_value;
bestMove = move;
}
})
return bestScore;
}
let winStateMatrix = [[0,1,2],
[3,4,5],
[6,7,8],
[0,3,6],
[1,4,7],
[2,5,8],
[0,4,8],
[2,4,6]]
function checkForTheWin(gamestate, winMatrix, whichPlayer){
let stateOne = 1;
let stateZero = 0;
for(let i = 0; i < winMatrix.length; i++){
if(gamestate[winMatrix[i][0]].state == whichPlayer.state && gamestate[winMatrix[i][1]].state == whichPlayer.state && gamestate[winMatrix[i][2]].state == whichPlayer.state){
if(whichPlayer.state == stateOne){
return 1;
}
if(whichPlayer.state == stateZero){
//
return 0;
}
}
}
let moves = possibleMoves(gamestate);
if(!moves){
return 2;
}
}
function handleWin(potentialWin){
//takes in checkforthewin function
if(potentialWin == 2){
game.running = false;
alert('DRAW')
window.location.reload(true);
}
if(potentialWin == 1){
game.running = false;
alert('AI WIN, RELOAD THE PAGE TO PLAY AGAIN')
window.location.reload(true);
}
if(potentialWin == 0){
game.running = false;
alert('PLAYER WIN, RELOAD THE PAGE TO PLAY AGAIN')
window.location.reload(true);
}
}
function possibleMoves (gamestate){
let arr = gamestate.filter(function(el){
return el.state == 2;
})
if(arr.length < 1){
return false;
} else {
return arr;
}
}
function displayMoveOnBoard(moveAsIndex, whichPlayer){
GRID[moveAsIndex].DOMElement.classList.remove('active');
GRID[moveAsIndex].DOMElement.textContent = whichPlayer.symbol;
}
function makeMove(board, moveAsIndex, whichPlayer){
board[moveAsIndex].state = whichPlayer.state;
}
init();