commit 353c78607d40b904d81786ccbe290ce43749ae58 Author: ak Date: Thu May 4 12:28:19 2023 -0700 final diff --git a/README.md b/README.md new file mode 100755 index 0000000..68d27f4 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# js-tic-tac-toe + +Tic-Tac-Toe game made in ES6 JS, with fully functional AI player to play against diff --git a/assets/css/style.css b/assets/css/style.css new file mode 100755 index 0000000..603583b --- /dev/null +++ b/assets/css/style.css @@ -0,0 +1,95 @@ +body { + text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5); + box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5); +} + +.cover-container { + max-width: 42em; +} + +.col-md-6 { + width: 100% !important; +} + +#gridspace-background { + justify-content: center; +} + +#gridspace { + height: 510px; + width: 510px; + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(3, 1fr); + text-shadow: none; +} + +.griditem { + border: 1px solid black; +} + +.nav-masthead .nav-link { + color: rgba(255, 255, 255, .5); +} + +.nav-masthead .nav-link:hover, +.nav-masthead .nav-link:focus { + border-bottom-color: rgba(255, 255, 255, .25); +} + +.nav-masthead .nav-link + .nav-link { + margin-left: 1rem; +} + +#darkenScreen { + background-color: #000c; + width: 100%; + position: absolute; + display: flex; + align-items: center; + justify-content: center; + color: black; + text-shadow: none; +} + +#closebutton { + display: flex; + justify-content: end; +} + +#closebutton > :first-child { + cursor: pointer; +} + +.card-body > .nav > .nav-item { + cursor: pointer; +} + +.card-body > .nav { + display: flex; + justify-content: center; +} + +input { + border-width: 1px; + border-style: solid; + border-color: rgba(0, 0, 0, .5); +} + +input:focus { + outline: none; +} + +select { + background-color: white; + border-width: 1px; + border-style: solid; + border-color: rgba(0, 0, 0, .5); + height: 24px; +} + +#divider { + height: 2px; + background-color: rgba(0, 0, 0, .25); + border-radius: 40%; +} \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js new file mode 100755 index 0000000..b98289a --- /dev/null +++ b/assets/js/main.js @@ -0,0 +1,725 @@ +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); \ No newline at end of file diff --git a/index.html b/index.html new file mode 100755 index 0000000..53d05be --- /dev/null +++ b/index.html @@ -0,0 +1,36 @@ + + + + + + + + + + +Tic-Tac-Toe + + +
+
+
+

Tic-Tac-Toe

+ +
+
+
+
+
+
+
+
+ + + + + \ No newline at end of file