diff --git a/globals.js b/globals.js new file mode 100644 index 0000000..3c8b474 --- /dev/null +++ b/globals.js @@ -0,0 +1,30 @@ +// ----- GLOBAL VARIABLES ----------------------- +const boardSize = 4; +const board = []; + +let deck; +let firstCard = null; +let firstCardElement; + +// For gameplay +let canClick = false; + +// For stopwatch +let milliseconds = 0; +const delayInMilliseconds = 100; // 0.1 second +const maxMilliseconds = 180000; // 3 minutes (1 min = 60 000ms) +let stopwatchStarted = false; +let stopwatchRef; + +const stopwatch = document.createElement('div'); +const startBtn = document.createElement('button'); +const stopBtn = document.createElement('button'); +const resetBtn = document.createElement('button'); + +// For game information +const stopwatchContainer = document.createElement('div'); +const gameRulesDiv = document.createElement('div'); +const gameInfoContainer = document.createElement('div'); +const gameInfo = document.createElement('div'); +let timeoutMsgMatch; +let timeoutMsgNoMatch; diff --git a/index.html b/index.html index 4771b50..40acce4 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,16 @@ - - Timer - - + + + + Liz's Card Match Game (Stopwatch version) + - -

Timer!

- - - + +

Liz's Card Match Game

+

(Stopwatch version)

