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();