Java Script Challenge: A Game of Memory

Hello everyone. If you visit the home page frequently or the CSS forum you might have seen some of the CSS challenges I’ve posted. This week’s challenge is broken in half between CSS and Java Script. The members of the CSS forum have been challenged to style and animate the cards of a game of Memory, and the challenge to the forum is to adjudicate the game. Here’s the HTML we are working from


<!DOCTYPE html>
<html>
<head>
  <title>Memory</title>
  <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
  <script type="text/javascript">
    $('.card').click(function(){
	  $(this).toggleClass('down');
	});
  </script>
</head>
<body>
  <div id="Playfield">
    <div class="card down one" data-face="1"></div>
    <div class="card down two" data-face="2"></div>
    <div class="card down three" data-face="3"></div>
    <div class="card down four" data-face="4"></div>
    <div class="card down five" data-face="5"></div>
    <div class="card down six" data-face="6"></div>
    <div class="card down seven" data-face="7"></div>
    <div class="card down eight" data-face="8"></div>
    <div class="card down nine" data-face="9"></div>
    <div class="card down ten" data-face="10"></div>
    <div class="card down eleven" data-face="11"></div>
    <div class="card down twelve" data-face="12"></div>
    <div class="card down one" data-face="1"></div>
    <div class="card down two" data-face="2"></div>
    <div class="card down three" data-face="3"></div>
    <div class="card down four" data-face="4"></div>
    <div class="card down five" data-face="5"></div>
    <div class="card down six" data-face="6"></div>
    <div class="card down seven" data-face="7"></div>
    <div class="card down eight" data-face="8"></div>
    <div class="card down nine" data-face="9"></div>
    <div class="card down ten" data-face="10"></div>
    <div class="card down eleven" data-face="11"></div>
    <div class="card down twelve" data-face="12"></div>
  </div>
</body>
</html>

Note that I’ve already provided the CSS members enough JS to test their styling - toggling the classes of the cards. Here’s the rest of the challenge, and most of its logic can be tied into the click event above

The cards are in pairs, their last class name reveals the matching. I don’t expect 4 year olds to check and read source code, so don’t worry about that - it simplifies the coding.

When a card is clicked, a check needs to be made if any other cards are face up that do not have the CSS class ‘matched’. If there is, does the value of the data-face attribute of the two cards match? If so, the two cards gain the css class of ‘matched’, otherwise turn both face down.

You also need to test when all cards are matched to give a ‘You Win’ result.

Advanced
Shuffle the position of the cards. You can do this a number of ways - but if you choose the changing of the data-face value make sure you change the corresponding css class because that’s how the styling is going to give the card it’s particular image.

Expert
Make a two player game and keep score.

At all levels you may add to the HTML to give additional controls, like a reset game or shuffle button, but don’t remove anything or you risk breaking the style sheets being written for the challenge.

Use spoiler tags to hide your answers to the challenge that you post.

This…
[noparse]

[spoiler]Like this[/spoiler]

[/noparse]

Renders

[spoiler]Like this[/spoiler]

If you want to participate in the CSS side of this challenge you may [thread=991430]do so here.[/thread]

Note - the data-face attribute is present to make the JS code easier to write - I realize it’s redundant to the final CSS class name. I also know it’s possible to style on attributes, but browser support for that trick is uneven (not that such has stopped us before). Both are presented to keep the difficulty of the challenge down some.

The problem with using HTML like that is that anyone with JavaScript turned off can see the cards but cannot interact with them. Far better id all the cards are added into the HTML via JavaScript after they have been shuffled. That way you haven’t got to update the existing HTML when shuffling and you also don’t have a broken game visible on the page when JavaScript is off.

For a single person version of the game you can also allow them to compare their score this attempt to prior attempts by keeping track of how long it takes. There’s nothing particularly difficult about creating a 2 or more player version that keeps score - it would just need each player to be identified first so it can display whose turn it is.

This is a Java Script exercise for fun. This is not a “make it work in all possible configurations over as wide an array of devices as possible” assignment. That wouldn’t be a challenge - that would be a job.

The HTML is presented primarily to simplify the exercise.

It is rather obvious that the basic exercise is aimed at JavaScript beginners as even the “expert” level suggestion should be a relatively trivial task to produce for anyone with an intermediate level knowledge of JavaScript. My comments were directed more at those considering the more “advanced” variants that you suggested. Anyone with a beginner level knowledge of JavaScript tackling the basic script should disregard all my comments as they are not intended for them.

For shuffling the cards first the task is definitely easier if that is done before adding the HTML into the page - which makes it easier to consider adding that HTML from JavaScript (at least that’s what I found with the various variants of this script that I have created).

Hi Michael,

Really nice idea for a challenge!!

