Javascript Memory Game

Hi,

I have a memory game in javascript and I need help with 2 things. Although the first one I’m wondering if it’s possible.

  1. I would like the cards to be shown when the game starts and then after X seconds, the card is flipped back.

  2. Each card I would like to have a number to be labelled on it (e.g 1, 2, 3, 4, 5, 6 for Easy mode)

Here are the codes and here’s a demo : https://kuochye.github.io/memorygame/2

HTML

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Memory Game</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="js/MemoryGame.js" type="text/javascript"></script>
    <script src="js/Card.js" type="text/javascript"></script>
    <link href="css/MemoryGame.css" type="text/css" rel="stylesheet" />
  </head>
  <body>
          <section class="memory--menu-bar">
      <div class="inner">
        <div class="left"><h1 class="memory--app-name"><img src="rsnlogo.png"></h1></div>
        <div class="right"><a href="#settings"><img id="memory--settings-icon" src="images/gear.png" /></a>
          </div>
      </div>
    </section>
    <section id="memory--settings-modal" class="valign-table modal show">
      <div class="valign-cell">
        <form>
          <h2 class="memory--settings-label">Difficulty level:</h2>
          <select id="memory--settings-grid">
            <option value="2x3">Easy</option>
            <option value="3x4">Medium</option>
            <option value="4x5">Hard</option>
            <option value="5x6">Insane</option>
          </select>
          <input id="memory--settings-reset" type="submit" value="Start New Game" />
        </form>
      </div>
    </section>
    <section id="memory--end-game-modal" class="valign-table modal">
      <div class="valign-cell">
        <div class="wrapper">
          <h2 id="memory--end-game-message"></h2>
          <h3 id="memory--end-game-score"></h3>
        </div>
      </div>
    </section>
    <section id="memory--app-container">
      <ul id="memory--cards">
      </ul>
    </section>
    <script src="js/BrowserInterface.js" type="text/javascript"></script>
  </body>
</html>

BrowserInterface.js

