TypeScript Tutorial: Building a Simple Interactive Memory Game

Ever found yourself captivated by a simple game of memory, trying to match pairs of cards and testing your recall? Memory games, with their blend of fun and cognitive challenge, are a timeless classic. But have you ever thought about how these games are built? In this tutorial, we’ll dive into the world of TypeScript and create our own interactive memory game. This isn’t just about coding; it’s about understanding how to structure a game, manage state, handle user interactions, and bring a fun experience to life.

Why TypeScript?

TypeScript, a superset of JavaScript, brings a whole new level of organization and reliability to your code. By adding static typing, TypeScript helps catch errors early in the development process, making your code more robust and easier to maintain. For a project like a memory game, where you have different card types, game states, and user interactions, TypeScript’s features will be invaluable.

Setting Up Your Project

Before we start coding, let’s set up our project. You’ll need Node.js and npm (Node Package Manager) installed on your system. Open your terminal or command prompt and navigate to the directory where you want to create your project. Then, follow these steps:

  1. Initialize the project: Run npm init -y. This creates a package.json file with default settings.
  2. Install TypeScript: Run npm install typescript --save-dev. This installs TypeScript as a development dependency.
  3. Create a TypeScript configuration file: Run npx tsc --init. This generates a tsconfig.json file, which configures how TypeScript compiles your code.
  4. Create project structure: Create a folder named src. Inside src, create a file named index.ts. This is where we will write our game logic.
  5. Create an HTML file: Create an index.html file in the root directory. This will hold the structure of our game.

Your project structure should look something like this:

memory-game/
├── index.html
├── package.json
├── tsconfig.json
└── src/
    └── index.ts

HTML Structure (index.html)

Let’s start by setting up the HTML structure for our game. This will include the game board, where the cards will be displayed, and potentially a section for displaying the score and game status.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Memory Game</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>Memory Game</h1>
        <div class="game-board" id="gameBoard">
            <!-- Cards will be dynamically added here -->
        </div>
        <div class="game-info">
            <p>Moves: <span id="moves">0</span></p>
            <p>Matches: <span id="matches">0</span> / <span id="totalMatches">0</span></p>
        </div>
    </div>
    <script src="./dist/index.js"></script>
</body>
</html>

Here, we have a basic structure with a title, a game-board div where our cards will be displayed, and a game-info section to display the moves and matches. Note that we’re linking to a style.css file (which we’ll create later) for styling and a dist/index.js file which will be generated by the TypeScript compiler.

Basic Styling (style.css)

Let’s add some basic styling to make our game look appealing. Create a style.css file in the root directory and add the following CSS rules:

.container {
    width: 80%;
    margin: 20px auto;
    text-align: center;
}

.game-board {
    display: grid;
    grid-template-columns: repeat(4, 100px);
    grid-gap: 10px;
    margin-top: 20px;
    justify-content: center;
}

.card {
    width: 100px;
    height: 100px;
    border: 1px solid #ccc;
    border-radius: 5px;
    cursor: pointer;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 2em;
    background-color: #f0f0f0;
}

.card.flipped {
    background-color: #fff;
    border: 1px solid #000;
}

.game-info {
    margin-top: 20px;
}

This CSS sets up the basic layout, including the grid for the cards, card dimensions, and some styling for the flipped state. Feel free to customize the styling as you like.

TypeScript Code (index.ts)

Now, let’s dive into the core of our game – the TypeScript code. We’ll start by defining the necessary types and variables.


// Define a type for our cards
interface Card {
    id: number;
    value: string;
    flipped: boolean;
}

// Get references to HTML elements
const gameBoard = document.getElementById('gameBoard') as HTMLElement;
const movesDisplay = document.getElementById('moves') as HTMLSpanElement;
const matchesDisplay = document.getElementById('matches') as HTMLSpanElement;
const totalMatchesDisplay = document.getElementById('totalMatches') as HTMLSpanElement;

// Game state variables
let cards: Card[] = [];
let flippedCards: Card[] = [];
let moves: number = 0;
let matches: number = 0;
const totalPairs: number = 8; // Assuming 16 cards (8 pairs)

// Function to generate cards
function createCards(): Card[] {
    const cardValues = [
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'
    ];
    const cards: Card[] = [];

    cardValues.forEach((value, index) => {
        // Create two cards for each value
        cards.push({ id: index * 2, value, flipped: false });
        cards.push({ id: index * 2 + 1, value, flipped: false });
    });

    // Shuffle the cards
    shuffleArray(cards);
    return cards;
}

// Function to shuffle an array (Fisher-Yates shuffle)
function shuffleArray<T>(array: T[]): T[] {
    for (let i = array.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [array[i], array[j]] = [array[j], array[i]]; // Swap elements
    }
    return array;
}

// Function to render the cards on the game board
function renderCards(): void {
    if (!gameBoard) return;
    gameBoard.innerHTML = ''; // Clear the board

    cards.forEach(card => {
        const cardElement = document.createElement('div');
        cardElement.classList.add('card');
        cardElement.dataset.id = card.id.toString(); // Store the card ID

        if (card.flipped) {
            cardElement.classList.add('flipped');
            cardElement.textContent = card.value;
        } else {
            cardElement.textContent = ''; // Or display a back of card image
        }

        cardElement.addEventListener('click', () => cardClickHandler(card));
        gameBoard.appendChild(cardElement);
    });
}