Here’s my entry:

[spoiler]<!DOCTYPE html>
<html>
  <head>
    <title>Memory</title>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
    <style>
      .card{
        width:65px;
        height:100px;
        float:left;
        margin:5px;
        color:blue;
        padding:5px;
        border:solid 1px blue;
      }
      
      #Playfield{
        width: 600px;
      }
      
      .down{
        background:blue;
      }
      
      .clear{
        clear:both;
      }
      
      p {
        margin:0; 
        padding: 5px 15px 0 5px;
      }
      
      #playerInfo{
        padding:15px 0 35px 0;
      }
			
      #player1 p, #player2 p{
        float:left;
      }
      
      .active{
        background: yellow;
      }
    </style>
  </head>
  
  <body>
    <div id="Playfield">
      <div class="card down one" data-face="1"></div>
      <div class="card down two" data-face="2"></div>
      <div class="card down three" data-face="3"></div>
      <div class="card down four" data-face="4"></div>
      <div class="card down five" data-face="5"></div>
      <div class="card down six" data-face="6"></div>
      <div class="card down seven" data-face="7"></div>
      <div class="card down eight" data-face="8"></div>
      <div class="card down nine" data-face="9"></div>
      <div class="card down ten" data-face="10"></div>
      <div class="card down eleven" data-face="11"></div>
      <div class="card down twelve" data-face="12"></div>
      <div class="card down one" data-face="1"></div>
      <div class="card down two" data-face="2"></div>
      <div class="card down three" data-face="3"></div>
      <div class="card down four" data-face="4"></div>
      <div class="card down five" data-face="5"></div>
      <div class="card down six" data-face="6"></div>
      <div class="card down seven" data-face="7"></div>
      <div class="card down eight" data-face="8"></div>
      <div class="card down nine" data-face="9"></div>
      <div class="card down ten" data-face="10"></div>
      <div class="card down eleven" data-face="11"></div>
      <div class="card down twelve" data-face="12"></div>
    </div>
    
    <div id="playerInfo" class="clear">
      <div id="player1">
        <p><strong>Player 1:</strong></p>
        <p>Turns Taken: <span class="noTurns">0</span></p>
        <p>Pairs matched: <span class="noPairs">0</span></p>
      </div>
      
      <div id="player2" class="clear">
        <p><strong>Player 2:</strong></p>
        <p>Turns Taken: <span class="noTurns">0</span></p>
        <p>Pairs matched: <span class="noPairs">0</span></p>
      </div>
    </div>
    <p class="clear"><a href="#" id="reset">Click here to reset game</a></p>
    
    <script type="text/javascript">
      function twoCardsFaceUp(){
        return $(".up").length == 2;
      }
      
      function cardsMatch(){
        return $(".up:eq(0)").text() == $(".up:eq(1)").text();
      }
      
      function markCardsAsMatched(){
        $(".up").each(function(){
          $(this).addClass("matched").removeClass("up").off("click");
        });
      }
      
      function updateScore(player){
        var el = $(player).find(".noPairs");
        var p = Number(el.text());
        el.text(p+1);
      }
      
      function allCardsMatched(){
        return ($(".matched").length == 24)
      }
      
      function flipCardsBackOver(){
        setTimeout(function(){
          $(".up").each(function(){
            $(this).addClass("down").removeClass("up");
          });
        }, 1000);
      }
      
      function shuffle(cards){
        // Uses Fisher&#8211;Yates shuffle
        // See http://en.wikipedia.org/wiki/Fisher-Yates_shuffle
        var i = cards.length, j, tempi, tempj;
        if ( i == 0 ) return false;
        while ( --i ) {
          j = Math.floor( Math.random() * ( i + 1 ) );
          tempi = cards[i];
          tempj = cards[j];
          cards[i] = tempj;
          cards[j] = tempi;
        }
        return cards;
      }
      
      function highlightCurrentPlayer(player){
        $("#playerInfo p").each(function(){
          if ($(this).hasClass("active")){
            $(this).removeClass("active");
          }
        });
        $(player).find("p").first().addClass("active");
      }
      
      function incrementTurns(player){
        var el = $(player).find(".noTurns");
        var t = Number(el.text());
        el.text(t+1);
      }
      
      function updateCurrentPlayer(player){
        currentPlayer = (currentPlayer.match(/1/))? "#player2" : "#player1"
      }
      
      function winner(){
        var playerOnePoints = Number($("#player1").find(".noPairs").text());
        var playerTwoPoints = Number($("#player2").find(".noPairs").text());
        if (playerOnePoints > playerTwoPoints){
          return "Player one won!";
        } else if (playerOnePoints < playerTwoPoints){
          return "Player two won!";
        } else {
          return "An honourable draw!";
        }
      }
      
      // Shuffle cards
      var cards = $(".card");
      cards.remove();
      cards = shuffle(cards);
      cards.appendTo($("#Playfield"));
      
      // Main Loop
      $('.card').on("click", function(){
        if ($(".up").length == 2){
          return false;
        }
  
        $(this).removeClass("down").addClass("up");
        
        if (twoCardsFaceUp()){
          incrementTurns(currentPlayer);
          if (cardsMatch()){
            markCardsAsMatched();
            updateScore(currentPlayer);
            if (allCardsMatched()){
              alert(winner());
            }
          }else{
            flipCardsBackOver();
            updateCurrentPlayer(currentPlayer);
            setTimeout(function(){highlightCurrentPlayer(currentPlayer)}, 1000);
          }
        }
      });
      
      $("#reset").click(function(){
        location.reload();
      });
      
      $(".card").each(function(){
        var num = $(this).data("face");
        $(this).text(num);
      });
      
      var currentPlayer = "#player1";
      highlightCurrentPlayer(currentPlayer);
    </script>
  </body>