(function($) {

// Handle clicking on settings icon
  var settings = document.getElementById('memory--settings-icon');
  var modal = document.getElementById('memory--settings-modal');
  var handleOpenSettings = function (event) {
    event.preventDefault();
    modal.classList.toggle('show');
  };
  settings.addEventListener('click', handleOpenSettings);

// Handle settings form submission
  var reset = document.getElementById('memory--settings-reset');
  var handleSettingsSubmission = function (event) {
    event.preventDefault();

    var selectWidget = document.getElementById("memory--settings-grid").valueOf();
    var grid = selectWidget.options[selectWidget.selectedIndex].value;
    var gridValues = grid.split('x');
    var cards = $.initialize(Number(gridValues[0]), Number(gridValues[1]));

    if (cards) {
      document.getElementById('memory--settings-modal').classList.remove('show');
      document.getElementById('memory--end-game-modal').classList.remove('show');
      document.getElementById('memory--end-game-message').innerText = "";
      document.getElementById('memory--end-game-score').innerText = "";
      buildLayout($.cards, $.settings.rows, $.settings.columns);
    }

  };
  reset.addEventListener('click', handleSettingsSubmission);

  // Handle clicking on card
  var handleFlipCard = function (event) {

    event.preventDefault();

    var status = $.play(this.index);
    console.log(status);

    if (status.code != 0 ) {
      this.classList.toggle('clicked');
    }

    if (status.code == 3 ) {
      setTimeout(function () {
        var childNodes = document.getElementById('memory--cards').childNodes;
        childNodes[status.args[0]].classList.remove('clicked');
        childNodes[status.args[1]].classList.remove('clicked');
      }.bind(status), 500);
    }
    else if (status.code == 4) {
      var score = parseInt((($.attempts - $.mistakes) / $.attempts) * 100, 10);
      var message = getEndGameMessage(score);

      document.getElementById('memory--end-game-message').textContent = message;
      /*document.getElementById('memory--end-game-score').textContent =
          'Score: ' + score + ' / 100';*/

      document.getElementById("memory--end-game-modal").classList.toggle('show');
    }

  };

  var getEndGameMessage = function(score) {
    var message = "";

    if (score == 100) {
      message = "Amazing job!"
    }
    else if (score >= 70 ) {
      message = "Great job!"
    }
    else if (score >= 50) {
      message = "Great job!"
    }
    else {
      message = "You can do better.";
    }

    return message;
  }

  // Build grid of cards
  var buildLayout = function (cards, rows, columns) {
    if (!cards.length) {
      return;
    }

    var memoryCards = document.getElementById("memory--cards");
    var index = 0;

    var cardMaxWidth = document.getElementById('memory--app-container').offsetWidth / columns;
    var cardHeightForMaxWidth = cardMaxWidth * (3 / 4);

    var cardMaxHeight = document.getElementById('memory--app-container').offsetHeight / rows;
    var cardWidthForMaxHeight = cardMaxHeight * (4 / 3);

    // Clean up. Remove all child nodes and card clicking event listeners.
    while (memoryCards.firstChild) {
      memoryCards.firstChild.removeEventListener('click', handleFlipCard);
      memoryCards.removeChild(memoryCards.firstChild);
    }

    for (var i = 0; i < rows; i++) {
      for (var j = 0; j < columns; j++) {
        // Use cloneNode(true) otherwise only one node is appended
        memoryCards.appendChild(buildCardNode(
            index, cards[index].value, cards[index].isRevealed,
            (100 / columns) + "%", (100 / rows) + "%"));
        index++;
      }
    }

    // Resize cards to fit in viewport
    if (cardMaxHeight > cardHeightForMaxWidth) {
      // Update height
      memoryCards.style.height = (cardHeightForMaxWidth * rows) + "px";
      memoryCards.style.width = document.getElementById('memory--app-container').offsetWidth + "px";
      memoryCards.style.top = ((cardMaxHeight * rows - (cardHeightForMaxWidth * rows)) / 2) + "px";
    }
    else {
      // Update Width
      memoryCards.style.width = (cardWidthForMaxHeight * columns) + "px";
      memoryCards.style.height = document.getElementById('memory--app-container').offsetHeight + "px";
      memoryCards.style.top = 0;
    }

  };

  // Update on resize
  window.addEventListener('resize', function() {
    buildLayout($.cards, $.settings.rows, $.settings.columns);
  }, true);

  // Build single card
  var buildCardNode = function (index, value, isRevealed, width, height) {
    var flipContainer = document.createElement("li");
    var flipper = document.createElement("div");
    var front = document.createElement("a");
    var back = document.createElement("a");

    flipContainer.index = index;
    flipContainer.style.width = width;
    flipContainer.style.height = height;
    flipContainer.classList.add("flip-container");
    if (isRevealed) {
      flipContainer.classList.add("clicked");
    }

    flipper.classList.add("flipper");
    front.classList.add("front");
    front.setAttribute("href", "#");
    back.classList.add("back");
    back.classList.add("card-" + value);
    back.setAttribute("href", "#");

    flipper.appendChild(front);
    flipper.appendChild(back);
    flipContainer.appendChild(flipper);

    flipContainer.addEventListener('click', handleFlipCard);

    return flipContainer;
  };

})(MemoryGame);

Card.js

MemoryGame.Card = function(value) {
  this.value = value;
  this.isRevealed = false;

  this.reveal = function() {
    this.isRevealed = true;
  }

  this.conceal = function() {
    this.isRevealed = false;
  }
};

MemoryGame.js

