diff --git a/index.html b/index.html
index 25a06fd..46b1104 100644
--- a/index.html
+++ b/index.html
@@ -1,13 +1,14 @@
-
- SWE101
-
-
+
+
+
+ Liz's Card Match Game
+
-
-
-
-
-
+
+
+
+
+
diff --git a/script.js b/script.js
index e2d0297..6e280f8 100644
--- a/script.js
+++ b/script.js
@@ -1 +1,345 @@
-// Please implement exercise logic here
+// ----- GLOBAL VARIABLES -----------------------
+const boardSize = 4;
+const board = [];
+
+let deck;
+let firstCard = null;
+let firstCardElement;
+
+// For gameplay
+let canClick = true;
+let gameCompleted = false;
+
+// For timer
+let milliseconds = 180000; // 3 minutes (1 min = 60 000ms)
+const delayInMilliseconds = 100; // 0.1 second
+let timerStarted = false;
+let timerRef;
+
+// For game information
+const gameInfoContainer = document.createElement('div');
+const gameInfo = document.createElement('div');
+const timerContainer = document.createElement('div');
+const timer = document.createElement('div');
+
+// ----- 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');
+};
+
+// Format timer
+const formatTimer = (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}`;
+};
+
+const startTimer = () => {
+ timerRef = setInterval(() => {
+ if (milliseconds <= 0) {
+ clearInterval(timerRef);
+ updateGameInfo(`Time's up! You lose.`);
+ canClick = false;
+ }
+
+ timer.innerHTML = formatTimer(milliseconds);
+ milliseconds -= delayInMilliseconds;
+ }, delayInMilliseconds);
+};
+
+const stopTimer = () => {
+ clearInterval(timerRef);
+ updateGameInfo(
+ `Congrats, you matched all the cards!
Refresh the page to play again.`
+ );
+ canClick = false;
+};
+
+const areAllCardsOpen = () => {
+ const numOfOpenCards = document.querySelectorAll('.open-card');
+ if (numOfOpenCards.length === 16) {
+ return true;
+ }
+ return false;
+};
+
+// ----- GAMEPLAY LOGIC -------------------------
+
+// What happens when user clicks on a square
+const openCard = (cardElement, row, column) => {
+ // Start timer on first ever card clicked
+ if (timerStarted === false) {
+ startTimer();
+ timerStarted = true;
+ }
+
+ // 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) {
+ stopTimer();
+ return;
+ }
+
+ // If not all cards have been open, update game info
+ else {
+ updateGameInfo(`Noice, it's a match!`);
+ setTimeout(() => {
+ updateGameInfo(
+ `Click a card to continue, or refresh the page to restart.`
+ );
+ }, 2000);
+
+ 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);
+
+ // "Turn cards over" after a set time
+ setTimeout(() => {
+ // "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');
+
+ updateGameInfo(`Click to open a card.`);
+ canClick = true;
+ }, 1500);
+
+ // Update game info
+ updateGameInfo(`Sorry, those didn't match. Try again!`);
+ }
+
+ // Reset the cards
+ firstCard = null;
+ }
+};
+
+// ----- GAME INITIALISATION --------------------
+
+// Create container for timer
+const createTimerContainer = () => {
+ timerContainer.classList.add('timer-container');
+ timerContainer.innerHTML = `You have 3 minutes to match all card pairs, starting from when you open your first card.
`;
+ document.body.appendChild(timerContainer);
+
+ timer.classList.add('timer');
+ timer.innerHTML = formatTimer(milliseconds);
+ timerContainer.appendChild(timer);
+};
+
+// 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);
+};
+
+const updateGameInfo = (msgText) => {
+ gameInfo.innerHTML = msgText;
+ gameInfoContainer.appendChild(gameInfo);
+};
+
+// 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());
+ }
+ }
+
+ createTimerContainer();
+ createGameInfoContainer();
+ createBoardContainer(board);
+};
+
+initGame();
diff --git a/styles.css b/styles.css
index 04e7110..836ddf3 100644
--- a/styles.css
+++ b/styles.css
@@ -1,3 +1,68 @@
body {
- background-color: pink;
+ background-color: lavenderblush;
+ font-family: monospace;
+ text-align: center;
+}
+
+h1 {
+ color: darkmagenta;
+}
+
+.timer-container {
+ color: darkmagenta;
+ font-size: 1.2em;
+ background-color: plum;
+ border: 2px solid mediumorchid;
+ width: 50vw;
+ padding: 1em;
+ margin: 2em auto;
+}
+
+.timer-container p {
+ margin-top: 0.5em;
+}
+
+.timer {
+ font-size: 1.5em;
+}
+
+.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;
}