</html>[/spoiler]

I also made a demo in case anyone wants to check it out.

I’m doing both challenges (CSS and JS). I’m covering the advanced and expert things. I used some of the CSS I did for my CSS entry so you can actually see the divs (basically sans transitions), but the Playfield and its stuff is completely unchanged. Here is my JS entry (also uploaded a demo for convenience):

[spoiler]<!DOCTYPE html>
<html>
<head>
  <title>Memory</title>
  <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
  <script type="text/javascript">
    $(function () {
        var freeze = false,
            firstShuffle = true,
            curPlayer = 0,
            maxPlayers = 1,
            shuffle,
            cardClick,
            changeGameType,
            showScore,
            setupPlayers,
            players,
            tCards,
            determineWinner;
        showScore = function () {
            var i;
            for (i = 0; i < players.length; i += 1) {
                players[i].el.innerHTML = '<b>Player ' + (i + 1) + '</b>: matches: ' + players[i].matches + '; turns: ' + players[i].turns;
                if (curPlayer === i) {
                    $(players[i].el).addClass('active');
                } else {
                    $(players[i].el).removeClass('active');
                }
            }
        };
        determineWinner = function () {
            var winner = 0, isDraw, i;
            if (players.length === 1) {
                alert('You Win!');
            } else {
                isDraw = true;
                for (i = 1; i < players.length; i += 1) {
                    if (players[i].matches !== players[winner].matches) { isDraw = false; }
                    if (players[i].matches > players[winner].matches) {
                        winner = i;
                    }
                }
                if (isDraw) {
                    alert('Alas, a draw!');
                } else {
                    alert('Player ' + (winner + 1) + ' wins!');
                }
            }
        };
        cardClick = function () {
            var t;
            if (!freeze) {
                if (!$(this).hasClass('matched') && $(this).hasClass('down')) {
                    $(this).removeClass('down');
                    t = $('.card:not(.down, .matched)');
                    if (t.length >= 2) {
                        players[curPlayer].turns += 1;
                        if (t[0].getAttribute('data-face') === t[1].getAttribute('data-face')) {
                            $(t).addClass('matched');
                            players[curPlayer].matches += 1;
                            if ($('.matched').length === $('.card').length) {
                                showScore();
                                determineWinner();
                            }
                        } else {
                            freeze = true;
                            setTimeout(function () {
                                $(t).addClass('down');
                                freeze = false;
                                curPlayer += 1;
                                if (curPlayer >= players.length) { curPlayer = 0; }
                                showScore();
                            }, 1000);
                        }
                        showScore();
                    }
                }
            }
        };
        shuffle = function () {
            var allCards, i;
            allCards = $('.card');
            $(allCards).addClass('down').removeClass('matched');
            tCards = [];
            while (allCards.length > 0) {
                i = parseInt(Math.random()*allCards.length);
                tCards.push(allCards[i]);
                allCards.splice(i,1);
            }
            if (firstShuffle) {
                $('#Playfield').empty();
                $('#Playfield').append(tCards);
                $('.card').click(cardClick);
                firstShuffle = false;
            } else {            
                freeze = true;
                setTimeout(function () {
                    $('#Playfield').empty();
                    $('#Playfield').append(tCards);
                    $('.card').click(cardClick);
                    freeze = false;
                }, 700);
            }
        };
        setupPlayers = function () {
            var i;
            players = [];
            $('#PlayScore').empty();
            for (i = 0; i < maxPlayers; i += 1) {
                players.push({turns: 0, matches: 0, el: document.createElement('li')});
                $('#PlayScore').append(players[i].el);
            }
        };
        changeGameType = function () {
            $(this).text('Play ' + maxPlayers + '-player game');
            maxPlayers = maxPlayers === 1 ? 2 : 1;
            setupPlayers();
            showScore();
            shuffle();
        };
        $('#shuffleButton').click(shuffle);
        $('#playerButton').click(changeGameType);
        setupPlayers();
        showScore();
        shuffle();
    });
  </script>
  <style type="text/css">
    #Playfield {
        width: 500px;
        height: 300px;
        margin: 0 auto;
    }
    #PlayControls, #PlayScore {
        width: 500px;
        margin: 0 auto;
    }
    .card {
        background-color: #ffffff;
        border: 1px solid black;
        border-radius: 0px;
        float: left;
        width: 50px;
        height: 50px;
        padding: 5px;
        margin: 5px;
    }
    
    .card.one {
        border-radius: 0px;
    }
    .card.two {
        border-radius: 10px;
    }
    .card.three {
        border-radius: 30px;
    }
    .card.four {
        border-top-left-radius: 60px;
    }
    .card.five {
        border-top-right-radius: 60px;
    }
    .card.six {
        border-bottom-right-radius: 60px;
    }
    .card.seven {
        border-bottom-left-radius: 60px;
    }
    .card.eight {
        border-top-left-radius: 60px;
        border-bottom-right-radius: 60px;
    }
    .card.nine {
        border-top-right-radius: 60px;
        border-bottom-left-radius: 60px;
    }
    .card.ten {
        border-top-left-radius: 30px;
        border-top-right-radius: 30px;
    }
    .card.eleven {
        border-bottom-left-radius: 30px;
        border-bottom-right-radius: 30px;
    }
    .card.twelve {
        border-top-right-radius: 30px;
        border-bottom-left-radius: 30px;
        border-bottom-right-radius: 30px;
    }
    .card.down {
        background-color: #000000;
        border-radius: 0px;
    }
    .card.matched {
        background-color: #999999;
    }
    li.active {
        background-color: #aaaaff;
    }
  </style>
