/*
Declaration of global variables
*/
//Product info variables
let productName;
let productPrice;
let productImage;
//Timeout IDs
let shakeTimeout;
let toastTimeout;
let warningTimeout;
/*
Global variable constants
*/
//The day Costcodle was launched. Used to find game number each day
const costcodleStartDate = new Date("09/21/2023");
const gameNumber = getGameNumber();
//Elements with event listeners to play the game
const input = document.getElementById("guess-input");
const buttonInput = document.getElementById("guess-button");
const infoButton = document.getElementById("info-button");
infoButton.addEventListener("click", switchState);
const statButton = document.getElementById("stat-button");
statButton.addEventListener("click", switchState);
const shareButton = document.getElementById("share-button");
shareButton.addEventListener("click", copyStats);
//User stats object
const userStats = JSON.parse(localStorage.getItem("stats")) || {
numGames: 0,
numWins: 0,
winsInNum: [0, 0, 0, 0, 0, 0],
currentStreak: 0,
maxStreak: 0,
};
//User game state
const gameState = JSON.parse(localStorage.getItem("state")) || {
gameNumber: -1,
guesses: [],
hasWon: false,
};
/*
Starts playing the game. Called at beginning of execution
*/
playGame();
function playGame() {
fetchGameData(getGameNumber());
}
/*
Acquiring Game Data
*/
//Fetches the current day's game data from the json and starts game
function fetchGameData(gameNumber) {
fetch("./games.json")
.then((response) => response.json())
.then((json) => {
productName = json[`game-${gameNumber}`].name;
productPrice = json[`game-${gameNumber}`].price;
productPrice = Number(productPrice.slice(1, productPrice.length));
productImage = json[`game-${gameNumber}`].image;
initializeGame();
});
}
/*
Used to initialize the game board using the current game state
*/
function initializeGame() {
//Reset game state and track new game if user last played on a previous day
if (gameState.gameNumber !== gameNumber) {
gameState.gameNumber = gameNumber;
gameState.guesses = [];
gameState.hasWon = false;
userStats.numGames++;
localStorage.setItem("stats", JSON.stringify(userStats));
localStorage.setItem("state", JSON.stringify(gameState));
}
displayProductCard();
updateGameBoard();
if (gameState.guesses.length < 6 && !gameState.hasWon) {
addEventListeners();
} else {
buttonInput.setAttribute("disabled", "");
buttonInput.classList.remove("active");
input.setAttribute("disabled", "");
input.setAttribute("placeholder", "Game Over!");
}
}
function displayProductCard() {
//First, update the image container with the new product image
const imageContainer = document.getElementById("image-container");
//Create a new image element to dynamically store game image
const productImageElement = document.createElement("img");
productImageElement.src = productImage;
productImageElement.setAttribute("id", "product-image");
//Add created image to the image container
imageContainer.appendChild(productImageElement);
//Select product info element and update the html to display product name
const productInfo = document.getElementById("product-info");
productInfo.innerHTML = `
${productName}`;
}
function updateGameBoard() {
updateGuessStat();
gameState.guesses.forEach((guess, index) => displayGuess(guess, index + 1));
}
function updateGuessStat() {
const guessStats = document.getElementById("game-stats");
if (gameState.hasWon) {
guessStats.innerHTML = `You win! Congratulations!🎉`;
guessStats.innerHTML += `The price was $${productPrice}`;
return;
}
if (gameState.guesses.length === 6) {
guessStats.innerHTML = `Better luck next time!`;
guessStats.innerHTML += `The price was $${productPrice}`;
} else {
guessStats.innerHTML = `Guess: ${gameState.guesses.length + 1}/6`;
}
}
/*
Event Listeners
*/
//Text input event listener to submit guess when user presses "Enter"
function inputEventListener(event) {
if (event.key === "Enter") {
handleInput();
}
}
//Button event listener to submit guess when user presses guess button
function buttonEventListener() {
handleInput();
}
function handleInput() {
const strippedString = input.value.replaceAll(",", "");
const guess = Number(strippedString).toFixed(2);
console.log(guess);
if (isNaN(guess) || !strippedString) {
displayWarning();
return;
}
checkGuess(guess);
input.value = "";
function displayWarning() {
clearTimeout(warningTimeout);
const warningElem = document.getElementById("warning-toast");
warningElem.classList.remove("hide");
warningElem.classList.add("animate__flipInX");
warningTimeout = setTimeout(() => {
warningElem.classList.remove("animate__flipInX");
warningElem.classList.add("animate__flipOutX");
setTimeout(() => {
warningElem.classList.remove("animate__flipOutX");
warningElem.classList.add("hide");
}, 1000);
}, 2000);
}
}
function copyStats() {
let output = `Costcodle #${gameNumber}`;
if (!gameState.hasWon) {
output += ` X/6\n`;
} else {
output += ` ${gameState.guesses.length}/6\n`;
}
gameState.guesses.forEach((guess) => {
switch (guess.direction) {
case "↑":
output += `⬆️`;
break;
case "↓":
output += `⬇️`;
break;
case "✓":
output += `✅`;
break;
}
switch (guess.closeness) {
case "guess-far":
output += `🟥`;
break;
case "guess-near":
output += `🟨`;
break;
}
output += `\n`;
});
output += `https://costcodle.com`;
navigator.clipboard.writeText(output);
displayToast();
function displayToast() {
clearTimeout(toastTimeout);
const toastElem = document.getElementById("stats-toast");
toastElem.classList.remove("hide");
toastElem.classList.add("animate__flipInX");
toastTimeout = setTimeout(() => {
toastElem.classList.remove("animate__flipInX");
toastElem.classList.add("animate__flipOutX");
setTimeout(() => {
toastElem.classList.remove("animate__flipOutX");
toastElem.classList.add("hide");
}, 1000);
}, 3000);
}
}
function addEventListeners() {
input.addEventListener("keydown", inputEventListener);
buttonInput.addEventListener("click", buttonEventListener);
input.addEventListener("focus", () => {
input.setAttribute("placeholder", "0.00");
});
input.addEventListener("blur", () => {
input.setAttribute("placeholder", "Enter a guess...");
});
}
function removeEventListeners() {
buttonInput.setAttribute("disabled", "");
buttonInput.classList.remove("active");
input.setAttribute("disabled", "");
input.setAttribute("placeholder", "Game Over!");
input.removeEventListener("keydown", inputEventListener);
buttonInput.removeEventListener("click", buttonEventListener);
}
/*
Handles the logic of Costocodle
Creates a guess object based on user guess and checks win condition
*/
function checkGuess(guess) {
const guessObj = { guess, closeness: "", direction: "" };
const percentAway = calculatePercent(guess);
if (Math.abs(percentAway) <= 5) {
guessObj.closeness = "guess-win";
gameState.hasWon = true;
} else {
shakeBox();
if (Math.abs(percentAway) <= 25) {
guessObj.closeness = "guess-near";
} else {
guessObj.closeness = "guess-far";
}
}
if (gameState.hasWon) {
guessObj.direction = "✓";
} else if (percentAway < 0) {
guessObj.direction = "↑";
} else {
guessObj.direction = "↓";
}
gameState.guesses.push(guessObj);
localStorage.setItem("state", JSON.stringify(gameState));
displayGuess(guessObj);
if (gameState.hasWon) {
gameWon();
} else if (gameState.guesses.length === 6) {
gameLost();
}
}
/*
Displays guess object from either game state or a new guess
*/
function displayGuess(guess, index = gameState.guesses.length) {
const guessContainer = document.getElementById(index);
const guessValueContainer = document.createElement("div");
const infoContainer = document.createElement("div");
guessValueContainer.classList.add(
"guess-value-container",
"animate__flipInX"
);
infoContainer.classList.add("guess-direction-container", "animate__flipInX");
guessValueContainer.innerHTML = `$${guess.guess}`;
infoContainer.classList.add(guess.closeness);
infoContainer.innerHTML = guess.direction;
guessContainer.classList.add("animate__flipOutX");
setTimeout(() => {
guessContainer.classList.add("transparent-background");
guessContainer.appendChild(guessValueContainer);
guessContainer.appendChild(infoContainer);
}, 500);
updateGuessStat();
}
/*
Helper function to compute guess accuracy
*/
function calculatePercent(guess) {
return ((guess * 100) / (productPrice * 100)) * 100 - 100;
}
/*
End state function to handle win/loss conditions
*/
function gameWon() {
userStats.numWins++;
userStats.currentStreak++;
userStats.winsInNum[gameState.guesses.length - 1]++;
if (userStats.currentStreak > userStats.maxStreak) {
userStats.maxStreak = userStats.currentStreak;
}
gameState.hasWon = true;
localStorage.setItem("state", JSON.stringify(gameState));
localStorage.setItem("stats", JSON.stringify(userStats));
removeEventListeners();
}
function gameLost() {
userStats.currentStreak = 0;
localStorage.setItem("stats", JSON.stringify(userStats));
removeEventListeners();
}
/*
DOM manipulation functions for overlays and animations
*/
function switchState(event) {
const overlayBtnClicked = event.currentTarget.dataset.overlay;
const overlayElem = document.getElementById(overlayBtnClicked);
const title = document.getElementById("title");
if (title.classList.contains("info-title")) {
title.classList.remove("info-title");
}
if (overlayElem.style.display === "flex") {
title.innerHTML = `COSTCODLE`;
overlayElem.style.display = "none";
return;
}
if (overlayBtnClicked === "info-overlay") {
document.getElementById("stats-overlay").style.display = "none";
renderInfo();
} else {
document.getElementById("info-overlay").style.display = "none";
renderStats();
}
function renderInfo() {
title.innerHTML = `HOW TO PLAY`;
if (!title.classList.contains("info-title")) {
title.classList.add("info-title");
}
overlayElem.style.display = "flex";
}
function renderStats() {
title.innerHTML = `GAME STATS`;
renderStatistics();
graphDistribution();
overlayElem.style.display = "flex";
function renderStatistics() {
const numWinsElem = document.getElementById("number-wins");
numWinsElem.innerHTML = `${userStats.numGames}`;
const winPercentElem = document.getElementById("win-percent");
if (userStats.numGames === 0) {
winPercentElem.innerHTML = `0`;
} else {
winPercentElem.innerHTML = `${Math.round(
(userStats.numWins / userStats.numGames) * 100
)}`;
}
const currentStreakElem = document.getElementById("current-streak");
currentStreakElem.innerHTML = `${userStats.currentStreak}`;
const maxStreakElem = document.getElementById("max-streak");
maxStreakElem.innerHTML = `${userStats.maxStreak}`;
}
function graphDistribution() {
console.log("here");
userStats.winsInNum.forEach((value, index) => {
const graphElem = document.getElementById(`graph-${index + 1}`);
if (userStats.numWins === 0) {
graphElem.style = `width: 5%`;
} else {
graphElem.style = `width: ${
Math.floor((value / userStats.numWins) * 0.95 * 100) + 5
}%`;
}
graphElem.innerHTML = `${value}`;
});
}
}
}
function shakeBox() {
clearTimeout(shakeTimeout);
const infoCard = document.getElementById("info-card");
if (infoCard.classList.contains("animate__headShake")) {
infoCard.classList.remove("animate__headShake");
}
shakeTimeout = setTimeout(
() => infoCard.classList.add("animate__headShake"),
100
);
}
/*
Finds current game number based off of Costcodle start date
*/
function getGameNumber() {
const currDate = new Date();
let timeDifference = currDate.getTime() - costcodleStartDate.getTime();
let dayDifference = timeDifference / (1000 * 3600 * 24);
return Math.ceil(dayDifference);
}