/* 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 localStorage.removeItem("stats"); 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 localStorage.removeItem("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 (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`; 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); }