var MemoryGame = {

  settings: {
    rows: 2,
    columns: 3
  },

  // Properties that indicate state
  cards: [], // Array of MemoryGame.Card objects
  attempts: 0, // How many pairs of cards were flipped before completing game
  mistakes: 0, // How many pairs of cards were flipped before completing game
  isGameOver: false,

  /**
   * Modify default settings to start a new game.
   * Both parameters need integers greater than one, and
   * at least one them  needs to be an even number.
   *
   * @param {number} columns
   * @param {number} rows
   * @return {array} shuffled cards
   */
  initialize : function(rows, columns) {
    var validOptions = true;

    // Validate arguments
    if (!(typeof columns === 'number' && (columns % 1) === 0 && columns > 1) ||
        !(typeof rows === 'number' && (rows % 1) === 0) && rows > 1) {
      validOptions = false;
      throw {
        name: "invalidInteger",
        message: "Both rows and columns need to be integers greater than 1."
      };
    }

    if ((columns * rows) % 2 !== 0) {
      validOptions = false;
      throw {
        name: "oddNumber",
        message: "Either rows or columns needs to be an even number."
      };
    }

    if (validOptions) {
      this.settings.rows = rows;
      this.settings.columns = columns;
      this.attempts = 0;
      this.mistakes = 0;
      this.isGameOver = false;
      this.createCards().shuffleCards();
    }

    return this.cards;
  },

  /**
   * Create an array of sorted cards
   * @return Reference to self object
   */
  createCards: function() {
    var cards = [];
    var count = 0;
    var maxValue = (this.settings.columns * this.settings.rows) / 2;
    while (count < maxValue) {
      cards[2 * count] = new this.Card(count + 1);
      cards[2 * count + 1] = new this.Card(count + 1);
      count++;
    }

    this.cards = cards;

    return this;
  },

  /**
   * Rearrange elements in cards array
   * @return Reference to self object
   */
  shuffleCards: function() {
    var cards = this.cards;
    var shuffledCards = [];
    var randomIndex = 0;

    // Shuffle cards
    while (shuffledCards.length < cards.length) {

      // Random value between 0 and cards.length - 1
      randomIndex  = Math.floor(Math.random() * cards.length);

      // If element isn't false, add element to shuffled deck
      if(cards[randomIndex]) {

        // Add new element to shuffle deck
        shuffledCards.push(cards[randomIndex]);

        // Set element to false to avoid being reused
        cards[randomIndex] = false;
      }

    }

    this.cards = shuffledCards;

    return this;
  },

  /**
   * A player gets to flip two cards. This function returns information
   * about what happens when a card is selected
   *
   * @param {number} Index of card selected by player
   * @return {object} {code: number, message: string, args: array or number}
   */
  play: (function() {
    var cardSelection = [];
    var revealedCards = 0;
    var revealedValues = [];

    return function(index) {
      var status = {};
      var value = this.cards[index].value;

      if (!this.cards[index].isRevealed) {
        this.cards[index].reveal();
        cardSelection.push(index);
        if (cardSelection.length == 2) {
          this.attempts++;
          if (this.cards[cardSelection[0]].value !=
              this.cards[cardSelection[1]].value) {
            // No match
            this.cards[cardSelection[0]].conceal();
            this.cards[cardSelection[1]].conceal();
            /**
             * Algorithm to determine a mistake.
             * Check if the pair of at least
             * one card has been revealed before
             *
             * indexOf return -1 if value is not found
             */
            var isMistake = false;

            if (revealedValues.indexOf(this.cards[cardSelection[0]].value) === -1) {
              revealedValues.push(this.cards[cardSelection[0]].value);
            }
            else {
              isMistake = true;
            }

            if (revealedValues.indexOf(this.cards[cardSelection[1]].value) === -1) {
              revealedValues.push(this.cards[cardSelection[1]].value);
            }

            if (isMistake) {
              this.mistakes++;
            }

            revealedValues.push(this.cards[cardSelection[0]].value);

            status.code = 3,
            status.message = 'No Match. Conceal cards.';
            status.args = cardSelection;
          }
          else {
            revealedCards += 2;
            if (revealedCards == this.cards.length) {
              // Game over
              this.isGameOver = true;
              revealedCards = 0;
              revealedValues = [];
              status.code = 4,
              status.message = 'GAME OVER! Attempts: ' + this.attempts +
                  ', Mistakes: ' + this.mistakes;
            }
            else {
              status.code = 2,
              status.message = 'Match.';
            }
          }
          cardSelection = [];
        }
        else {
          status.code = 1,
          status.message = 'Flip first card.';
        }
      }
      else {
        status.code = 0,
        status.message = 'Card is already facing up.';
      }

      return status;

    };
  })()

};

You can show them all with:

document.querySelectorAll(".flip-container").forEach(card => card.classList.add("clicked"));

So put that together with a setTimeout function and you have:

document.querySelectorAll(".flip-container").forEach(card => card.classList.add("clicked"));
setTimeout(function () {
    document.querySelectorAll(".flip-container").forEach(card => card.classList.remove("clicked"));
}, 1000);

And you have the cards showing for one second.

When it comes to showing a number on the cards, I recommend using CSS to achieve that.
The following technique to vert&horiz center content for unknown width and height elements from Centering in CSS: A Complete Guide looks to be your best shot.

