const style = (() => { // for setting multiple CSS attributes on an element Element.prototype.setAttributes = function (attributes) { Object.keys(attributes).forEach(key => this.setAttribute(key, attributes[key])); } // highlights nav button based on mouse hover const navButtons = document.querySelectorAll(".nav-link"); function makeActive() { this.setAttribute('class', this.getAttribute("class") + ' active'); } function deactivateButton() { this.setAttribute('class', "nav-link fw-bold"); } // assigns listeners to nav buttons navButtons.forEach((button) => { button.onmouseenter = makeActive; button.onmouseleave = deactivateButton; }); // resizes background if user adjusts zoom level function resizeBody () { const childHeight = document.querySelector(".cover-container").offsetHeight; if (window.innerHeight < childHeight) { document.querySelector("body").setAttribute("style", "height: " + childHeight +"px;"); } else { document.querySelector("body").setAttribute("style", "height: " + window.innerHeight +"px;"); } } // calls on page load resizeBody(); // calls when user changes window size window.onresize = resizeBody; // changes greeting based on passed in name function changeGreeting(name) { document.querySelector("#name").textContent = "Welcome " + name + "!"; } // creates a modal window to change player name and sign function namePopup(callback1, callback2) { // makes divs const darkenScreen = document.createElement('div'); const popupCard = document.createElement('div'); const cardBody = document.createElement('div'); const closeButton = document.createElement('div'); const closeButtonContainer = document.createElementNS("http://www.w3.org/2000/svg","svg"); const closeButtonSVGPath = document.createElementNS("http://www.w3.org/2000/svg","path"); const cardTitle = document.createElement('h5'); const nameInput = document.createElement('input'); const signInput = document.createElement('select'); const xs = document.createElement('option'); const os = document.createElement('option'); const okayButton = document.createElement('div'); const wrapperul = document.createElement('ul'); const wrapperli = document.createElement('li'); // sets attributes (() => { darkenScreen.setAttributes({id: 'darkenScreen', style: "height: " + document.querySelector('body').offsetHeight + "px"}); popupCard.setAttributes({class: "card", style: "width: 18rem"}); cardBody.setAttribute('class', "card-body"); closeButton.setAttribute('id', "closebutton"); closeButtonContainer.setAttributes({style: "width:24px;height:24px", "viewBox": "0 0 24 24"}); closeButtonSVGPath.setAttributes({fill: "#6c757d", d: "M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"}); cardTitle.setAttribute('class', "card-text"); cardTitle.textContent = "Your name:"; nameInput.setAttributes({type: 'text', class: "mb-3"}); signInput.setAttribute('class', "mb-3"); xs.setAttribute('value', 'x'); os.setAttribute('value', 'o'); xs.textContent = "X's"; os.textContent = "O's"; okayButton.setAttribute('class', "nav-link fw-bold active"); okayButton.textContent = "Ok"; wrapperul.setAttribute('class', "nav nav-pills"); wrapperli.setAttribute('class', "nav-item"); })(); // appends divs to DOM (() => { document.querySelector('body').appendChild(darkenScreen); darkenScreen.appendChild(popupCard); popupCard.appendChild(cardBody); cardBody.appendChild(closeButton); closeButton.appendChild(closeButtonContainer); closeButtonContainer.appendChild(closeButtonSVGPath); cardBody.appendChild(cardTitle); cardBody.appendChild(nameInput); cardBody.appendChild(document.createElement("br")); cardBody.appendChild(signInput); signInput.appendChild(xs); signInput.appendChild(os); cardBody.appendChild(wrapperul); wrapperul.appendChild(wrapperli); wrapperli.appendChild(okayButton); })(); // focuses name field nameInput.focus(); // closes window when called function closeWindow() { document.querySelector('body').removeChild(darkenScreen); } // close window if close button pushed - without updating name closeButtonContainer.onclick = closeWindow; // changes the name variable to a filtered string of the input value function inputComplete() { // regex filteredInput = nameInput.value.replace(/[^a-zA-Z]+/g, ''); // checks for non-empty string (actual value) before proceeding if (filteredInput != "") { // changes name and sign callback1(filteredInput, signInput.value); // updates AI sign if (signInput.value === 'x') { callback2('AI', 'o'); } else { callback2('AI', 'x'); } // closes window closeWindow(); } else { alert("Not a valid name!"); } } // creates event listeners on modal window (() => { // updates name when OK button clicked wrapperli.onclick = inputComplete; // updates name when Enter keystroke detected document.addEventListener("keydown", function(event) { if (event.code === 'Enter') { event.preventDefault(); inputComplete(); } }); })(); } const winnerPopup = (name, tie = false) => { // makes divs const darkenScreen = document.createElement('div'); const popupCard = document.createElement('div'); const cardBody = document.createElement('div'); const cardTitle = document.createElement('h5'); const newButton = document.createElement('div'); const wrapperul = document.createElement('ul'); const wrapperli = document.createElement('li'); // sets attributes (() => { darkenScreen.setAttributes({id: 'darkenScreen', style: "height: " + document.querySelector('body').offsetHeight + "px"}); popupCard.setAttributes({class: "card", style: "width: 18rem"}); cardBody.setAttribute('class', "card-body"); cardTitle.setAttribute('class', "card-text"); if (tie) { cardTitle.innerHTML = "It's a tie!"; } else { if (name === "AI") { cardTitle.innerHTML = "Sorry " + user.getName() + "!
You lost!"; } else { cardTitle.innerHTML = "Congratulations " + name + "!
You won!"; } } newButton.setAttribute('class', "nav-link fw-bold active"); newButton.textContent = "New"; wrapperul.setAttribute('class', "nav nav-pills"); wrapperli.setAttribute('class', "nav-item"); })(); // appends divs to DOM (() => { document.querySelector('body').appendChild(darkenScreen); darkenScreen.appendChild(popupCard); popupCard.appendChild(cardBody); cardBody.appendChild(cardTitle); cardBody.appendChild(wrapperul); wrapperul.appendChild(wrapperli); wrapperli.appendChild(newButton) })(); newButton.onclick = () => { location.reload(); }; } return { changeGreeting, namePopup, winnerPopup } })(); const gameBoard = (() => { // creates gameArray[][] containing each position let gameArray = new Array(3); for(let arrayPosition = 0; arrayPosition < gameArray.length; arrayPosition++) { gameArray[arrayPosition] = new Array(3); } // checks for wins const check = (player) => { // purely background var to allow the program to not freeze let gameConcluded = false; // get player's sign const sign = player.getSign(); //get player's name const name = player.getName(); // checks for wins horizontally for (yloc = 0; yloc < 3; yloc++) { let finder = 0; for (xloc = 0; xloc < 3; xloc ++) { if (gameArray[yloc][xloc] === sign) { finder++; } } if (finder === 3) { gameConcluded = true; style.winnerPopup(name); } } // checks for wins vertically for (xloc = 0; xloc < 3; xloc++) { let finder = 0; for (yloc = 0; yloc < 3; yloc ++) { if (gameArray[yloc][xloc] === sign) { finder++; } } if (finder === 3) { gameConcluded = true; style.winnerPopup(name); } } // checks for wins from top left to bottom right (() => { let finder = 0; for (posDiag = 0; posDiag < 3; posDiag++) { if (gameArray[posDiag][posDiag] === sign) { finder++; } } if (finder === 3) { gameConcluded = true; style.winnerPopup(name); } })(); // checks for wins from top right to bottom left (() => { let finder = 0; for (yloc = 0; yloc < 3; yloc++) { xloc = Math.abs(yloc - 2); if (gameArray[yloc][xloc] === sign) { finder++; } } if (finder === 3) { gameConcluded = true; style.winnerPopup(name); } })(); // checks for ties (() => { let counter = 0; for (yloc = 0; yloc < 3; yloc++) { for (xloc = 0; xloc < 3; xloc++) { if (gameArray[yloc][xloc] != undefined) { counter++; } } } if (counter === 9) { gameConcluded = true; style.winnerPopup(player, true); } })(); // return the var to move to prevent AI move after game completion return gameConcluded; } // sets validity boolean to check if AI move has already been made let AIMoveCompleted = false; // when a move is made const move = (player, square = undefined) => { // if called with a square passed - that is, for player move or AI final move if (square != undefined) { // get player's sign const sign = player.getSign(); // determine grid position based on square id const yloc = Number(square.getAttribute('id').charAt(0)); const xloc = Number(square.getAttribute('id').charAt(1)); // if the position is empty, append to array and DOM if (gameArray[yloc][xloc] === undefined) { // assign sign to array position gameArray[yloc][xloc] = player.getSign();; // make SVG elements const SVGBox = document.createElementNS("http://www.w3.org/2000/svg","svg"); SVGBox.setAttributes({style: "width:168px; height:168px", "viewBox": "0 0 24 24"}); const SVGPath = document.createElementNS("http://www.w3.org/2000/svg","path"); // assign correct SVG path based on player's sign if (sign == "x") { SVGPath.setAttributes({fill: "black", d: "M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"}); } else { SVGPath.setAttributes({fill: "black", d: "M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"}); } // append to DOM square.appendChild(SVGBox); SVGBox.appendChild(SVGPath); // check player win conditions let finished = check(player); // only do AI move if check determines game is NOT done // have to do this or it lags the program if (finished === false) { // call if AI move has NOT been done - otherwise infinite recursion if (AIMoveCompleted === false) { // call ai player move with undefined square move(aiplayer); // check ai player win conditions check(aiplayer); // set AIMoveCompleted boolean to false AIMoveCompleted = false; } } } } // when square is undefined (called for AI logic) else { (() => { // get human and AI players' signs let userSign = "x"; let AISign = player.getSign(); if (AISign === "x") { userSign = "o"; } // make the actual move in the game after calculations function setSquare (y,x) { const square = document.getElementById("" + y + x); AIMoveCompleted = true; move(player, square); } // at the twos stage // check for two of the same sign in a row on a grid function dubsChecker (sign) { // selects proper square and makes move, then sets validity boolean to false // HORIZONTAL (() => { let row = 3; let column = 3; horizontalSearcher: for (yloc = 0; yloc < 3; yloc++) { let finder = 0; for (xloc = 0; xloc < 3; xloc ++) { if (gameArray[yloc][xloc] === sign) { finder++; } else { column = xloc; } } // returns the offending row and breaks loop if (finder === 2) { row = yloc; break horizontalSearcher; } } // checks for non-dummy row value then moves on blank square if (row != 3) { if (gameArray[row][column] === undefined) { setSquare(row,column); } } })(); if (AIMoveCompleted) { return; } // VERTICAL (() => { let column = 3; let row = 3; verticalSearcher: for (xloc = 0; xloc < 3; xloc++) { let finder = 0; for (yloc = 0; yloc < 3; yloc ++) { if (gameArray[yloc][xloc] === sign) { finder++; } else { row = yloc; } } // returns the offending column and breaks loop if (finder === 2) { column = xloc; break verticalSearcher; } } // checks for non-dummy column value then moves on blank square if (column != 3) { if (gameArray[row][column] === undefined) { setSquare(row,column); } } })(); if (AIMoveCompleted) { return; } // L-R DIAGONAL (() => { let finder = 0; let diag = 3; for (posDiag = 0; posDiag < 3; posDiag++) { if (gameArray[posDiag][posDiag] === sign) { finder++; } else { diag = posDiag; } } if (finder === 2) { if (gameArray[diag][diag] === undefined) { setSquare(diag,diag); } } })(); if (AIMoveCompleted) { return; } // R-L DIAGONAL (() => { let finder = 0; let row = 3; let column = 3; for (yloc = 0; yloc < 3; yloc++) { xloc = Math.abs(yloc - 2); if (gameArray[yloc][xloc] === sign) { finder++; } else { row = yloc; column = xloc; } } if (finder === 2) { if (gameArray[row][column] === undefined) { setSquare(row,column); } } })(); } // first check opponent's (user's) and block to prevent a 3 dubsChecker(userSign); if (AIMoveCompleted) { return; } // then check own (ai's) and go for gold dubsChecker(AISign); if (AIMoveCompleted) { return; } // at the ones stage function singlesChecker () { let row = 3; let column = 3; // find a single one that looks like the ai's sign (() => { for (yloc = 0; yloc < 3; yloc++) { for (xloc = 0; xloc < 3; xloc++) { if (gameArray[yloc][xloc] === AISign) { row = yloc; column = xloc; return; } } } })(); // create an array of possible move options let coordArray = []; // find all empty squares and add them to the array // gross way - if statements // checks for non-dummy value if one exists // NOTE: one if statement (checking for row alone) faster than two (checking for row and column) if (row > 3) { // creates a -1/0/+1 int for neighbor calculations for (i = -1; i <= 1; i++) { // checks +1/0/-1 place of column value if (0 <= column + i <= 2) { // checks for open square if (gameArray[row][column + i] === undefined) { tempVar = column + i; // pushes to array coordArray.push({ row: row, column: tempVar }); } } // do the same thing for the rows at the column value if (0 <= row + i <= 2) { if (gameArray[row + i][column] === undefined) { tempVar = row + i; coordArray.push({ row: tempVar, column: column }); } } // do that but horizontally if (0 <= row + i <= 2 && 0 <= column + i <= 2) { if (gameArray[row + i][column + i] === undefined) { yloc = row + i; xloc = column + i; coordArray.push({ row: yloc, column: xloc }); } if (gameArray[row - i][column + i] === undefined) { yloc = row - i; xloc = column + i; coordArray.push({ row: yloc, column: xloc }); } } } } // if the array actually has some coordinates after the last check if (coordArray.length < 0) { // get random choice // floor removes decimals // random gets number between 0 and 1 // multiplied by the array length let rand = Math.floor(Math.random() * coordArray.length); // make move at random based on values in coordinate array setSquare(coordArray[rand].row, coordArray[rand].column); } } // check if ai already has a square down, make adjacent move singlesChecker(); if (AIMoveCompleted) { return; } // at this point, the ai has no squares on the board function middleChecker() { if (gameArray[1][1] === undefined) { setSquare(1,1); } } // pick the middle square if available middleChecker(); if (AIMoveCompleted) { return; } // if middle is not available, pick square at random function randomSquare() { // random value from 0 - 3 for x value const x = Math.floor(Math.random() * 3); // different random value from 0 - 3 for y value const y = Math.floor(Math.random() * 3); // check if square at values is available, move if so if (gameArray[y] === undefined) { console.log(y); setSquare(y, x); } else if (gameArray[y][x] === undefined) { setSquare(y, x); } } if (AIMoveCompleted) { return; } // keep doing it until an open square is found while (AIMoveCompleted === false) { randomSquare(); } })(); } } // refreshes the page const newGrid = () => { location.reload(); } // initializes grid const initGrid = () => { // makes square based on location coordinates and puts those location coordinates into the id of the square function makeSquare (yloc, xloc) { let gridElement = document.createElement('div'); gridElement.setAttribute('class', 'griditem'); gridElement.onclick = function() { move(user, this); }; document.querySelector("#gridspace").appendChild(gridElement); gridElement.setAttribute('id', "" + yloc + "" + xloc + ""); } // create array of y positions let varArray = []; // for each y location for (yloc = 0; yloc < 3; yloc++) { // make each array entry a function that loops thrice and calls makeSquare[][] each time varArray[yloc] = () => { for (xloc=0;xloc<3;xloc++) { makeSquare(yloc,xloc); } } // calls above function varArray[yloc](); } } return { newGrid, initGrid } })(); const player = (name, sign) => { // assigns player name to passed in name let playerName = name; // assigns player sign to passed in sign let playerSign = sign; // makes greeting with player name style.changeGreeting(playerName); function updateInfo (newName, newSign) { playerName = newName; playerSign = newSign; style.changeGreeting(playerName); } // returns player name function getName() { return playerName; } // returns player sign function getSign() { return playerSign; } return { updateInfo, getName, getSign } }; // creates a user player with default values const user = player("John Doe", 'x'); // creates an AI player with default values const aiplayer = player("AI", 'o'); // initializes grid spaces gameBoard.initGrid(); // reloads page when new button pressed document.querySelector("#newButton").onclick = gameBoard.newGrid; // changes player info when name button pressed document.querySelector("#nameButton").onclick = () => { style.namePopup(user.updateInfo, aiplayer.updateInfo); } // immediately queries the user for their information to replace default values style.namePopup(user.updateInfo, aiplayer.updateInfo);