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)
+
-
-
-
-
-
+
+
+ (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;
}