Improve Your JavaScript Skills by Coding a Card Game

As a full-stack developer, one of the best ways to sharpen your front-end development skills is by building interactive projects with JavaScript. Implementing common UI patterns like games provides an opportunity to practice key programming concepts in a fun, engaging way.

In this article, we‘ll walk through the process of building a card matching game using vanilla JavaScript, HTML, and CSS. Along the way, you‘ll level up your JS abilities in areas like:

  • DOM manipulation and event handling
  • Controlling flow and application state
  • Working with arrays and objects
  • Writing modular, reusable code

But the learning doesn‘t stop there! Styling the game will flex your CSS muscles as you dive into responsive layouts with Grid and Flexbox, CSS animations and transitions, and more.

By the end, you‘ll not only have a fully functional game to play or add to your portfolio, but you‘ll also walk away with a handful of transferable, real-world programming techniques you can apply to any front-end development project. Let‘s get started!

Setting Up the Development Environment

Before diving into the code, let‘s make sure you have the tools needed.

  1. Code Editor – Any text editor will work, but I highly recommend using Visual Studio Code. It‘s free, runs everywhere, and has a massive extensions library to supercharge your coding. I use it for all my web development.

  2. Local Server – To properly load assets and test features like local storage, you‘ll need to run the project on a web server instead of opening the files directly in your browser. If you‘re using VS Code, the Live Server extension makes this a breeze. It starts up a development server and automatically reloads the page whenever you save changes.

  3. Starter Files – Create a new folder for the project with three files:

    • index.html
    • styles.css
    • script.js

With that, you‘re ready to start building! I encourage you to follow along and type out the code samples to get hands-on practice. If you get stuck, you can always refer to the [complete source code]().

Creating the Game HTML Structure

Let‘s begin by scaffolding out the game‘s HTML in index.html. We‘ll need:

  1. A <header> to display the game title and some controls
  2. A <main> section to hold the cards
  3. A <footer> for game info and messages
<body>
  <header>

    <button id="new-game-btn">New Game</button>
  </header>

  <main>
    <div id="game-board"></div>  
  </main>

  <footer>
    <p><span id="move-count">0</span> moves</p>
    <p id="game-message"></p>
  </footer>

  <script src="script.js"></script>
</body>

Each card will need to display an icon or image and maybe some text. We‘ll also use data attributes to track its state (face-up or face-down) and its matched pair.

<div class="card" data-index="0" data-match="A" data-face="down">
  <img src="card-back.png" class="card-back">
  <img src="apple.png" class="card-front">
  <span class="card-label">Apple</span>
</div>  

Since the number of cards is variable based on the game options, we‘ll generate the cards dynamically with JavaScript in a bit. For now, just include one "dummy" card element as a reference for styling.

Coding the Game Logic

With the HTML skeleton in place, let‘s turn our attention to the JavaScript needed to power the game engine. We‘ll use an object to track the game state:

const gameState = {
  cards: [],
  flippedCards: [],
  moveCount: 0,
  startTime: null,
  timerRef: null  
}

The cards array will hold objects representing each card and its state. We‘ll populate this when starting a new game based on the chosen difficulty level.

To kick things off, let‘s flesh out the startNewGame() function attached to the "New Game" button click:

function startNewGame() {
  resetGameState();

  const deckSize = parseInt(prompt(‘How many cards? (Enter an even number)‘));
  if (deckSize % 2 !== 0) {
    alert(‘Please enter an even number.‘);
    return;
  }

  const uniqueCards = generateUniqueCards(deckSize / 2);
  gameState.cards = [...uniqueCards, ...uniqueCards];  
  shuffleArray(gameState.cards);

  const gameBoard = document.getElementById(‘game-board‘);
  const cardHTML = gameState.cards.map((card, index) => `
    <div class="card" data-index="${index}" data-face="down" data-matched="false">
      <img src="${card.image}" class="card-front" alt="${card.name}">
      <img src="card-back.png" class="card-back" alt="Card back">         
    </div>
  `);
  gameBoard.innerHTML = cardHTML.join(‘‘);

  gameBoard.addEventListener(‘click‘, handleCardClick);

  gameState.startTime = Date.now();
  gameState.timerRef = setInterval(updateTimer, 1000);
}

This prompts the player to choose a deck size, generates an array of unique cards and their duplicates, shuffles them, and renders the cards to the page. It also kicks off a timer to track how long the game takes.

💡 Pro Tip: Generating the cards HTML with Array.map() and join() is much faster than repeated string concatenation or appendChild() calls.

The meat of the gameplay happens in the handleCardClick event listener callback:

function handleCardClick(event) {
  const card = event.target.closest(‘.card‘);
  const cardIndex = parseInt(card.dataset.index);

  if (!card || card.dataset.face === ‘up‘ || card.dataset.matched === ‘true‘ || gameState.locked) {
    return; // Don‘t allow invalid, already flipped, or matched cards to be flipped    
  }

  flipCard(card);
  gameState.flippedCards.push(gameState.cards[cardIndex]);

  if (gameState.flippedCards.length === 2) {
    gameState.locked = true;   
    gameState.moveCount++;

    if (isMatch(gameState.flippedCards[0], gameState.flippedCards[1])) {
      gameState.flippedCards.forEach(card => {
        document.querySelector(`[data-index="${card.index}"]`).dataset.matched = ‘true‘;
      });

      if (isGameWon()) {
        showGameOver();        
      }
    }
    else {
      setTimeout(() => {
        gameState.flippedCards.forEach(card => {
          flipCard(document.querySelector(`[data-index="${card.index}"]`), ‘down‘);
        });
        gameState.locked = false;
      }, 1000);
    }

    gameState.flippedCards = [];
  }

  updateMoveCount();
}

When a card is clicked, it‘s flipped over (if valid) and added to the flippedCards array. If two cards are flipped, the move count is incremented and we check for a match.

For matches, both cards are marked as "matched" and a check is done to see if the game has been won (all cards matched). For mismatches, the cards are flipped face-down again after a slight delay.

The flipCard() helper function can flip a card in either direction based on an optional second argument:

function flipCard(card, direction = ‘up‘) {
  card.dataset.face = direction;  
  card.classList.toggle(‘flipped‘, direction === ‘up‘);
}

The rest of the logic is filled out with small helper functions like isMatch(), isGameWon(), updateMoveCount(), and so on. Breaking the code into small, focused functions like this makes it more readable and maintainable.

Styling the Game

Now for the fun part – making the game look great! In styles.css, we can set up some basic styles for the page layout and the cards themselves.

The game board is a perfect use case for CSS Grid:

#game-board {
  display: grid;  
  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
  gap: 10px;
  perspective: 1000px;
}

.card {
  position: relative;
  height: 100px;
  transform-style: preserve-3d;
  transition: transform 0.5s;
}

.card-front, .card-back {    
  position: absolute;
  width: 100%;
  height: 100%;
  backface-visibility: hidden;
}

.card-back {
  background-color: black;
}

.card.flipped {
  transform: rotateY(180deg);  
}

.card-front {
  transform: rotateY(180deg);
}

The perspective and transform-style: preserve-3d on the game board, plus backface-visibility: hidden and the rotateY transforms on the card faces set up the "flip" effect. The .flipped class is toggled by the flipCard() helper function.

🎨 Design Tip: Add a slight delay to the card flip transition so it doesn‘t feel too abrupt. You can also experiment with different easing functions.

Media queries make it easy to adjust the card and font sizes for different screen widths:

@media (min-width: 600px) {
  .card {
    height: 150px;  
  }

  .card-label {
    font-size: 1.25rem;
  }
}

To jazz it up further, add a celebratory animation when the game is won:

@keyframes tada {
  0% {transform: scale(1);}
  10%, 20% {transform: scale(0.9) rotate(-3deg);}
  30%, 50%, 70%, 90% {transform: scale(1.1) rotate(3deg);}
  40%, 60%, 80% {transform: scale(1.1) rotate(-3deg);}
  100% {transform: scale(1) rotate(0);}
}

.card.matched {
  animation: tada 1s;
}

Next-Level Enhancements

With the core game complete, it‘s time to think about some extra features and enhancements:

  • Difficulty levels – Add buttons to choose between easy, medium, and hard difficulties. The harder the mode, the more cards to match.

  • High scores – Use localStorage to save the player‘s best times for each difficulty. Display a leaderboard on the page.

  • Accessibility – Ensure the game is playable with keyboard navigation and assistive technologies like screen readers. The ARIA spec has many techniques for this.

  • Sound effects – Add satisfying sounds when cards are flipped, matched, and when the game is won. Libraries like Howler.js make working with audio in JS a breeze.

  • Multiplayer – Allow two players to compete head-to-head using WebSockets.

  • PWA functionality – Make the game installable and playable offline by adding a web app manifest and service worker.

Leveling Up Your Skills

Building this card matching game covers a wide swath of front-end web development skills. But the learning doesn‘t stop there!

Practicing these techniques consistently is the key to internalizing them. Seek out coding challenges and tutorials to continue honing your craft. You can find a ton of free resources on sites like freeCodeCamp and Frontend Mentor.

Most importantly, build more projects! Create a Minesweeper clone, a Wordle-inspired word guessing game, or a virtual pet simulator. The more you stretch your skills, the more confident and capable you‘ll become.

If you‘re looking to level-up your back-end development skills next, try implementing this game using a JS framework like React, Angular, or Vue. You can even hook it up to a backend API using Node and Express or Firebase.

Wrapping Up

Congratulations on making it to the end! Take a moment to appreciate the working game you‘ve built from scratch. Developing a project like this is no small achievement.

I hope this deep dive has given you not only a fun game to play and share, but also a newfound confidence in your JavaScript and front-end development skills. You‘re well on your way to becoming a JS ninja!

Remember, the road to mastery is paved with consistent practice and a dedication to continuous learning. Keep coding, keep building, and most importantly, keep having fun! Happy coding!

Similar Posts