.parent {
  position: relative;
}
.child {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

The bed calls, good luck. zzz

Hi Paul,

Thank you so much for your advice!

  1. The flipping of cards for x seconds works great except why would the very last card not be flipped over for the x seconds as specified?

Regarding showing number on the cards, how would I be able to use css as each card wont have a individual id. It would be dynamically created through the javascript.

This is where the grid of cards are built, would the labelling of cards be here too when the cards are displayed by the for loop? How would I be able to do it?

 // Build grid of cards
  var buildLayout = function (cards, rows, columns) {
    if (!cards.length) {
      return;
    }

    var memoryCards = document.getElementById("memory--cards");
    var index = 0;

    var cardMaxWidth = document.getElementById('memory--app-container').offsetWidth / columns;
    var cardHeightForMaxWidth = cardMaxWidth * (3 / 4);

    var cardMaxHeight = document.getElementById('memory--app-container').offsetHeight / rows;
    var cardWidthForMaxHeight = cardMaxHeight * (4 / 3);
      
    // Clean up. Remove all child nodes and card clicking event listeners.
    while (memoryCards.firstChild) {
      memoryCards.firstChild.removeEventListener('click', handleFlipCard);
      memoryCards.removeChild(memoryCards.firstChild);
    }

    for (var i = 0; i < rows; i++) {
      for (var j = 0; j < columns; j++) {
        // Use cloneNode(true) otherwise only one node is appended
        memoryCards.appendChild(buildCardNode(
            index, cards[index].value, cards[index].isRevealed,
            (100 / columns) + "%", (100 / rows) + "%"));
        index++;
      }
    }

    // Resize cards to fit in viewport
    if (cardMaxHeight > cardHeightForMaxWidth) {
      // Update height
      memoryCards.style.height = (cardHeightForMaxWidth * rows) + "px";
      memoryCards.style.width = document.getElementById('memory--app-container').offsetWidth + "px";
      memoryCards.style.top = ((cardMaxHeight * rows - (cardHeightForMaxWidth * rows)) / 2) + "px";
    }
    else {
      // Update Width
      memoryCards.style.width = (cardWidthForMaxHeight * columns) + "px";
      memoryCards.style.height = document.getElementById('memory--app-container').offsetHeight + "px";
      memoryCards.style.top = 0;
    }

  };

This is the css for your reference

html {
  width:100%;
  height:100%;
}
body {
  margin:0;
  padding:0;
  width:100%;
  height:100%;
}

.modal {
  position: fixed;
  height: 100%;
  right: 0;
  top: 0;
  z-index: 3;
  width: 100%;
  visibility: hidden;
  opacity:0;
  -webkit-transition:opacity 0.4s linear;
  -moz-transition:opacity 0.4s linear;
  -ms-transition:opacity 0.4s linear;
  -o-transition:opacity 0.4s linear;
  transition:opacity 0.4s linear;
}
.modal.show {
  visibility: visible;
  opacity:1;
}

.valign-table {
  display: table;
  table-layout: fixed;
}
.valign-cell {
  display: table-cell;
  vertical-align: middle;
}

#memory--app-container {
  /*background-color: #0061a8;*/
  background-image: url(../images/rsnback.jpg);
  background-position:50% 50%;
  background-repeat: no-repeat;
  width:100%;
  height:90%;
}

.memory--menu-bar {
  width:100%;
  background-color: #222;
  height:10%;
  display:table;
}

.memory--menu-bar .inner {
  display:table-cell;
  vertical-align:middle;
}
.memory--menu-bar .left {
  text-align: left;
  float:left;
  width:50%;
}
.memory--menu-bar .right {
  text-align: right;
  float:right;
  width:50%;
}
.memory--app-name {
  color:#ccc;
  font-size:26px;
  margin:0;
  padding:20px 20px;
  text-transform: uppercase;
  font-family: "Courier New";
  letter-spacing: 2px;
}
@media screen and (max-width: 480px) {
  .memory--app-name {
    font-size:16px;
  }
}

#memory--settings-icon {
  height: 24px;
  display: block;
  padding:45px 20px;
  float: right;
}

#memory--end-game-modal .wrapper {
  background-color: rgba(0, 0, 0, .86);
  text-align: center;
  color:#ccc;
  padding:8px 0;
}
#memory--end-game-modal h2,
#memory--end-game-modal h3 {
  margin:0;
  margin-bottom: 4px;
}

#memory--settings-modal {
  background-color: rgba(0, 0, 0, .86);
  z-index: 4;
}
#memory--settings-modal form {
  min-width:240px;
  width:50%;
  margin:0 auto;
  text-align: center;
  color:#ccc;
}

.memory--settings-label {
  margin:8px 0;
}

#memory--settings-grid,
#memory--settings-reset {
  width:100%;
  margin-bottom:16px;
  font-size:18px;
  background:transparent;
  color:#ccc;
  height:50px;
  text-align: center;
}

#memory--settings-grid option {
  padding-top:5px;
  padding-bottom:5px;
}

#memory--settings-reset {
  border-radius:5px;
  border:2px solid #ccc;
  cursor: pointer;
}