+ + + + diff --git a/script.js b/script.js index e2d0297..bd8843c 100644 --- a/script.js +++ b/script.js @@ -1 +1,418 @@ -// Please implement exercise logic here +// ----- HELPER FUNCTIONS ----------------------- +// Get a random index ranging from 0 (inclusive) to max (exclusive). +const getRandomIndex = (max) => Math.floor(Math.random() * max); + +// Create deck +const makeDeck = () => { + const newDeck = []; + const suits = ['hearts', 'diamonds', 'clubs', 'spades']; + const suitSymbols = ['♥️', '♦️', '♣️', '♠️']; + const cardName = [ + 'A', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10', + 'J', + 'Q', + 'K', + ]; + const cardRank = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]; + + // Loop over the suits array + for (let suitIndex = 0; suitIndex < suits.length; suitIndex += 1) { + // Store the current suit in a variable + const currentSuit = suits[suitIndex]; + + for (let i = 0; i < 13; i += 1) { + // Set suit color + let suitColor = 'black'; + if (currentSuit === 'hearts' || currentSuit === 'diamonds') { + suitColor = 'red'; + } + + // Create a new card with the current name, suit, and rank + const card = { + name: cardName[i], + suit: currentSuit, + symbol: suitSymbols[suitIndex], + color: suitColor, + rank: cardRank[i], + }; + + // Add the new card to the deck + newDeck.push(card); + newDeck.push(card); + } + } + + // Return the completed card deck + return newDeck; +}; + +// Shuffle cards +const shuffleCards = (cards) => { + // Loop over the card deck array once + for (let currentIndex = 0; currentIndex < cards.length; currentIndex += 1) { + // Select a random index in the deck + const randomIndex = getRandomIndex(cards.length); + // Select the card that corresponds to randomIndex + const randomCard = cards[randomIndex]; + // Select the card that corresponds to currentIndex + const currentCard = cards[currentIndex]; + // Swap positions of randomCard and currentCard in the deck + cards[currentIndex] = randomCard; + cards[randomIndex] = currentCard; + } + // Return the shuffled deck + return cards; +}; + +// Format open cards +const formatOpenCard = (cardDiv, card) => { + cardDiv.innerText = `${card.name}${card.symbol}`; + if (card.symbol === '♥️' || card.symbol === '♦️') { + cardDiv.classList.add('red'); + } + cardDiv.classList.add('open-card'); +}; + +// Close all cards that are open +const closeAllOpenCards = () => { + let openCardsList = document.querySelectorAll('div.open-card'); + for (let i = 0; i < openCardsList.length; i += 1) { + openCardsList[i].classList.remove('open-card', 'red', 'black'); + openCardsList[i].innerText = ``; + } +}; + +// Checks if all cards are open (game complete) +// Returns true || false +const areAllCardsOpen = () => { + const numOfOpenCards = document.querySelectorAll('.open-card'); + if (numOfOpenCards.length === 16) { + return true; + } + return false; +}; + +// Update game info +const updateGameInfo = (msgText) => { + gameInfo.innerHTML = msgText; + gameInfoContainer.appendChild(gameInfo); +}; + +// Format stopwatch +const formatStopwatch = (ms) => { + // Show min:sec + // calculate minutes + let min = Math.floor((ms / 1000 / 60) % 60); + // calculate seconds + let sec = Math.floor((ms / 1000) % 60); + + // add leading 0 + if (min < 10) { + min = '0' + min; + } + if (sec < 10) { + sec = '0' + sec; + } + return `${min}:${sec}`; +}; + +// Stopwatch functions: start the stopwatch +const startStopwatch = () => { + canClick = true; + startBtn.disabled = true; + stopBtn.disabled = false; + + stopwatchRef = setInterval(() => { + if (milliseconds >= maxMilliseconds) { + // Clear all intervals and timeouts + clearInterval(stopwatchRef); + clearTimeout(timeoutMsgMatch); + clearTimeout(timeoutMsgNoMatch); + + // Find all open cards and remove the open-card class + closeAllOpenCards(); + + // Discontinue gameplay, only allow to Reset + canClick = false; + startBtn.disabled = true; + stopBtn.disabled = true; + resetBtn.disabled = false; + + updateGameInfo(`Time's up! You lose.
Hit reset to try again.`); + } + + stopwatch.innerHTML = formatStopwatch(milliseconds); + milliseconds += delayInMilliseconds; + }, delayInMilliseconds); +}; + +// Stopwatch fuctions: stop the stopwatch +const stopStopwatch = () => { + clearInterval(stopwatchRef); + + canClick = false; + startBtn.disabled = false; + stopBtn.disabled = true; + + if (areAllCardsOpen() === true) { + updateGameInfo( + `Congrats, you matched all the cards!
Click reset to play again.` + ); + startBtn.disabled = true; + } +}; + +// Stopwatch functions: reset the stopwatch +const resetStopwatch = () => { + clearInterval(stopwatchRef); + milliseconds = 0; + stopwatch.innerHTML = formatStopwatch(milliseconds); + startBtn.disabled = false; + stopBtn.disabled = true; + canClick = false; + + // Reset game + resetGame(); +}; + +// Reset game +const resetGame = () => { + board.length = 0; + firstCard = null; + const bodyDivs = document.querySelectorAll('body > div'); + for (let i = 0; i < bodyDivs.length; i += 1) { + document.body.removeChild(bodyDivs[i]); + } + initGame(); +}; + +// ----- GAMEPLAY LOGIC ------------------------- + +// What happens when user clicks on a square +const openCard = (cardElement, row, column) => { + // Store the clicked card + const clickedCard = board[row][column]; + + // If this card is already open (user has already clicked this square) + // Or setTimeout is running + if (cardElement.innerText !== '' || canClick === false) { + return; + } + + // First turn + if (firstCard === null) { + // Set the firstCard to the card that was clicked + firstCard = clickedCard; + + // "Turn the card over" by showing the card name in the square + formatOpenCard(cardElement, clickedCard); + + // Hold on to this first in case second card doesn't match + firstCardElement = cardElement; + + // Update game info + updateGameInfo(`Great, now find its match!`); + } + + // Second turn + else { + canClick = false; + + // If it's a match + if ( + clickedCard.name === firstCard.name && + clickedCard.suit === firstCard.suit + ) { + // "Turn the card over" by showing the card name in the square + + formatOpenCard(cardElement, clickedCard); + + // Check if all cards are open + if (areAllCardsOpen() === true) { + stopStopwatch(); + return; + } + + // If not all cards have been open, update game info + else { + updateGameInfo(`Noice, it's a match!
Pick your next card.`); + if (milliseconds <= 2100) { + timeoutMsgMatch = setTimeout(() => { + updateGameInfo(`Click a card to continue.`); + }, 1500); + } + + canClick = true; + } + } + + // If it's not a match + else { + // "Open cards" by showing the card name in the square and adding the relevant classes + + formatOpenCard(cardElement, clickedCard); + + stopBtn.disabled = true; + resetBtn.disabled = true; + // "Turn cards over" after a set time + timeoutMsgNoMatch = setTimeout(() => { + stopBtn.disabled = false; + resetBtn.disabled = false; + + // "Turn cards over" by removing card name in square + cardElement.innerText = ``; + firstCardElement.innerText = ``; + + cardElement.classList.remove('open-card', 'red', 'black'); + firstCardElement.classList.remove('open-card', 'red', 'black'); + + canClick = true; + }, 1500); + + // Update game info + updateGameInfo(`Sorry, those didn't match.
Try again!`); + } + + // Reset the cards + firstCard = null; + } +}; + +// ----- GAME INITIALISATION -------------------- + +// Create container for stopwatch +const createStopwatchContainer = () => { + // Format game instructions + stopwatchContainer.innerHTML = ``; + + let gameRules = [ + `Time how long it takes for you to match all the cards.`, + `You can only flip cards over when the stopwatch is running.`, + ` You have a maximum of 3 minutes to play.`, + ]; + + let header = document.createElement('h3'); + header.innerText = `How to play`; + + let ul = document.createElement('ul'); + for (let i = 0; i < gameRules.length; i += 1) { + let li = document.createElement('li'); + li.innerText = gameRules[i]; + ul.appendChild(li); + } + + stopwatchContainer.appendChild(header); + stopwatchContainer.appendChild(ul); + + // Format the container + stopwatchContainer.classList.add('stopwatch-container'); + document.body.appendChild(stopwatchContainer); + + // Format the stopwatch + stopwatch.classList.add('stopwatch'); + stopwatch.innerHTML = formatStopwatch(milliseconds); + stopwatchContainer.appendChild(stopwatch); + + // Format the buttons + startBtn.innerText = 'Start'; + stopBtn.innerText = 'Stop'; + resetBtn.innerText = 'Reset'; + stopwatchContainer.appendChild(startBtn); + stopwatchContainer.appendChild(stopBtn); + stopwatchContainer.appendChild(resetBtn); + + stopBtn.disabled = true; + + // Add event listeners to buttons + startBtn.addEventListener('click', startStopwatch); + stopBtn.addEventListener('click', stopStopwatch); + resetBtn.addEventListener('click', resetStopwatch); +}; + +// Create container for game info +const createGameInfoContainer = () => { + gameInfoContainer.classList.add('game-info-container'); + gameInfo.classList.add('game-info'); + + gameInfo.innerHTML = `Click on the squares to match cards.`; + + gameInfoContainer.appendChild(gameInfo); + document.body.appendChild(gameInfoContainer); +}; + +// Create container for board elements +const createBoardContainer = (board) => { + // Create main container + const boardContainer = document.createElement('div'); + boardContainer.classList.add('board-container'); + + // Create the board grid with 2 loops ------ + // First for row and second for column + for (let i = 0; i < board.length; i += 1) { + // Create variable to hold cards in this row + const row = board[i]; + + // Create div for the row + const rowDiv = document.createElement('div'); + rowDiv.classList.add('row'); + + // Start second loop -------- + // to create the columns (cards / squares) in the row + for (let j = 0; j < row.length; j += 1) { + // Create the square (card) + const square = document.createElement('div'); + square.classList.add('square'); + + // Add event listener to the square + square.addEventListener('click', (e) => { + openCard(e.currentTarget, i, j); + }); + + // Append the square to the row + rowDiv.appendChild(square); + } + + // Append row to the board + boardContainer.appendChild(rowDiv); + } + document.body.appendChild(boardContainer); +}; + +// Game initialisation +const initGame = () => { + // Prepare the deck ---------- + // Create a deck with twice the number of cards + let doubleDeck = makeDeck(); + + // Select enough to make a smaller deck + let deckSubset = doubleDeck.slice(0, boardSize * boardSize); + + // Shuffle the cards + deck = shuffleCards(deckSubset); + + // Deal cards to the board data structure (nested array) ----- + for (let i = 0; i < boardSize; i += 1) { + // Create the array for each row + board.push([]); + + // Deal the cards per row + for (let j = 0; j < boardSize; j += 1) { + board[i].push(deck.pop()); + } + } + + createStopwatchContainer(); + createGameInfoContainer(); + createBoardContainer(board); +}; + +initGame(); diff --git a/styles.css b/styles.css index 04e7110..3925580 100644 --- a/styles.css +++ b/styles.css @@ -1,3 +1,107 @@ body { - background-color: pink; + background-color: lavenderblush; + font-family: monospace; + text-align: center; +} + +h1 { + color: darkmagenta; +} + +h2 { + color: darkmagenta; +} + +h3 { + text-align: left; + margin: 0.5em; +} + +li { + text-align: left; + margin-bottom: 0.5em; +} + +.stopwatch-container { + color: darkmagenta; + font-size: 1.2em; + background-color: plum; + border: 2px solid mediumorchid; + width: 50vw; + padding: 1em; + margin: 2em auto; +} + +.stopwatch-container button { + font-family: monospace; + font-size: 1rem; + letter-spacing: 2px; + font-weight: bold; + text-transform: uppercase; + padding: 0.5rem; + margin: 0.5rem; + color: white; + background-color: mediumorchid; + border-radius: 5em; + border-width: 2px; + border-style: solid; + border-color: darkorchid; +} + +.stopwatch-container button:disabled { + color: thistle; + background-color: lavender; + border-color: thistle; +} + +.stopwatch { + font-size: 1.5em; + background-color: lavenderblush; + width: 10em; + margin: auto; + padding: 0.5em; + border-radius: 5em; + border: 2px solid mediumorchid; + box-sizing: border-box; +} + +.game-info-container { + color: darkmagenta; + font-size: 1.2em; + background-color: plum; + border: 2px solid mediumorchid; + height: 2.5em; + width: 50vw; + padding: 1em; + margin: 2em auto; +} + +.board-container { + background-color: plum; + border: 2px solid mediumorchid; + width: 50vw; + padding: 1em; + margin: 2em auto; +} + +.square { + padding: 10px; + margin: 10px; + background-color: darkmagenta; + display: inline-block; + height: 15px; + width: 15px; + vertical-align: top; + text-align: center; +} + +.open-card { + background-color: lavenderblush; +} + +.red { + color: crimson; +} +.black { + color: darkslategrey; }