4

I'm trying to rebrand this cool Memory Game developed by Nate Wiley.

I'm having an issue with it though. When you win, it allows you to restart, but the game become unplayable when the cards re-appear for a second time.

The restart button code calls this function:

reset: function(){
    this.hideModal();
    this.shuffleCards(this.cardsArray);
    this.setup();
    this.$game.show();
},

This correctly hides the modal, shuffles the card, runs the setup function, and shows the game again.

The Setup Function looks like this:

setup: function(){
    this.html = this.buildHTML();
    this.$game.html(this.html);
    this.$memoryCards = $(".card");
    this.paused = false;
    this.guess = null;
},

I'm not getting any errors in the console. How would I go about diagnosing an error in this situation?

I've created a codepen with only 2 matching pairs to make testing a lot quicker but I'm at a loss for how to figure this out.

https://codepen.io/nolaandy/pen/wPeGgo

Zakaria Acharki
  • 66,747
  • 15
  • 75
  • 101
4ndy
  • 446
  • 7
  • 26
  • *unplayable* sounds like some click listeners need to be rebound – Jonas Wilms Nov 13 '17 at 16:30
  • 2
    When you use `this.$game.html()`, you'll lose any event listeners for the elements in that HTML. – Barmar Nov 13 '17 at 16:33
  • The setup function destroys and redefines a bunch of html, but never attaches new event listeners to the re-created elements. You've to learn how to [delegate events](http://stackoverflow.com/questions/203198/event-binding-on-dynamically-created-elements). – Teemu Nov 13 '17 at 16:33

2 Answers2

3

Udpated Codepen.

All you need to do is binding the click event using the event delegation on like :

$('body').on("click", ".card", this.cardClicked);

Instead of :

this.$memoryCards.on("click", this.cardClicked);

So when you reset the cards dynamically from the reset() function, the click event will persist and could deal with the new card elements.

Hope this help.

Zakaria Acharki
  • 66,747
  • 15
  • 75
  • 101
  • Not the same answer, in my opinion. The OP asked "How would I go about diagnosing an error in this situation?" so I explained my process and posted a cleaner solution, I think. – Jonas Grumann Nov 13 '17 at 16:48
3

When you don't have errors in the console you need to think about what you'd expect the code to do and work back from there. In you example, I'd expect the game to do something when I click on the card, but it doesn't so I added a console.log() in the click handler and I noticed that nothing was being logged in the console so I though "probably the card element get's rebuilt from scratch, so it looses it's event listener", so I tried delegating the event to the body, with

$('body').on("click", ".card", this.cardClicked);

and I noticed that now the click event worked, so I knew I was getting somewhere. I still wanted to keep the click code the way it was so I noticed that in the setup function he doesn't call the binding function, which is the one that attaches click events to elements, so I tried calling this.binding(); inside the setup function and sure enough it works.

PS: In my opinion it would make sense to call the buinding() function inside the setup() function, because I probably want my click events to be attached to the buttons every time I set up a game, but I kept it the way it was to leave the code as similar as possible as the original one.

// Memory Game
// © 2014 Nate Wiley
// License -- MIT
// best in full screen, works on phones/tablets (min height for game is 500px..) enjoy ;)
// Follow me on Codepen

