js-tic-tac-toe/assets/js/main.js
2023-05-04 12:28:19 -07:00

725 lines
No EOL
28 KiB
JavaScript
Executable file

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() + "! <br />You lost!";
}
else {
cardTitle.innerHTML = "Congratulations " + name + "! <br />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);