JavaScript beginner here! I have written a Tic Tac Toe game is JS that seems to be unbeatable (Title says 'almost' because I'm not sure if it's really unbeatable or just I can't beat it :) ).
I really appreciate any help to improve my code and learn from my mistakes.
Project's CodePen = https://codepen.io/MiladM1715/full/ZEQOLmZ
// Tic Tac Toe Win figures
var gameCtrl = (function () {
var winningConditions, corners, randomCorner;
winningConditions = [
[3, 4, 5],
[2, 4, 6],
[0, 4, 8],
[0, 1, 2],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
];
corners = [0, 2, 6, 8];
randomCorner = corners[Math.floor(Math.random() * 4)];
// If win possible? Win! if not? Block
function winOrBlock(arr, marker, winCondition) {
var status;
// Count number of founded markers (first user & then opponent) if more the two, win or block
var count = 0;
if (arr[0] === marker) count++;
if (arr[1] === marker) count++;
if (arr[2] === marker) count++;
if (count >= 2 && arr.includes("")) {
// Return empty marker to use
if (arr[0] === "") status = winCondition[0];
if (arr[1] === "") status = winCondition[1];
if (arr[2] === "") status = winCondition[2];
return status;
}
}
// Don't put marker somewhere that there's no chance to win
function noStupidMove(arr, marker, winCondition) {
var checkCorners;
var count = 0;
if (arr[0] === '') count++;
if (arr[1] === '') count++;
if (arr[2] === '') count++;
if (arr.includes(marker) && count > 1) {
return winCondition[arr.indexOf("")];
}
}
// If none of others work
function neturalMove(arr, marker, winCondition) {
// If win figures include marker, and there
if(arr.includes(marker) && arr.includes('')) {
return winCondition[arr.indexOf("")];
}
}
//Function to add moves id to game board structure
return {
addToBoard: function (id, marker, board) {
board[id] = marker;
},
// Works for first and 2nd move
firstMoves: function (board, counter, moves) {
var result;
// after opponent's first move, if Center is empty, place it in center, If not? random corner
if (counter === 1) {
return board[4] === "" ? 4 : randomCorner;
} else {
// If it's opponent's second move, check moves array and decide. If none of conditions met, then return false and let winOrBlock or neturalMove do it's job
if (moves[0] === 0 && moves[1] === 7) result = 6;
if (moves[0] === 6 && moves[1] === 5) result = 1;
if (moves[0] === 4 && moves[1] === 8) result = 2;
if (moves[0] === 4 && moves[1] === 2) result = 8;
return board[result] === "" ? result : false;
}
},
// Check if there is a chance for win, block or netural move || check win too
checkStatus: function (board, type, marker, counter) {
var a, b, c, winCondition, callback, check, opMarker;
// Set oponet marker based on currnt marker
marker === "O" ? (opMarker = "X") : (opMarker = "O");
if (type === "check" && counter !== 0) {
// Call functions based on stategy 1.win 2.block 3.netural
callback = [
[winOrBlock, marker],
[winOrBlock, opMarker],
[noStupidMove, marker],
[neturalMove, marker],
];
} else if (type === "check" && counter === 0) {
return randomCorner;
} else if (type === "win") {
callback = "1";
}
for (var x = 0; x < callback.length; x++) {
for (var i = 0; i < winningConditions.length; i++) {
winCondition = winningConditions[i];
a = board[winCondition[0]];
b = board[winCondition[1]];
c = board[winCondition[2]];
// Check win or place number?
if (type === "check") {
check = callback[x][0]([a, b, c], callback[x][1], winCondition);
if (check || check === 0) {
return check;
}
// if check 'type' is "win" only check for win
} else if (type === "win") {
// If a,b,c are same and not empty then it's a win
if (a === b && b === c && c !== "") {
return true;
}
}
}
}
},
// If there is no empty cell, it's a draw (called after win check)
isDraw: function (board) {
return !board.includes("");
},
};
})();
// Takes care of UI
var UICtrl = (function () {
return {
DOMstrings: {
startBtn: '.start-btn',
userScore: '.sc-',
gameResult: '.result',
finalMsg: '.msg',
gameCells: '.cells',
gameCell: '.cell',
},
clearUI: function () {
var cells, cellArr;
cells = document.querySelectorAll(this.DOMstrings.gameCell);
cellArr = Array.prototype.slice.call(cells);
cellArr.forEach(function (cur) {
cur.textContent = "";
});
},
// Add marker to UI
addMarkerUI: function (id, marker) {
var color;
marker === "X" ? (color = "black") : (color = "white");
document.getElementById(
id
).innerHTML = `<span style="color: ${color}">${marker}</span>`;
},
// disable start btn afte start and Enable it after draw or win
disableStartBtn: function (state) {
document.querySelector(this.DOMstrings.startBtn).disabled = state;
},
// Display score on UI
displayScore: function (player, score) {
document.querySelector(this.DOMstrings.userScore + player).textContent = score[player];
},
// display Win or Draw result
displayResult: function (win, draw, player) {
var msg, resultDiv;
player === 0 ? (player = "YOU WIN!") : (player = "YOU LOSE!");
if (win) msg = player;
if (draw) msg = "DRAW";
resultDiv = document.querySelector(this.DOMstrings.gameResult);
resultDiv.style.display = "flex";
document.querySelector(this.DOMstrings.finalMsg).textContent = msg;
setTimeout(function () {
resultDiv.style.display = "none";
}, 2000);
},
};
})();
// Control game behavior
var controll = (function () {
var gameBoard,
isActive,
playerMarker,
currentPlayer,
score,
counter,
twoMoveArr,
DOM;
gameBoard = ["", "", "", "", "", "", "", "", ""];
isActive = true;
playerMarker = ["X", "O"];
currentPlayer = 0;
score = [0, 0];
twoMoveArr = [];
counter = 0;
whoIsPlayingFirst = 0;
DOM = UICtrl.DOMstrings;
// Game Start
document.querySelector(DOM.startBtn).addEventListener("click", function () {
// 1.hide start btn
UICtrl.disableStartBtn(true);
// 2 Reset game UI
UICtrl.clearUI();
// 3 Active game
isActive = true;
// 4. Decide who's playing first
changePlayer();
});
// changes player after hitting start btn and invokes functions
function changePlayer() {
whoIsPlayingFirst === 1 ? (whoIsPlayingFirst = 0) : (whoIsPlayingFirst = 1);
whoIsPlayingFirst === 1 ? userPlay() : AIplay();
}
function userPlay() {
document.querySelector(DOM.gameCells).addEventListener("click", function (e) {
// Works only if clicked cell is empty and game is active
if (isActive && e.target.textContent === "") {
// 1. Get clicked cell and set marker
var cellID = parseInt(e.target.id);
var marker = playerMarker[0];
// Add Selected cell to board and UI
handleDataUI(cellID, marker, gameBoard);
// increase counter to findout play count
counter++;
// Push first two moves into an array to use it later fo blocking
counter < 2 ? twoMoveArr.push(cellID) : (twoMoveArr = false);
// Check for Win or Draw
var win, draw;
win = resultChecker(score, currentPlayer);
draw = resultChecker(score, currentPlayer);
if (!win && !draw) {
AIplay();
}
}
});
}
function AIplay() {
// Change player id to 1
currentPlayer = 1;
// Set Marker
marker = playerMarker[1];
// If User plays first
if (whoIsPlayingFirst === 1) {
// check for first and second moves
firstTwo = gameCtrl.firstMoves(gameBoard, counter, twoMoveArr);
if (counter < 3 && typeof firstTwo === "number") {
cellID = firstTwo;
} else {
// If itsn't two first moves or it returned False, Try to win, block or netural move
cellID = gameCtrl.checkStatus(gameBoard, "check", marker, counter);
}
// If AI plays first, if it's first move then, place marker on random corner. if not first move then try to win or block or do netural move
} else {
cellID = gameCtrl.checkStatus(gameBoard, "check", marker, counter);
}
// Add it to Data strucure and UI
handleDataUI(cellID, marker, gameBoard);
// Check result
resultChecker(score, currentPlayer);
currentPlayer = 0;
}
// adds moves to data and UI
function handleDataUI(id, marker, board) {
gameCtrl.addToBoard(id, marker, board);
UICtrl.addMarkerUI(id, marker);
}
// Checks for win and draw
function resultChecker(score, currentPlayer) {
var win = gameCtrl.checkStatus(gameBoard, "win");
var draw = gameCtrl.isDraw(gameBoard);
if (win) {
score[currentPlayer] += 1;
UICtrl.displayScore(currentPlayer, score);
UICtrl.displayResult(win, false, currentPlayer);
resetGame();
return true;
}
if (draw) {
UICtrl.displayResult(false, draw, currentPlayer);
resetGame();
return true;
}
return false;
}
// Resets game after every game
function resetGame() {
document.querySelector(DOM.startBtn).textContent = "Play Again";
gameBoard = ["", "", "", "", "", "", "", "", ""];
currentPlayer = 0;
isActive = false;
counter = 0;
twoMoveArr = [];
// Enables start btn
UICtrl.disableStartBtn(false);
}
return {
init: function () {
UICtrl.clearUI();
},
};
})(gameCtrl, UICtrl);
controll.init();