This commit is contained in:
ak 2023-05-04 12:28:19 -07:00
commit 353c78607d
4 changed files with 859 additions and 0 deletions

3
README.md Executable file
View file

@ -0,0 +1,3 @@
# js-tic-tac-toe
Tic-Tac-Toe game made in ES6 JS, with fully functional AI player to play against

95
assets/css/style.css Executable file
View file

@ -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%;
}

725
assets/js/main.js Executable file
View file

@ -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() + "! <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);

36
index.html Executable file
View file

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="Description" content="Enter your description here"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.1.0/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
<link rel="stylesheet" href="assets/css/style.css">
<title>Tic-Tac-Toe</title>
</head>
<body class="d-flex flex-column text-center text-white bg-dark">
<div class="cover-container d-flex p-3 mx-auto flex-column">
<header class="mb-3">
<div>
<h3 class="float-md-start mb-0">Tic-Tac-Toe</h3>
<nav class="nav nav-masthead justify-content-center float-md-end">
<ul class="nav nav-pills">
<li class="nav-item"><a href="#" class="nav-link fw-bold" id="newButton">New</a></li>
<li class="nav-item"><a href="#" class="nav-link fw-bold" id="nameButton">Change Name</a></li>
</ul>
</nav>
</div>
</header>
<div id="name" class="mb-3 fw-bold"></div>
<div class="bg-light border rounded-3 p-5 d-flex" id="gridspace-background">
<div class="text-black" id="gridspace">
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.1.0/js/bootstrap.min.js"></script>
<script src="assets/js/main.js"></script>
</body>
</html>