#memory--cards {
  margin:0 auto;
  padding:0;
  height:100%;
  list-style-type: none;
  list-style-image: none;
  position:relative;
}
/* entire container, keeps perspective */
.flip-container {
  -webkit-perspective: 1000px;
  perspective: 1000px;
  float:left;
}

/* flip the pane when clicked */
.flip-container.clicked .front {
  -webkit-transform: rotateY(180deg);
  -moz-transform: rotateY(180deg);
  -ms-transform: rotateY(180deg);
  -o-transform: rotateY(180deg);
  transform: rotateY(180deg);
}
.flip-container.clicked .back {
  -webkit-transform: rotateY(0deg);
  -moz-transform: rotateY(0deg);
  -ms-transform: rotateY(0deg);
  -o-transform: rotateY(0deg);
  transform: rotateY(0deg);
}

/* flip speed goes here */
.flipper {
  width:90%;
  height:90%;
  margin:0% auto;
  -webkit-transition: 0.5s;
  -moz-transition: 0.5s;
  -ms-transition: 0.5s;
  -o-transition: 0.5s;
  transition: 0.5s;
  -webkit-transform-style: preserve-3d;
  -moz-transform-style: preserve-3d;
  transform-style: preserve-3d;
  position: relative;
  top:5%;
  bottom:5%;
}

/* hide back of pane during swap */
.front, .back {
  width:100%;
  height:100%;
  display: block;
  -webkit-backface-visibility: hidden;
  -moz-backface-visibility: hidden;
  backface-visibility: hidden;
  -webkit-transition: 0.5s;
  -moz-transition: 0.5s;
  -ms-transition: 0.5s;
  -o-transition: 0.5s;
  transition: 0.5s;
  -webkit-transform-style: preserve-3d;
  -moz-transform-style: preserve-3d;
  transform-style: preserve-3d;
  position: absolute;
  top: 0;
  left: 0;
}

/* front pane, placed above back */
.front {
  /* for firefox 31 */
  -webkit-transform: rotateY(0deg);
  -moz-transform: rotateY(0deg);
  -ms-transform: rotateY(0deg);
  -o-transform: rotateY(0deg);
  transform: rotateY(0deg);
  background-color: #000000;
  border-radius: 5%;
  background-image: url(../images/rsn.png);
  background-position:50% 50%;
  background-repeat: no-repeat;
  /*background-size: contain;*/
}

/* back, initially hidden pane */
.back {
  -webkit-transform: rotateY(-180deg);
  -moz-transform: rotateY(-180deg);
  -ms-transform: rotateY(-180deg);
  -o-transform: rotateY(-180deg);
  transform: rotateY(-180deg);
  background-color: #fff;
  border-radius: 5%;
  background-position:50% 50%;
  background-repeat: no-repeat;
  background-size: cover;
}

.back.card-1 {
  background-image: url(../images/fruits/FCU1.jpg);
}
.back.card-2 {
  background-image: url(../images/fruits/fig.jpg);
}
.back.card-3 {
  background-image: url(../images/fruits/grape.jpg);
}
.back.card-4 {
  background-image: url(../images/fruits/kiwi.jpg);
}
.back.card-5 {
  background-image: url(../images/fruits/lemon.jpg);
}
.back.card-6 {
  background-image: url(../images/fruits/lime.jpg);
}
.back.card-7 {
  background-image: url(../images/fruits/mango.jpg);
}
.back.card-8 {
  background-image: url(../images/fruits/melon.jpg);
}
.back.card-9 {
  background-image: url(../images/fruits/orange.jpg);
}
.back.card-10 {
  background-image: url(../images/fruits/peach.jpg);
}
.back.card-11 {
  background-image: url(../images/fruits/pear.jpg);
}
.back.card-12 {
  background-image: url(../images/fruits/pinapple.jpg);
}
.back.card-13 {
  background-image: url(../images/fruits/plum.jpg);
}
.back.card-14 {
  background-image: url(../images/fruits/raspberry.jpg);
}
.back.card-15 {
  background-image: url(../images/fruits/strawberry.jpg);
}

[quote=“kaydentan, post:3, topic:279715, full:true”]

  1. The flipping of cards for x seconds works great except why would the very last card not be flipped over for the x seconds as specified?[/quote]

Some of the images won’t have loaded. You would need to wait until all of the images have loaded, which can get tricky, or you could use a sprite sheet for the images which makes it much easier to wait only for that sheet to have loaded.

While you can get increasing numbers using an <ol> structure, I don’t know if the index can be appropriately positioned in to place.

Thank you so much for your help Paul!

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.