skip to content

JavaScript: Memory Card Game with Animation

This is a more advanced version of our previously presented Memory card game with source code. In the game you have to find pairs among a set of face-down cards by turning over two at a time.

Working Demonstration

The game uses a JavaScript class and requires only a single HTML element on the page with the rest being created dynamically during initialisation:

The card symbols to be matched in pairs are the following:

Changes from the previous version include:

  • no inline HTML;
  • CSS-generated card backs;
  • AI-generated card symbols;
  • better behaved animations;
  • card placeholders;
  • pop-up messages;
  • ...

We could have also included speech and sound effects, but that seemed unnecessary.

HTML Source Code

As mentioned above, we have done away with all of the inline HTML leaving just an empty container that will be populated (hydrated) with the game elements.

The entire HTML code you will need to run this is as follows:

<html> <head> <title>JavaScript Card Game v2.0 | The Art of Web</title> <link rel="stylesheet" href="css-animation.css"> </head> <body> <div id="stage"><!-- --></div> <script src="css-animation2.js"></script> <script> new CardGame("stage"); </script> </body> </html>

CSS Styles

The CSS is a bit more complicated to that presented earlier as there are a lot more elements at play. We have not just the cards, but also all the placeholders that appear underneath, as well as the start and end messages:

#stage {   border: 0.5em solid;   border-color: #D2AB6F #8C6529; } #felt {   position: relative;   margin: 0 0 0 200px;   height: 500px;   background-color: green;   background-image: repeating-linear-gradient(     to top right,     rgba(255,255,255,0),     rgba(255,255,255,0) 80px,     rgba(255,255,255,0.2) 80px,     rgba(255,255,255,0.2) 150px   );   box-shadow: inset 0 0 10px rgba(0,0,0,0.4); } #felt > div {   position: absolute;   left: -140px;   top: 100px;   width: 74px;   height: 105px;   background-position: center center;   background-repeat: no-repeat;   background-size: contain;   border-radius: 4px;   transform-origin: center; } #felt > div.deck {   background: rgba(0,0,0,0.1); } #felt > div.holder {   background: rgba(255,255,255,0.1); } #felt > div.card {   background-color: white;   background-image: repeating-linear-gradient(     to top left,     midnightblue 0em,     white 4em,     midnightblue 4em   );   border: 1px solid #666;   transition-property: left, top, transform;   transition-timing-function: ease-out, ease-out, ease-in-out;   transition-duration: 0.5s;   cursor: pointer; } #felt > div.card.black {   background-color: black; } #felt > div.card:nth-child(odd) {   transform: rotate(1deg); } #felt > div.card:nth-child(3n) {   transform: rotate(-1deg); } #felt > aside {   display: none;   position: absolute;   left: 10%;   top: 20%;   width: 80%;   height: 60%;   justify-content: center;   align-items: center;   background: rgba(0,0,0,0.9);   border: 5px solid white;   font-family: monospace;   text-transform: uppercase;   text-shadow: 0 0 0.5em black;   text-align: center;   font-size: 3em;   color: white; }

expand code box

Note that there are no images being sourced by the CSS as everything apart from the card symbols has been created using CSS. The card symbols are loaded using JavaScript when a card is clicked.

While JavaScript is used to move and rotate cards, the actual transitions are defined in the CSS with separate transition timing functions for left, top and transform.

JavaScript Source Code

Here is the replacement JavaScript code in full:

const CardGame = function(targetId) {   "use strict";   // Original JavaScript code by Chirp Internet: www.chirpinternet.eu   // Please acknowledge use of this code by including this header.   const cards = [];   const cardValues = [     "1B", "2B", "3B", "4B", "5B", "6B", "7B", "8B",     "1W", "2W", "3W", "4W", "5W", "6W", "7W", "8W"   ];   /* initialise */   let gameStarted = false;   let card1 = false, card2 = false;   let matchesFound = 0;   const container = document.getElementById(targetId);   const felt = document.createElement("div");   felt.id = "felt";   container.replaceChildren(felt);   const messageWindow = document.createElement("aside");   const deckHolder = document.createElement("div");   deckHolder.className = "deck";   felt.append(messageWindow, deckHolder);   /* create cards from template */   for(let i=0; i < cardValues.length; i++) {     const holder = document.createElement("div");     holder.className = "holder";     holder.style.top = 15 + 120 * Math.floor(i/4) + "px";     holder.style.left = 70 + 100 * (i%4) + "px";     const newCard = document.createElement("div");     newCard.className = "card";     newCard.fromTop  = 15 + 120 * Math.floor(i/4);     newCard.fromLeft = 70 + 100 * (i%4);     newCard.matched = false;     newCard.addEventListener("click", (e) => { cardClick(i); });     felt.append(holder, newCard);     cards.push(newCard);   }   /* define local methods */   const displayMessage = (text) => {     if(text) {       messageWindow.style.display = "flex";       messageWindow.innerText = text;     } else {       messageWindow.style.display = "none";       messageWindow.innerText = "";     }   };   displayMessage("⬅ Click deck to start game");   const hideCard = (id) => {     /* turn card face down */     cards[id].style.backgroundImage = "";     cards[id].style.backgroundSize = "cover";     cards[id].style.transform = "scale(1.0) rotate(" + (Math.floor(Math.random() * 5) + 178) + "deg)";   };   const moveToPack = (id) => {     /* move card to pack */     hideCard(id);     cards[id].matched = true;     Object.assign(cards[id].style, {       top: "100px",       left: "-140px",       transform: "rotate(0deg)",       zIndex: (10 * matchesFound),     });     if(++matchesFound == cardValues.length) {       /* game over, reset */       displayMessage("Game over 😼");       matchesFound = 0;       gameStarted = false;     }   };   const moveToPlace = (id) => {     /* deal card */     Object.assign(cards[id].style, {       top: cards[id].fromTop + "px",       left: cards[id].fromLeft + "px",       transform: "rotate(" + (Math.floor(Math.random() * 5) + 178) + "deg)",       zIndex: 10,     });   };   const showCard = (id) => {     /* reveal card, check for match */     cards[id].style.backgroundImage = "url(/images/symbols/" + cardValues[id] + ".png)";     cards[id].style.backgroundSize = "80% 80%";     /* because Firefox doesn't know which way to rotate */     const matches = cards[id].style.transform.match(/^rotate\((\d+)deg\)$/);     if(matches && (matches[1] <= 180)) {       cards[id].style.transform = "scale(1.2) rotate(175deg)";     } else {       cards[id].style.transform = "scale(1.2) rotate(185deg)";     }     if(false === card1) {       /* first card revealed */       card1 = id;     } else {       /* second card revealed */       card2 = id;       if(parseInt(cardValues[card1]) == parseInt(cardValues[card2])) {         /* match found */         window.setTimeout(moveToPack, 800, card1);         window.setTimeout(moveToPack, 1000, card2);       } else {         /* no match found */         window.setTimeout(hideCard, 700, card1);         window.setTimeout(hideCard, 800, card2);       }       card1 = card2 = false;     }   };   const dealCards = () => {     /* shuffle cards */     cardValues.sort(       () => Math.round(Math.random()) - 0.5     );     /* deal cards */     for(let i=0; i < cardValues.length; i++) {       cards[i].matched = false;       if(cardValues[i].match(/W/)) {         cards[i].classList.add("black");       } else {         cards[i].classList.remove("black");       }       window.setTimeout(moveToPlace, i * 100, i);     }     gameStarted = true;   };   const cardClick = (id) => {     if(!gameStarted) {       displayMessage("");       dealCards();     } else if((id !== card1) && !cards[id].matched) {       showCard(id);     }   }; };

expand code box

The code has been marked up using ES6 notation and uses a JavaScript object which operates as a stand-alone class with private variables and methods.

It includes a work-around for a Firefox quirk where the cards were rotating in the wrong direction, and some dynamic tweaking of the z-index values to give the appearance of cards being taken from and returning to the top of the deck. We also have to toggle the background colour for the inverted symbols.

Improvements

The next step would be to have the style sheet appended dynamically to the DOM using JavaScript and being able to specify a path to the card images, but those are relatively minor details.

Please feel free to adapt use this code and let us know what you come up with using the Feedback form below. The game can be adapted for image recognition or language learning by changing the images used.

References

< JavaScript

Post your comment or question
top