(function(){
 
 var Memory = {

  init: function(cards){
   this.$game = $(".game");
   this.$modal = $(".modal");
   this.$overlay = $(".modal-overlay");
   this.$restartButton = $("button.restart");
   this.cardsArray = $.merge(cards, cards);
   this.shuffleCards(this.cardsArray);
   this.setup();
   this.binding();
  },

  shuffleCards: function(cardsArray){
   this.$cards = $(this.shuffle(this.cardsArray));
  },

  setup: function(){
   this.html = this.buildHTML();
   this.$game.html(this.html);
   this.$memoryCards = $(".card");
   this.binding();
   this.paused = false;
      this.guess = null;
  },

  binding: function(){
   this.$memoryCards.on("click", this.cardClicked);
   $("button.restart").unbind().click( $.proxy(this.reset, this));
  },
  // kinda messy but hey
  cardClicked: function(){
   var _ = Memory;
   var $card = $(this);
   if(!_.paused && !$card.find(".inside").hasClass("matched") && !$card.find(".inside").hasClass("picked")){
    $card.find(".inside").addClass("picked");
    if(!_.guess){
     _.guess = $(this).attr("data-id");
    } else if(_.guess == $(this).attr("data-id") && !$(this).hasClass("picked")){
     $(".picked").addClass("matched");
     _.guess = null;
    } else {
     _.guess = null;
     _.paused = true;
     setTimeout(function(){
      $(".picked").removeClass("picked");
      Memory.paused = false;
     }, 600);
    }
    if($(".matched").length == $(".card").length){
     _.win();
    }
   }
  },

  win: function(){
   this.paused = true;
   setTimeout(function(){
    Memory.showModal();
    Memory.$game.fadeOut();
   }, 1000);
  },

  showModal: function(){
   this.$overlay.show();
   this.$modal.fadeIn("slow");
  },

  hideModal: function(){
   this.$overlay.hide();
   this.$modal.hide();
  },

  reset: function(){
   this.hideModal();
   this.shuffleCards(this.cardsArray);
   this.setup();
   this.$game.show();
  },
 
  // Fisher--Yates Algorithm -- https://bost.ocks.org/mike/shuffle/
  shuffle: function(array){
   var counter = array.length, temp, index;
     // While there are elements in the array
     while (counter > 0) {
         // Pick a random index
         index = Math.floor(Math.random() * counter);
         // Decrease counter by 1
         counter--;
         // And swap the last element with it
         temp = array[counter];
         array[counter] = array[index];
         array[index] = temp;
      }
      return array;
  },

  buildHTML: function(){
   var frag = '';
   this.$cards.each(function(k, v){
    frag += '<div class="card" data-id="'+ v.id +'"><div class="inside">\
    <div class="front"><img src="'+ v.img +'"\
    alt="'+ v.name +'" /></div>\
    <div class="back"><img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/74196/codepen-logo.png"\
    alt="Codepen" /></div></div>\
    </div>';
   });
   return frag;
  }
 };

 var cards = [
  {
   name: "php",
   img: "https://s3-us-west-2.amazonaws.com/s.cdpn.io/74196/php-logo_1.png",
   id: 1,
  },
  {
   name: "css3",
   img: "https://s3-us-west-2.amazonaws.com/s.cdpn.io/74196/css3-logo.png",
   id: 2
  },
 ];
    
 Memory.init(cards);


})();
* {
  box-sizing: border-box;
}
html, body {
  height: 100%;
}
body {
  background: black;
  min-height: 100%;
  font-family: "Arial", sans-serif;
}
.wrap {
  position: relative;
  height: 100%;
  min-height: 500px;
  padding-bottom: 20px;
}
.game {
  transform-style: preserve-3d;
  perspective: 500px;
  min-height: 100%;
  height: 100%;
}
@keyframes matchAnim {
  0% {
    background: #bcffcc;
  }
  100% {
    background: white;
  }
}
.card {
  float: left;
  width: 16.66666%;
  height: 25%;
  padding: 5px;
  text-align: center;
  display: block;
  perspective: 500px;
  position: relative;
  cursor: pointer;
  z-index: 50;
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
@media (max-width: 800px) {
  .card {
    width: 25%;
    height: 16.666%;
  }
}
.card .inside {
  width: 100%;
  height: 100%;
  display: block;
  transform-style: preserve-3d;
  transition: 0.4s ease-in-out;
  background: white;
}
.card .inside.picked, .card .inside.matched {
  transform: rotateY(180deg);
}
.card .inside.matched {
  animation: 1s matchAnim ease-in-out;
  animation-delay: 0.4s;
}
.card .front, .card .back {
  border: 1px solid black;
  backface-visibility: hidden;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  padding: 20px;
}
.card .front img, .card .back img {
  max-width: 100%;
  display: block;
  margin: 0 auto;
  max-height: 100%;
}
.card .front {
  transform: rotateY(-180deg);
}
@media (max-width: 800px) {
  .card .front {
    padding: 5px;
  }
}
.card .back {
  transform: rotateX(0);
}
@media (max-width: 800px) {
  .card .back {
    padding: 10px;
  }
}
.modal-overlay {
  display: none;
  background: rgba(0, 0, 0, .8);
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
.modal {
  display: none;
  position: relative;
  width: 500px;
  height: 400px;
  max-height: 90%;
  max-width: 90%;
  min-height: 380px;
  margin: 0 auto;
  background: white;
  top: 50%;
  transform: translateY(-50%);
  padding: 30px 10px;
}
.modal .winner {
  font-size: 80px;
  text-align: center;
  font-family: "Anton", sans-serif;
  color: #4d4d4d;
  text-shadow: 0px 3px 0 black;
}
@media (max-width: 480px) {
  .modal .winner {
    font-size: 60px;
  }
}
.modal .restart {
  font-family: "Anton", sans-serif;
  margin: 30px auto;
  padding: 20px 30px;
  display: block;
  font-size: 30px;
  border: none;
  background: #4d4d4d;
  background: linear-gradient(#4d4d4d, #222);
  border: 1px solid #222;
  border-radius: 5px;
  color: white;
  text-shadow: 0px 1px 0 black;
  cursor: pointer;
}
.modal .restart:hover {
  background: linear-gradient(#222, black);
}
.modal .message {
  text-align: center;
}
.modal .message a {
  text-decoration: none;
  color: #28afe6;
  font-weight: bold;
}
.modal .message a:hover {
  color: #56c0eb;
  border-bottom: 1px dotted #56c0eb;
}
.modal .share-text {
  text-align: center;
  margin: 10px auto;
}
.modal .social {
  margin: 20px auto;
  text-align: center;
}
.modal .social li {
  display: inline-block;
  height: 50px;
  width: 50px;
  margin-right: 10px;
}
.modal .social li:last-child {
  margin-right: 0;
}
.modal .social li a {
  display: block;
  line-height: 50px;
  font-size: 20px;
  color: white;
  text-decoration: none;
  border-radius: 5px;
}
.modal .social li a.facebook {
  background: #3b5998;
}
.modal .social li a.facebook:hover {
  background: #4c70ba;
}
.modal .social li a.google {
  background: #d34836;
}
.modal .social li a.google:hover {
  background: #dc6e60;
}
.modal .social li a.twitter {
  background: #4099ff;
}
.modal .social li a.twitter:hover {
  background: #73b4ff;
}
footer {
  height: 20px;
  position: absolute;
  bottom: 0;
  width: 100%;
  z-index: 0;
}
footer .disclaimer {
  line-height: 20px;
  font-size: 12px;
  color: #727272;
  text-align: center;
}
@media (max-width: 767px) {
  footer .disclaimer {
    font-size: 8px;
  }
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="wrap">
<div class="game"></div>
 
 <div class="modal-overlay">
  <div class="modal">
   <h2 class="winner">You Rock!</h2>
   <button class="restart">Play Again?</button>
   <p class="message">Developed on <a href="https://codepen.io">CodePen</a> by <a href="https://codepen.io/natewiley">Nate Wiley</a></p>
   <p class="share-text">Share it?</p>
   <ul class="social">
    <li><a target="_blank" class="twitter" href="https://twitter.com/share?url=https://codepen.io/natewiley/pen/HBrbL"><span class="fa fa-twitter"></span></a></li>
    <li><a target="_blank" class="facebook" href="https://www.facebook.com/sharer.php?u=https://codepen.io/natewiley/pen/HBrbL"><span class="fa fa-facebook"></span></a></li>
    <li><a target="_blank" class="google" href="https://plus.google.com/share?url=https://codepen.io/natewiley/pen/HBrbL"><span class="fa fa-google"></span></a></li>
   </ul>
  </div>
 </div>
  <footer>
  <p class="disclaimer">All logos are property of their respective owners, No Copyright infringement intended.</p>
 </footer>
  </div><!-- End Wrap -->
Jonas Grumann
  • 10,438
  • 2
  • 22
  • 40