'use strict'; const displayChars = { "x": "✕", "o": "◯" }; const X = displayChars.x; const O = displayChars.o; var chars = []; var cellVals = ["empty", "empty", "empty", "empty", "empty", "empty", "empty", "empty", "empty"]; var turn = 0; //the current player whose turn it is (0 = human, 1 = computer) var winner = "none"; var gameOver = true; var players = {}; var playerNames = ["Player 1", "The AI"]; var factorials = [1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]; var winCombinations = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [2, 5, 8], [0, 3, 6], [1, 4, 7], [0, 4, 8], [2, 4, 6] ]; var eachOfClass = function(className, func){ //executes a function for each element with a certain class var elements = document.getElementsByClassName(className); for (let i = 0; i < elements.length; i ++){ func(elements[i], i, elements); } }; var allFilled = function(grid){ //checks if all the cells in a grid are filled by players var all = true; for (let i in grid){ if (grid[i] === "empty"){ all = false; } } return all; } var checkVictory = function(player, board){ //checks for a winner (1: the chosen player won, 0: a tie, -1: the opposite player won, false: no one won) for (let i in winCombinations){ if ( board[winCombinations[i][0]] === board[winCombinations[i][1]] && board[winCombinations[i][0]] === board[winCombinations[i][2]] && board[winCombinations[i][0]] !== "empty" ){ //console.log(player); return (cellVals[winCombinations[i][0]] === chars[player? 1: 0])? 1: -1; }else{ //console.log(cellVals[winCombinations[i][0]]); //if (cellVals[winCombinations[i][0]] !== "empty") console.log(cellVals[winCombinations[i][0]] + ", " + cellVals[winCombinations[i][1]] + ", " + cellVals[winCombinations[i][2]]); } } var all = true; for (let i in board){ if (board[i] === "empty"){ all = false; } } if (all){ return 0; } return false; }; var flyDown = function(div){ div.classList.remove("fly-up"); div.classList.add("fly-down"); }; var flyUp = function(div){ div.classList.remove("fly-down"); div.classList.add("fly-up"); }; var choosePlayers = function(){ playerNames[1] = this.dataset.player2; flyUp(document.getElementById("choose-players")); flyDown(document.getElementByid("choose-char")); }; var chooseChar = function(){ /*players[this.dataset.char] = "player"; chars["player"] = this.dataset.char;*/ }; var newGame = function(c){ //resets variables and sets up elements for play eachOfClass("cell", function(cell, i){ cellVals[i] = "empty"; cell.classList.add('empty'); cell.classList.remove("winning"); cell.classList.remove("player"); cell.classList.remove("computer"); cell.classList.remove("full"); cell.innerHTML = ""; var cellChar = document.createElement("span"); cellChar.classList.add("player-char"); cellChar.innerHTML = c; cell.appendChild(cellChar); }); gameOver = false; turn = 0; }; var chooseCell = function(cellVal, player){ //fill in the cell with the player's symbol and update the global grid if (cellVals[cellVal] === "empty"){ cellVals[cellVal] = chars[player]; var cell = document.getElementsByClassName("cell")[cellVal]; cell.classList.remove("empty"); cell.classList.add("full"); cell.innerHTML = chars[player]; var victory = checkVictory(player, cellVals); if (victory !== false){ gameOver = true; document.getElementById("win-text").innerHTML = (function(){ switch(victory){ case 1: return playerNames[player] + " wins! "; case 0: return "It's a tie..." case -1: return playerNames[player? 0: 1] + " wins! "; } })(); var chooseWindow = document.getElementById("choose-window"); var winWindow = document.getElementById("win-window"); winWindow.classList.add("fly-down"); winWindow.classList.remove("fly-up"); var restart = setTimeout(function(){ winWindow.classList.remove("fly-down"); winWindow.classList.add("fly-up"); /*console.log(chars); console.log(player); console.log(chars[player]);*/ chooseWindow.classList.remove("fly-up"); chooseWindow.classList.add("fly-down"); }, 1500); } } }; var emptyCellCount = function(board = cellVals){ //returns the amount of empty cells in a grid var empty = 0; for (let i in board){ if (board[i] === "empty"){ empty ++; } } return empty; } var findBestMove = function(player, grid, depth = 0, async = false, callback = null){ //the AI, which finds the best move to make, AKA what results in the most benefit for it, and the least benefit for the opposite player. Uses recursion to find all possible outcomes can best moves. if (depth === 0 && emptyCellCount(grid) > 7){ //if it is the first or second turn, just do one of these predetermined moves var starters = [4, 0, 2, 6, 8]; for (let i in starters){ if (grid[starters[i]] === "empty"){ if (callback){ callback(starters[i]); } return starters[i]; } } }else if (async){ //splitting up "processes" for performance var possibilities = []; var totalProcesses = 0; var completeProcesses = 0; for (let i in grid){ if (grid[i] === "empty"){ totalProcesses ++; let t = totalProcesses; let delay = setTimeout(function(){ let curGrid = grid.slice(); curGrid[i] = chars[player? 1: 0]; //if (depth === 0) console.log("process " + t + " started"); findBestMove(!player, curGrid, depth + 1, false/*depth <= emptyCellCount() - 4*/, function(val){ //if (depth === 0) console.log("process " + t + " complete"); //console.log(completeProcesses); //console.log(totalProcesses); possibilities[i] = val; completeProcesses ++; if (completeProcesses === totalProcesses){ if (depth === 0) console.log(possibilities); if (depth % 2 === 0){ var bestScore = possibilities.reduce(function(prev, curr){ return Math.max(prev, curr); }, -Infinity); }else{ var bestScore = possibilities.reduce(function(prev, curr){ return Math.min(prev, curr); }, Infinity); } //if (depth === 0) console.log(bestScore); var bestMove = possibilities.indexOf(bestScore); var result = depth === 0? bestMove: bestScore; if (callback){ callback(result); } } }); }, factorials[emptyCellCount() - depth] * (t - 1) * 0.01 / (9 * (depth + 1))); } } }else{ //what normally executes: var victory = checkVictory(0, grid); //check for victory if (victory === -1 || victory === 0 || victory === 1){ //if the game is over //if (victory !== 0) console.log(victory); if (callback){ callback(-victory); } return -victory; } var possibilities = new Array(9); for (let i in grid){ if (grid[i] === "empty"){ //if the AI can go here... let curGrid = grid.slice(); //create a temporary grid for each possible move grid[i] = chars[player? 1: 0]; possibilities[i] = findBestMove(!player, grid, depth + 1); //find the quality of each move within this move, or see if someone wins after grid[i] = "empty"; } } if (depth % 2 === 0){ //if we are from the computer's perspective, find the highest score var bestScore = possibilities.reduce(function(prev, curr){ return Math.max(prev, curr); }, -Infinity); }else{ //otherwise find the lowest score var bestScore = possibilities.reduce(function(prev, curr){ return Math.min(prev, curr); }, Infinity); } if (depth === 0) console.log(bestScore); var bestMove = possibilities.indexOf(bestScore); var result = depth === 0? bestMove: bestScore; if (callback){ callback(result); } return result; } }; var equalDimensions = function(ele){ //is supposed to make an element's height equal to its width var cw = ele.clientWidth; console.log(ele.clientWidth); ele.height = cw + 'px'; return ele; }; document.addEventListener("DOMContentLoaded", function(){ var chooseWindow = document.getElementById("choose-window"); var board = document.getElementById("board"); equalDimensions(board); eachOfClass("cell", function(cell, i){ equalDimensions(cell); cell.innerHTML = " "; cell.onclick = function(){ if (turn === 0 && !gameOver && cellVals[i] === "empty"){ //if the player can go turn = 1; //make it the computer's turn chooseCell(i, 0); //do the player's move if (!gameOver){ findBestMove(1, cellVals, 0, true, function(best){ //find the best move the computer can do console.log(best); chooseCell(best, 1); //and do it turn = 0; //make it the player's turn again }); } } }; }); document.getElementById("choose-o").onclick = function(){ chars = [O, X]; newGame(O); turn = 1; findBestMove(1, cellVals, 0, false, function(best){ //console.log(best); chooseCell(best, 1); turn = 0; }); chooseWindow.classList.remove("fly-down"); chooseWindow.classList.add("fly-up"); }; document.getElementById("choose-x").onclick = function(){ chars = [X, O]; newGame(X); chooseWindow.classList.remove("fly-down"); chooseWindow.classList.add("fly-up"); //test_findBestMove(); //test_allFilled(); //cellVals = ["empty", "empty", "empty", X, X, "empty", O, O, "empty"]; //cellVals = [O, "empty", "empty", "empty", "empty", "empty", "empty", "empty", "empty"]; }; chooseWindow.classList.add("fly-down"); }); //Tests var test_findBestMove = function(){ var tests = [ [["empty", "empty", "empty", X, X, "empty", O, O, "empty"], 8], [[X, "empty", "empty", "empty", "empty", "empty", "empty", "empty", "empty"], 1], [[X, "empty", O, X, "empty", "empty", "empty", "empty", "empty"], 6], [[X, X, X, O, O, "empty", "empty", "empty", "empty"], -1, 3], [[O, O, O, X, X, "empty", "empty", "empty", "empty"], 1, 2], [[X, O, X, O, X, O, O, X, O], 0, 1] ]; for (let i in tests){ findBestMove(1, tests[i][0], tests[i][2] || 0, true, function(result){ console.log(i + ":"); console.log(result); }); /*let result = findBestMove(1, tests[i][0], tests[i][2] || 0); if (result !== tests[i][1]){ console.log(i + ":"); console.log(result); }*/ } }; var test_allFilled = function(){ var tests = [ [["empty", "empty", "empty", X, X, "empty", O, O, "empty"], false], [[X, "empty", "empty", "empty", "empty", "empty", "empty", "empty", "empty"], false], [[X, "empty", O, X, "empty", "empty", "empty", "empty", "empty"], false], [[X, X, X, O, O, "empty", "empty", "empty", "empty"], false], [[X, O, X, O, X, O, X, O, X], true] ]; for (let i in tests){ console.log(allFilled(tests[i][0]) === tests[i][1]); } }; var test_checkVictory = function(){ var tests = [ [[X, O, X, O, X, O, X, O, X], 0] ]; }

!