</head>
<body>
  <div id="Playfield">
    <div class="card down one" data-face="1"></div>
    <div class="card down two" data-face="2"></div>
    <div class="card down three" data-face="3"></div>
    <div class="card down four" data-face="4"></div>
    <div class="card down five" data-face="5"></div>
    <div class="card down six" data-face="6"></div>
    <div class="card down seven" data-face="7"></div>
    <div class="card down eight" data-face="8"></div>
    <div class="card down nine" data-face="9"></div>
    <div class="card down ten" data-face="10"></div>
    <div class="card down eleven" data-face="11"></div>
    <div class="card down twelve" data-face="12"></div>
    <div class="card down one" data-face="1"></div>
    <div class="card down two" data-face="2"></div>
    <div class="card down three" data-face="3"></div>
    <div class="card down four" data-face="4"></div>
    <div class="card down five" data-face="5"></div>
    <div class="card down six" data-face="6"></div>
    <div class="card down seven" data-face="7"></div>
    <div class="card down eight" data-face="8"></div>
    <div class="card down nine" data-face="9"></div>
    <div class="card down ten" data-face="10"></div>
    <div class="card down eleven" data-face="11"></div>
    <div class="card down twelve" data-face="12"></div>
  </div>
  <div id="PlayControls">
    <button id="shuffleButton">Shuffle/reset</button>
    <button id="playerButton">Play 2-player game</button>
  </div>
  <div id="PlayScore"></div>
</body>
</html>[/spoiler]

Here is a demo of both the JS and CSS entries working together, in case if anyone’s interested hehe. x] This and my post at the CSS challenge are my first posts. I did a few of the challenges but I never posted them for reasons unknown to myself.

@Pullo; I found a minor flaw in your version of the game which is if you press Ctrl + A you can see where all the matching numbers are

Aww dude, that’s not a flaw, it’s a feature!!

Just kidding :slight_smile:
Thanks for pointing it out.
I fixed this in my demo.

No problem, I used it to cheat for about 10 minutes since it was a “feature” :wink:

@jaythar Could you add local storage to your game, to show us how that works. Could you use it to keep a record of our attempts, so that we can see if we are improving?

OK bit cheeky but by that logic if you use any developer toolbar/view source you can view the data attributes and also just cheat and match them. Using just HTML and JS how would you suggest you mask the data-ids? Only way I can suggest is by minimising the JS file and hiding performing some kind of hash on each data-id making each card unique?!

Perhaps by not using them at all since they are in fact completely unnecessary to being able to get a memory game script to work. If the JavaScript only adds the information about the card selected into the HTML while the card is visible then you can’t tell what any of the other cards are unless you actually set a breakpoint in the script itself so as to be able to see what is stored where in the array.