diff --git a/.vscode/settings.json b/.vscode/settings.json index 202a1c1..40bef48 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,6 @@ "editor.wordWrap": "on", "eslint.format.enable": true, "eslint.lintTask.enable": true, - "eslint.migration.2_x": "off" + "eslint.migration.2_x": "off", + "editor.fontFamily": "Segoe UI Emoji" } diff --git a/index.html b/index.html index 25a06fd..f4819e4 100644 --- a/index.html +++ b/index.html @@ -2,12 +2,67 @@ SWE101 + + + -

SWE101! 🚀

- +

🎴🃏 Match Game 🃏🎴

+ + +
+
+
+
+
+
+
+ +
+
+
+ +
+
Find the Pairs.
+
+ + +
+
+
diff --git a/script.js b/script.js index e2d0297..ffa4988 100644 --- a/script.js +++ b/script.js @@ -1 +1,303 @@ // Please implement exercise logic here +// ### DOM SELECTORS ### +// from modal +const modal = document.querySelector(".modal") +const modalContainer = document.querySelector(".modal-container") +const content = document.querySelector(".content") +const formPlayerName = document.querySelector("#form-player-name") +const formBoardSize = document.querySelector("#form-board-size") +const formTimeSelection = document.querySelector( + "#form-time-selection" +) +const formSubmitBtn = document.querySelector(".form-submit") + +// from main container +const container = document.getElementById("main-container") +const playerMessage = document.getElementById("game-title") +const playerWinMessage = document.getElementById("player-wins") +const playerWinRate = document.getElementById("player-win-rate") +const playerTimer = document.getElementById("player-timer") +const boardElement = document.getElementsByClassName("board-container")[0] +const outputMessage = document.getElementById("output-message") +const resetBoard = document.getElementById("reset") +const resetGame = document.getElementById("total-reset") + +// ### GLOBAL VARIABLES ### +let playerName +let boardSize +let timer +let firstCard = null +let firstCardElement = null +let deck =[] +let board = [] +let canClick = true +let matchedPairs = 0 +let totalPairs = 0 +let winCounter = 0 +let numOfGames = -1 + +// ### CARD FUNCTIONS ### +// Get a random index ranging from 0 (inclusive) to max (exclusive). +const getRandomIndex = (max) => Math.floor(Math.random() * max) + +const generateCard = (cardRank, suit) => { + const symbols = ['♥', '♦', '♣', '♠'] + let cardName = '' + switch (cardRank) { + case 1: + cardName = 'A' + break + case 11: + cardName = 'J' + break + case 12: + cardName = 'Q' + break + case 13: + cardName = 'K' + break + default: + cardName = cardRank; + } + + const card = { + suit: symbols[suit], + name: cardName, + colour: suit < 2 ? 'red' : 'black', + rank: cardRank, + }; + return card; +} + +// generate the deck for gameplay +const makeDeck = () => { + const tempDeck = []; + for (let i = 1; i <= 13; i += 1){ + for (let j = 0; j < 4; j += 1){ + // generates cards and inserts randomly to get a 52-card deck + tempDeck.splice(getRandomIndex(tempDeck.length + 1), 0, generateCard(i, j)); + } + } + + // get random cards from the tempdeck and insert 2 of it randomly into deck + for (let k = 0; k < totalPairs; k += 1) { + const randCard = tempDeck.pop(); + deck.splice(getRandomIndex(deck.length + 1), 0, randCard); + deck.splice(getRandomIndex(deck.length + 1), 0, randCard); + } +} + +// ### HELPER FUNCTIONS ### +//win rate calculator +const winRate = (numberOfWins,numberOfGames) => { + if (numberOfGames == 0) { + return 0 + } + else { + return ((numberOfWins/numberOfGames)*100).toFixed(2) + } +} + +//display output message +const printMessage = message => { + outputMessage.innerHTML = message +} + +//reset game +const resetGameFunction = () => { + if (confirm("Reset Game? Your data will be erased.")) { + window.location = window.location + } else { + alert('Game reset fail.') + } +} + +//reset board +const resetBoardFunction = () => { + board = [] + firstCard = null + firstCardElement = null + deck = [] + canClick = true + totalPairs = 0 + matchedPairs = 0 +} +// Create the appearance of a card by adding name and suit to the existing face-down cards +const createCardUI = (card, cardElement) => { + const suit = document.createElement('div') + suit.classList.add('suit', card.colour) + suit.innerText = card.suit + + const name = document.createElement('div') + name.classList.add('name', card.colour) + name.innerText = card.name + + cardElement.appendChild(name) + cardElement.appendChild(suit) + cardElement.classList.add('face-up') +} + +const setUIEffects = (clickedCard, cardElement) => { + const match = clickedCard.name === firstCard.name && clickedCard.suit === firstCard.suit; + if (match) { + matchedPairs += 1; + let message = `It's a match!` + cardElement.classList.add('match') + firstCardElement.classList.add('match') + if (matchedPairs === totalPairs) { + // This means user won, clear timer + winCounter +=1 + message += '
You win!!!' + clearInterval(timer) + } + printMessage(message) + } else { + cardElement.classList.add('no-match') + firstCardElement.classList.add('no-match') + printMessage(`No match! Try again.`) + } + + setTimeout(() => { + // The effects set above last for 1 sec and will be removed here + cardElement.classList.remove('match') + firstCardElement.classList.remove('match') + cardElement.classList.remove('no-match') + firstCardElement.classList.remove('no-match') + + if (matchedPairs === totalPairs) { + resetBoardFunction() + } + else if (!match) { + cardElement.innerHTML = '' + firstCardElement.innerHTML = '' + cardElement.classList.remove('face-up') + firstCardElement.classList.remove('face-up') + } + firstCard = null + canClick = true + printMessage(`Find the Pairs.`) + }, 1000) +} + +const cardClick = (cardElement, row, column) => { + if (canClick) { + const clickedCard = board[row][column]; + // the user already clicked on this card + if (cardElement.innerHTML !== '') { + return + } + // first turn + if (firstCard === null) { + firstCard = clickedCard + // turn this card over + createCardUI(firstCard, cardElement) + // hold onto this for later when it may not match + firstCardElement = cardElement + // second turn + } else { + // don't allow the user to click until the ui effects are done + canClick = false; + createCardUI(clickedCard, cardElement) + setUIEffects(clickedCard, cardElement) + } + } +} + +// Create all the board elements that will go on the screen +const buildBoardElements = () => { + for (let i = 0; i < board.length; i += 1) { + // make a container for a row of cards + const rowElement = document.createElement('div') + rowElement.classList.add('row') + + // make all the cards for this row + for (let j = 0; j < board[i].length; j += 1) { + // create the card element + const card = document.createElement('div') + card.classList.add('card', 'face-down') + + card.addEventListener('click', (event) => { + cardClick(event.currentTarget, i, j) + }); + rowElement.appendChild(card) + } + boardElement.appendChild(rowElement) + } +} + +const initBoard = () => { + numOfGames += 1 + // Assignment of variables + playerName = formPlayerName.value + boardSize = formBoardSize.value + + // Change texts based on inputs + playerMessage.textContent=`${playerName}'s Match Game!` + playerWinMessage.textContent =`Total Wins: ${winCounter}` + playerWinRate.textContent = `Win Rate: ${winRate(winCounter,numOfGames)}%` + + boardElement.innerHTML ="" + boardSize = formBoardSize.value + totalPairs = (boardSize * boardSize) / 2; + makeDeck(); + + // deal the cards out to the board data structure + for (let i = 0; i < boardSize; i += 1) { + board.push([]); + for (let j = 0; j < boardSize; j += 1) { + board[i].push(deck.pop()); + } + } + buildBoardElements(board); +} + +const initTimer = () => { + const minutes = formTimeSelection.value + let seconds = minutes * 60 + playerTimer.innerText = `${minutes}:00` + timer = setInterval(() => { + seconds -= 1 + const secondsLeft = seconds % 60 + const minutesLeft = Math.floor(seconds / 60) + + if (secondsLeft >= 10) { + playerTimer.innerText = `${minutesLeft}:${secondsLeft}` + } else { + playerTimer.innerText = `${minutesLeft}:0${secondsLeft}` + } + + if (seconds <= 0) { + clearInterval(timer) + canClick = false + printMessage("Time's up! You lose.") + setTimeout(resetBoardFunction, 1000) + } + }, 1000) +} + +// ### INITIALISE GAME PROPER ### +const initGame = () => { + if (formPlayerName.value.length < 1 || formPlayerName.value == " ") { + alert('Please input your name') + } + else { + // Hide modal & unhide container + modal.classList.add("hide") + modalContainer.classList.add("hide") + container.classList.remove("hide") + + //Initialise Board and Timer + initBoard() + initTimer() + } +} + +// ### EVENT LISTENERS ### +formSubmitBtn.addEventListener("click", initGame) +resetGame.addEventListener("click",resetGameFunction) +resetBoard.addEventListener("click",() => { + clearInterval(timer) + resetBoardFunction() + initTimer() + initBoard() +}) diff --git a/styles.css b/styles.css index 04e7110..20577e2 100644 --- a/styles.css +++ b/styles.css @@ -1,3 +1,206 @@ +@import url("https://fonts.googleapis.com/css2?family=Odibee+Sans&family=Poppins&display=swap"); body { - background-color: pink; + background-image: url("https://i.pinimg.com/564x/9a/b9/d2/9ab9d210c745e2a3d588a29c10fa440f.jpg"); + background-size: cover; + background-position: center center; + background-repeat: no-repeat; + display: flex; + flex-direction: column; + box-sizing: border-box; + align-items: center; + height: 97.8vh; + font-family: "Poppins"; +} + +.hide { + height: 0; + display: none; +} + +#header { + margin-top: -4px; + color: #f5f5f5; +} + +.modal { + margin-top: 80px; + display: flex; + justify-content: center; + align-items: center; + font-size: 1.1rem; +} +.modal .modal-container { + width: 500px; + padding: 15px 10px; + margin-top: 80px; + background-color: #a2a2a1; + background-image: url("https://media.istockphoto.com/photos/green-casino-background-picture-id842444492?k=20&m=842444492&s=170667a&w=0&h=D88I7VvqcL_SyCn_lPRUSJo5L-izvzQYaCJqpSzHZ_g="); + background-size: cover; + background-position: center center; + background-repeat: no-repeat; + border: 3px solid #b60014; +} +.modal .modal-container .modal-title { + font-size: 1.65rem; + text-align: center; + padding: 0 10px 20px 10px; + font-family: "Odibee Sans"; + color: #fcf6f5; + text-transform: uppercase; + text-align: center; + letter-spacing: 0.5rem; + vertical-align: text-bottom; +} +.modal .modal-container .form-container { + display: flex; + flex-direction: column; + padding: 0 5%; +} +.modal .modal-container .form-container .form-item { + width: 100%; + display: flex; + margin-right: -5px; + margin-bottom: 5px; +} +.modal .modal-container .form-container .form-item .label { + display: inline-block; + width: 50%; + vertical-align: bottom; + font-family: "Poppins"; + color: #f5f5f5; +} +.modal .modal-container .form-container .form-item #form-player-name, +.modal .modal-container .form-container .form-item #form-time-selection, +.modal .modal-container .form-container .form-item #form-board-size { + font-size: 1.3rem; + width: 50%; + border: 2px solid black; + font-family: "Poppins"; + margin: 2px 0; + background-color: #a2a2a1; + border: 2px solid #b60014; +} +.modal .modal-container .form-container .form-submit { + font-size: 1rem; + margin-top: 20px; + margin-bottom: 5px; + padding: 12px; + align-self: center; + border-radius: 10px; + display: inline-block; + border-color: #7c7c7c; + + background-image: linear-gradient( + 180deg, + #c7c7c7 0%, + #e6e6e6 47%, + #c7c7c7 53%, + #b3b3b3 100% + ); +} +.modal .modal-container .form-container .form-submit:hover { + transform: scale(0.98); + color: #b60014; + text-shadow: rgba(211, 62, 79, 0.701) 0 -1px 0, #f0d1d1 0 2px 1px, + #99ddff 0 0 5px, rgba(0, 128, 255, 0.6) 0 0 20px; + box-shadow: inset #c72929 0 0px 0px 4px, + inset rgba(193, 10, 10, 0.816) 0 -1px 5px 4px, + inset rgba(23, 27, 28, 0.7) 0 -1px 0px 7px, + inset rgba(255, 255, 255, 0.7) 0 2px 1px 7px, + rgba(243, 30, 30, 0.8) 0 0px 3px 2px; +} +.container { + font-size: 18px; + margin-top: -80px; +} +#game-title { + text-transform: capitalize; +} + +.center-middle { + color: mintcream; + display: flex; + align-items: center; + justify-content: center; + margin: 10px; +} + +.content-top, +.content-bottom { + color: mintcream; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.card { + margin: 5px 10px; + padding: 5px; + width: 35px; + height: 50px; + text-align: center; + border: solid 2px; + border-radius: 8px; + display: inline-block; + vertical-align: top; +} + +.face-down { + background: repeating-linear-gradient( + 135deg, + LightGoldenRodYellow 0px, + LightGoldenRodYellow 15px, + black 30px, + red 60px + ); +} + +.face-up { + background: white; +} + +.suit { + margin: 3px; + font-size: 14px; +} + +.name { + font-size: 16px; + font-weight: bold; + font-family: sans-serif; +} + +.red { + color: red; + font-size: 24px; + margin: -2px; +} + +.black { + color: black; + font-size: 24px; + margin: 0; +} + +#board { + margin-top: 10px; +} + +.match { + border: solid 2px gold; +} + +.no-match { + border: solid 2px red; +} + +button { + font-family: "Poppins"; + border-radius: 10px; +} +button:hover { + transform: scale(0.98); + background-color: red; + color: whitesmoke; }