// Function to handle card clicks
function cardClickHandler(card: Card): void {
    if (flippedCards.length < 2 && !card.flipped) {
        card.flipped = true;
        flippedCards.push(card);
        renderCards();

        if (flippedCards.length === 2) {
            setTimeout(checkMatch, 1000);
        }
    }
}

// Function to check for a match
function checkMatch(): void {
    moves++;
    movesDisplay.textContent = moves.toString();

    const [card1, card2] = flippedCards;

    if (card1.value === card2.value) {
        // Match found
        matches++;
        matchesDisplay.textContent = matches.toString();
        flippedCards = []; // Reset

        if (matches === totalPairs) {
            alert('Congratulations! You won with ' + moves + ' moves!');
            resetGame();
        }
    } else {
        // No match
        setTimeout(() => {
            card1.flipped = false;
            card2.flipped = false;
            flippedCards = [];
            renderCards();
        }, 1000);
    }
}

// Function to reset the game
function resetGame(): void {
    cards = createCards();
    flippedCards = [];
    moves = 0;
    matches = 0;
    movesDisplay.textContent = '0';
    matchesDisplay.textContent = '0';
    totalMatchesDisplay.textContent = totalPairs.toString();
    renderCards();
}

// Initialize the game
function initializeGame(): void {
    totalMatchesDisplay.textContent = totalPairs.toString();
    cards = createCards();
    renderCards();
}

// Call initializeGame when the page loads
window.onload = initializeGame;

Let’s break down this code:

  • Card Interface: Defines the structure of a card with properties for ID, value, and flipped state.
  • HTML Element References: Gets references to the game board and the elements that display the moves and matches.
  • Game State Variables: Declares variables to store the cards, flipped cards, moves, and matches.
  • createCards(): This function creates an array of card objects, each with a unique ID and a value (e.g., ‘A’, ‘B’, ‘C’). It duplicates each card value to create pairs. Then, it shuffles the array using the shuffleArray function.
  • shuffleArray<T>(): Implements the Fisher-Yates shuffle algorithm to randomize the order of the cards.
  • renderCards(): Clears the game board and then iterates through the cards array to create a div element for each card. It adds the card class to each div, as well as a flipped class if the card’s flipped property is true. It adds an event listener to each card to handle clicks.
  • cardClickHandler(card: Card): Handles the click event on a card. It checks if the user has selected less than two cards and that the card is not already flipped. If both conditions are met, it flips the card, adds it to the flippedCards array, and calls renderCards(). If two cards have been flipped, it calls checkMatch() after a short delay.
  • checkMatch(): Increments the move counter and checks if the two flipped cards match. If they match, the match counter is incremented, and flippedCards is reset. If they do not match, the cards are flipped back after a delay. If all matches are found the game is reset.
  • resetGame(): Resets the game state and re-renders the cards.
  • initializeGame(): This function is called when the page loads to set up the game by creating the cards and rendering them on the board.

Compiling and Running the Game

Now that we have our TypeScript code, let’s compile it and run the game. Open your terminal and navigate to your project directory. Then, run the following command:

tsc

This command will compile your TypeScript code into JavaScript, creating a dist folder containing index.js. Next, open your index.html file in a web browser. You should see the game board with the cards. When you click on a card, it should flip over, and when you click on a second card, the game should determine if it’s a match.

Common Mistakes and How to Fix Them

Here are some common mistakes and how to avoid them:

  • Incorrect Paths: Ensure that the paths to your CSS and JavaScript files in your HTML are correct. If the game doesn’t load correctly, double-check these paths.
  • Typo Errors: TypeScript helps prevent these. Check for any typos in variable names or function calls.
  • Uninitialized Variables: Make sure all variables are initialized before use. TypeScript will often catch this.
  • Incorrect Event Handling: Ensure that event listeners are correctly attached to the card elements.
  • Missing Semicolons: While TypeScript can often infer semicolons, it’s good practice to use them for clarity.

Enhancements and Next Steps

This is a basic implementation, and there are many ways to enhance the game. Here are some ideas:

  • Add Difficulty Levels: Implement different difficulty levels by changing the number of card pairs.
  • Add Timer: Include a timer to track how long it takes to complete the game.
  • Add Visual Effects: Enhance the user experience with animations when cards flip or match.
  • Add Sound Effects: Play sound effects when cards flip, match, or the game is won.
  • Improve Styling: Refine the CSS to create a more visually appealing game.
  • Add Local Storage: Save high scores using local storage.

Key Takeaways

In this tutorial, we’ve built a simple yet engaging memory game using TypeScript. We’ve covered the basics of setting up a TypeScript project, creating HTML and CSS, and writing the core game logic. You’ve learned how to define types, handle events, manage game state, and create a user-friendly experience. Remember, the key to building a good game is to break it down into smaller, manageable parts. Start with the basic functionality, and then add features incrementally.

FAQ

  1. Why use TypeScript for a simple game? TypeScript adds structure, helps prevent errors, and makes your code easier to maintain, even for small projects.
  2. How do I debug my TypeScript code? Use your browser’s developer tools to inspect the generated JavaScript code, set breakpoints, and examine variables.
  3. Can I use a framework like React or Angular with this game? Yes, you can. TypeScript works well with frameworks. You could refactor the code to use components and state management provided by the framework.
  4. How can I deploy this game online? You can deploy your game to platforms like Netlify or GitHub Pages.

This tutorial provides a solid foundation, and you can now experiment with different features and improvements. The world of game development is vast, and with TypeScript, you have a powerful tool to bring your ideas to life. The experience of creating a game like this helps you develop problem-solving skills, which are transferable to any software engineering role.