Have you ever played the classic memory game, where you flip over cards to find matching pairs? It’s a fun and engaging way to test your memory, and it’s also a fantastic project to learn TypeScript! In this tutorial, we’ll build a simple, interactive memory game from scratch, using TypeScript to add type safety and structure to our code. We’ll cover everything from setting up your project to implementing the game logic and user interface. By the end, you’ll have a working game and a solid understanding of how TypeScript can improve your development workflow.
Why TypeScript?
Before we dive in, let’s talk about why we’re using TypeScript. TypeScript is a superset of JavaScript that adds static typing. This means you can define the types of your variables, function parameters, and return values. This helps catch errors early in the development process, makes your code easier to understand and maintain, and provides better tooling support (like autocompletion and refactoring) in your IDE.
Here are some key benefits:
- Early Error Detection: TypeScript catches type-related errors during development, saving you time and frustration.
- Improved Code Readability: Type annotations make it easier to understand what your code does.
- Enhanced Tooling: IDEs provide better autocompletion, refactoring, and navigation.
- Refactoring Support: Makes refactoring your code safer and easier.
- Scalability: TypeScript helps manage larger codebases more effectively.
In this tutorial, we’ll see how these benefits translate into a more robust and enjoyable development experience.
Setting Up Your Project
Let’s get started by setting up our project. We’ll use npm (Node Package Manager) to manage our dependencies and TypeScript to compile our code. If you don’t have Node.js and npm installed, you’ll need to install them first. You can download them from the official Node.js website.
- Create a Project Directory: Create a new directory for your project, for example, `memory-game`.
- Initialize npm: Open your terminal, navigate to your project directory, and run `npm init -y`. This creates a `package.json` file.
- Install TypeScript: Run `npm install typescript –save-dev`. This installs TypeScript as a development dependency.
- Create a `tsconfig.json` file: In your project directory, run `npx tsc –init`. This creates a `tsconfig.json` file, which configures the TypeScript compiler. You can customize this file to adjust compilation options, but for this tutorial, the default settings will work fine.
- Create a Source Directory: Create a directory called `src` where we’ll store our TypeScript files.
Your project structure should now look something like this:
memory-game/
├── package.json
├── tsconfig.json
└── src/
Creating the Game Logic
Now, let’s start writing the core game logic. We’ll create a TypeScript file called `game.ts` inside the `src` directory. This file will contain the logic for shuffling the cards, handling card flips, checking for matches, and managing the game state.
Here’s the code:
// src/game.ts
// Define a type for a card
interface Card {
id: number;
value: string;
flipped: boolean;
matched: boolean;
}
// Function to shuffle an array (Fisher-Yates shuffle)
function shuffleArray<T>(array: T[]): T[] {
const shuffledArray = [...array]; // Create a copy to avoid modifying the original array
for (let i = shuffledArray.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]];
}
return shuffledArray;
}
// Function to create a deck of cards
function createDeck(cardValues: string[]): Card[] {
const deck: Card[] = [];
cardValues.forEach((value, index) => {
deck.push({ id: index * 2, value, flipped: false, matched: false });
deck.push({ id: index * 2 + 1, value, flipped: false, matched: false });
});
return shuffleArray(deck);
}
// Function to flip a card
function flipCard(card: Card): Card {
return { ...card, flipped: !card.flipped };
}
// Function to check for a match
function checkForMatch(card1: Card, card2: Card): boolean {
return card1.value === card2.value;
}
// Function to update matched cards
function markAsMatched(card1: Card, card2: Card): Card[] {
return [{ ...card1, matched: true }, { ...card2, matched: true }];
}
// Function to check if the game is won
function isGameWon(deck: Card[]): boolean {
return deck.every(card => card.matched);
}
export { Card, createDeck, flipCard, checkForMatch, markAsMatched, isGameWon, shuffleArray };
Let’s break down this code:
- `Card` Interface: We define an interface `Card` to represent a card in our game. It has properties for `id`, `value`, `flipped`, and `matched`.
- `shuffleArray` Function: This function shuffles an array using the Fisher-Yates shuffle algorithm. This is essential for randomizing the card positions.
- `createDeck` Function: This function creates the deck of cards. It takes an array of card values (e.g., “A”, “B”, “C”) and duplicates each value to create pairs. It then shuffles the deck.
- `flipCard` Function: This function flips a card by toggling its `flipped` property.
- `checkForMatch` Function: This function checks if two cards match based on their `value`.
- `markAsMatched` Function: This function marks two matching cards as `matched`.
- `isGameWon` Function: This function checks if all cards in the deck have been matched.
- `export` Statement: We export the `Card` interface and the functions so they can be used in other parts of our application.
Building the User Interface (UI)
Now, let’s create the HTML and JavaScript (using TypeScript) for the user interface. We’ll create a simple layout with a grid to display the cards, and we’ll add event listeners to handle card clicks.
- Create an `index.html` file: Create an `index.html` file in your project directory.
- Add HTML Structure: Add the basic HTML structure, including a container for the game board.
- Create a `style.css` file: Create a `style.css` file in your project directory.
- Add CSS Styling: Add some basic CSS to style the game board and cards.
- Create an `app.ts` file: Create an `app.ts` file in your `src` directory. This file will contain the code to interact with the DOM and handle game events.
Here’s the code for each file:
index.html:
<!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 generated here -->
</div>
<button id="resetButton">Reset Game</button>
</div>
<script src="app.js"></script>
</body>
</html>
style.css:
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
background-color: #f0f0f0;
font-family: sans-serif;
}
h1 {
margin-bottom: 20px;
}
.game-board {
display: grid;
grid-template-columns: repeat(4, 100px);
grid-gap: 10px;
margin-bottom: 20px;
}
.card {
width: 100px;
height: 100px;
background-color: #ccc;
border-radius: 5px;
display: flex;
justify-content: center;
align-items: center;
font-size: 2em;
cursor: pointer;
user-select: none;
transition: transform 0.3s;
}
.card.flipped {
background-color: #fff;
border: 1px solid #ddd;
}
.card.matched {
background-color: #90ee90; /* Light green */
pointer-events: none; /* Disable clicks on matched cards */
}
#resetButton {
padding: 10px 20px;
font-size: 1em;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
app.ts:
// src/app.ts
import { Card, createDeck, flipCard, checkForMatch, markAsMatched, isGameWon } from './game';
const cardValues = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']; // Card values (letters)
let deck: Card[] = [];
let flippedCards: Card[] = [];
let gameBoard: HTMLElement | null = null;
let resetButton: HTMLButtonElement | null = null;
// Function to create a card element
function createCardElement(card: Card): HTMLElement {
const cardElement = document.createElement('div');
cardElement.classList.add('card');
cardElement.dataset.id = card.id.toString(); // Store the card's ID as a data attribute
cardElement.textContent = card.flipped ? card.value : ''; // Show value if flipped
// Add click event listener to the card element
cardElement.addEventListener('click', () => {
if (card.flipped || card.matched || flippedCards.length === 2) return; // Prevent flipping if already flipped, matched, or two cards are already flipped
flipCardAndUpdateUI(card);
});
return cardElement;
}
// Function to flip a card and update the UI
async function flipCardAndUpdateUI(card: Card) {
const cardIndex = deck.findIndex(c => c.id === card.id);
if (cardIndex === -1) return; // Defensive check
deck[cardIndex] = flipCard(card);
flippedCards.push(deck[cardIndex]);
updateUI();
if (flippedCards.length === 2) {
await checkForMatchAndUpdate();
}
}
// Function to check for a match and update the UI
async function checkForMatchAndUpdate() {
const [card1, card2] = flippedCards;
if (!card1 || !card2) return;
if (checkForMatch(card1, card2)) {
// Mark cards as matched
deck = deck.map(card => {
if (card.id === card1.id || card.id === card2.id) {
return { ...card, matched: true };
}
return card;
});
updateUI();
if (isGameWon(deck)) {
setTimeout(() => {
alert('Congratulations! You won!');
resetGame();
}, 500); // Delay to show the matched cards
}
} else {
// Flip cards back after a short delay
await new Promise(resolve => setTimeout(resolve, 1000)); // Delay for 1 second
deck = deck.map(card => {
if (card.id === card1.id || card.id === card2.id) {
return { ...card, flipped: false };
}
return card;
});
updateUI();
}
flippedCards = []; // Clear flipped cards
}
// Function to update the UI (render the cards)
function updateUI() {
if (!gameBoard) return;
gameBoard.innerHTML = ''; // Clear existing cards
deck.forEach(card => {
const cardElement = createCardElement(card);
if (card.flipped) {
cardElement.classList.add('flipped');
}
if (card.matched) {
cardElement.classList.add('matched');
}
gameBoard.appendChild(cardElement);
});
}
// Function to reset the game
function resetGame() {
deck = createDeck(cardValues);
flippedCards = [];
updateUI();
}
// Initialize the game when the page loads
function initializeGame() {
gameBoard = document.getElementById('gameBoard');
resetButton = document.getElementById('resetButton') as HTMLButtonElement;
if (!gameBoard || !resetButton) {
console.error('Game board or reset button not found.');
return;
}
deck = createDeck(cardValues);
updateUI();
resetButton.addEventListener('click', resetGame);
}
// Call initializeGame after the DOM is fully loaded
document.addEventListener('DOMContentLoaded', () => {
initializeGame();
});
Let’s break down `app.ts`:
- Import Statements: We import the `Card`, `createDeck`, `flipCard`, `checkForMatch`, `markAsMatched`, and `isGameWon` functions from `game.ts`.
- Variables: We declare variables to store the card values, the deck of cards, the flipped cards, and references to the game board and reset button in the HTML.
- `createCardElement` Function: This function creates a card element (a `div`) and adds a click event listener.
- `flipCardAndUpdateUI` Function: This function flips a card, updates the UI, and calls `checkForMatchAndUpdate` if two cards are flipped.
- `checkForMatchAndUpdate` Function: This function checks if the flipped cards match. If they match, it marks them as matched. If they don’t match, it flips them back after a short delay. It also handles the win condition.
- `updateUI` Function: This function clears the game board and renders the cards based on the current state of the `deck`.
- `resetGame` Function: This function resets the game by creating a new deck and updating the UI.
- `initializeGame` Function: This function initializes the game by getting references to the game board and reset button, creating the initial deck, and attaching the reset button’s click event.
- `DOMContentLoaded` Event Listener: This ensures that the `initializeGame` function runs after the HTML is fully loaded.
Compiling and Running the Game
Now, let’s compile our TypeScript code and run the game.
- Compile the TypeScript code: In your terminal, run `npx tsc`. This will compile your `app.ts` and `game.ts` files into JavaScript files (`app.js` and `game.js`) in the same `src` directory.
- Run the HTML file: Open the `index.html` file in your web browser. You should see the game board with the cards.
- Play the game: Click on the cards to flip them over and try to find matching pairs.
If everything is set up correctly, you should now be able to play the memory game in your browser! You can also inspect the `app.js` and `game.js` files to see the compiled JavaScript code.
Common Mistakes and How to Fix Them
Here are some common mistakes you might encounter and how to fix them:
- Type Errors: TypeScript will highlight type errors during compilation. Read the error messages carefully; they usually point to the exact line of code where the error occurred. Make sure your variables and function parameters have the correct types.
- Incorrect Paths: Double-check the paths in your import statements. Make sure you’re importing the correct files and functions.
- DOM Element Not Found: If you get an error that a DOM element (like the game board or reset button) is not found, make sure you have the correct `id` attributes in your HTML and that your JavaScript code is running after the DOM is fully loaded. The `DOMContentLoaded` event listener in `app.ts` is crucial for this.
- Incorrect CSS Styling: If your cards don’t look right, review your CSS. Make sure you’ve correctly applied the styles to the relevant classes (e.g., `.card`, `.flipped`, `.matched`). Use your browser’s developer tools to inspect the elements and see which styles are being applied.
- Logic Errors: Debugging game logic can be tricky. Use `console.log()` statements to check the values of your variables and the flow of your code. For instance, log the `flippedCards` array after each card flip to ensure it’s behaving as expected.
Adding More Features (Enhancements)
Once you have the basic game working, you can add more features to enhance it:
- Timer: Add a timer to track how long the player takes to complete the game.
- Scoreboard: Keep track of the player’s score (e.g., number of moves).
- Difficulty Levels: Allow the player to choose the number of card pairs (e.g., easy, medium, hard).
- Animations: Add CSS transitions or JavaScript animations to make the card flips more visually appealing.
- Sound Effects: Add sound effects for card flips, matches, and winning the game.
- Responsive Design: Make the game responsive so it looks good on different screen sizes.
- Themes: Allow the player to choose different themes (e.g., different card designs or background colors).
These enhancements are great for practicing your TypeScript skills and expanding your understanding of game development principles.
Key Takeaways
In this tutorial, we’ve built a simple memory game using TypeScript. We’ve covered:
- Setting up a TypeScript project.
- Defining interfaces and using types.
- Creating game logic with functions for shuffling, flipping cards, checking for matches, and managing the game state.
- Building a user interface with HTML, CSS, and TypeScript.
- Compiling and running the game.
- Troubleshooting common mistakes.
You’ve also learned how TypeScript improves code readability, reduces errors, and makes your code more maintainable. This project provides a solid foundation for further TypeScript exploration and game development.
FAQ
Here are some frequently asked questions about this tutorial and TypeScript in general:
- Why use TypeScript instead of JavaScript? TypeScript provides static typing, which helps catch errors early, improves code readability, and enhances tooling support. It makes your code more robust and easier to maintain, especially for larger projects.
- Can I use this game on a website? Yes! You can deploy the `index.html`, `style.css`, and `app.js` files to a web server. Make sure to compile your TypeScript code to JavaScript first using `npx tsc`.
- How can I debug my TypeScript code? You can debug your TypeScript code using your browser’s developer tools (e.g., Chrome DevTools). Set breakpoints in your `app.ts` file and inspect the values of your variables. You can also use `console.log()` statements to print values to the console.
- What are some other TypeScript projects I can try? You can try building other simple games (e.g., Tic-Tac-Toe, Hangman), web applications (e.g., a to-do list app, a simple blog), or exploring libraries like React or Angular with TypeScript.
This tutorial is just a starting point. Experiment with the code, add new features, and try different approaches. The more you practice, the better you’ll become at using TypeScript and building interactive applications.
