TypeScript Adventure: Conquering the Second Boss

TypeScript Adventure: Conquering the Second Boss

Upgrade your tools for the second boss

·

36 min read


Welcome, TypeScript adventurers, to Level 5 of our journey! You've made it this far, and now it's time to face the second boss challenge. We're going to put our newfound knowledge of functions and types to the test by creating an exciting Pixel Match Game. Let's dive in!

Prerequisites

Before we begin, make sure you're comfortable with JavaScript basics and have completed the previous levels. If you need to set up your development environment, check out the first blog for the tools you'll need to install.

The Pixel Match Game Challenge

Our second boss challenge is to create a game where players need to color specific pixels to match a given pattern within a time limit. We'll be using TypeScript, HTML, and CSS to bring this game to life.

What You'll Learn

  • Practical application of functions and types

  • Compiling and running a TypeScript project

  • Interacting with HTML and CSS from TypeScript

Step 1: Set up the project

  1. Create a new directory for your project and navigate into it:

    • Open your terminal or command prompt.

    • Use the mkdir command followed by the project name to create a new directory, e.g., mkdir pixel-match-game.

    • Use the cd command followed by the project name to navigate into the newly created directory, e.g., cd pixel-match-game.

mkdir pixel-match-game
cd pixel-match-game
  1. Initialize a new TypeScript project:
  • Run npm init -y to initialize a new Node.js project with default settings.

    • This command creates a package.json file that contains information about your project and its dependencies.
  • Run npm install typescript --save-dev to install TypeScript as a development dependency.

    • TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.
  • Run npx tsc --init to create a default TypeScript configuration file named tsconfig.json.

    • This file specifies the compiler options for your TypeScript project.

    • You will need to update the tsconfig.json target to es2017 or later for this Boss fight.

npm init -y
npm install typescript --save-dev
npx tsc --init
  1. Create the project structure:

Create a new directory named src inside your project directory. This directory will contain the source code files for your game.

Inside the src directory, create three files: index.html, style.css, and index.ts.

mkdir src
touch src/index.html
touch src/style.css
touch src/index.ts
  • The index.html file will contain the structure and layout of the game's user interface.

  • The style.css file will contain the styles and visual properties for the game's appearance.

  • The index.ts file will contain the game logic written in TypeScript, which will be compiled to JavaScript.

pixel-match-game/
├── src/
│   ├── index.html
│   ├── style.css
│   └── index.ts
├── package.json
└── tsconfig.json

Step 2: Implement the game layout

  1. Open the src/index.html file and copy the provided HTML code:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Pixel Match Game</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div id="container">
    <div id="canvas"></div>
    <div id="targetPattern"></div>
    <div id="controls">
      <button id="startGame">Start Game</button>
      <div id="timer">Time: <span id="timeRemaining">0</span></div>
      <div id="score">Score: <span id="currentScore">0</span></div>
    </div>
  </div>
  <script src="index.js"></script>
</body>
</html>

This HTML code sets up the basic structure for the game, including a canvas for the pixel grid, a targetPattern area to display the target pattern, and controls for starting the game, displaying the timer, and showing the score.

We will not be going step by step into the HTML or CSS code as part of this lesson. So please copy and paste the HTML and CSS code as-is.

  1. Next, open the src/style.css file and copy the provided CSS code:
#container {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: 50px;
}

#canvas {
  display: grid;
  grid-template-columns: repeat(8, 40px);
  grid-template-rows: repeat(8, 40px);
  gap: 2px;
  background-color: #ccc;
  padding: 2px;
}

#canvas button {
  width: 100%;
  height: 100%;
  border: none;
  background-color: #FFFFFF;
}

#targetPattern {
  display: grid;
  grid-template-columns: repeat(8, 20px);
  grid-template-rows: repeat(8, 20px);
  gap: 2px;
  margin-bottom: 20px;
}

#targetPattern div {
  background-color: #ccc;
}

#controls {
  margin-top: 20px;
  text-align: center;
}

#timer, #score {
  margin-top: 10px;
  font-size: 18px;
}

This CSS code styles the game elements, including the container, canvas, target pattern, and controls.

Step 3: Implement the game logic

  1. Open the src/index.ts file:

This code will declare the necessary types, variables, and arrays to store the game state, including pixels, target pattern, remaining time, score, and canvas buttons.

Now, let's go through the rest of the code step by step:

Declare variables

We first must declare a type for our game of type Pixel and also declare our local variables we will be using. They are as follows:

type Pixel = {
    x: number;
    y: number;
    color: string;
  };

  let pixels: Pixel[] = [];
  let targetPattern: Pixel[] = [];
  let timeRemaining = 0;
  let score = 0;
  let isGameRunning = false;
  let startButton: HTMLButtonElement | null = null;
  let canvasButtons: HTMLButtonElement[] = [];

Let's break these variables down further:

We create a TypeScript type called Pixel. As we know a type in TypeScript is a way to define the shape or structure of an object or variable. In this case, the Pixel type is an object type with three properties:

  • x of type number, which represents the column index of the pixel

  • y of type number, which represents the row index of the pixel

  • color of type string, which represents the color of the pixel (usually in hexadecimal format)

By defining this Pixel type, we can create objects that follow this structure and ensure type safety throughout our code.

let pixels: Pixel[] = [];
let targetPattern: Pixel[] = [];

We then declare variables pixels and targetPattern as arrays of type Pixel objects. The type annotations Pixel[] indicate that these variables will hold arrays of objects that conform to the Pixel type we defined earlier.

  • pixels is an empty array that will store the state of all pixels in the game.

  • targetPattern is also an empty array that will store the target pattern the player needs to match.

let timeRemaining = 0;
let score = 0;
let isGameRunning = false;

The above declared variables keep track of the game state:

  • timeRemaining is a number that represents the remaining time in the game, initialized to 0.

  • score is a number that represents the player's score, also initialized to 0.

  • isGameRunning is a boolean that indicates whether the game is currently running or not, initially set to false.

You may have noticed we haven't declared types for these variables. In Typescript we don't always have to declare the type unless we want to enforce type-safety. We could improve the variables type-safety by forcefully setting their types.

let startButton: HTMLButtonElement | null = null;
let canvasButtons: HTMLButtonElement[] = [];

The above declared variables are related to the game's UI elements:

  • startButton is a variable that will hold a reference to the HTML button element used to start the game. Its type is HTMLButtonElement | null, which means it can either be an HTMLButtonElement object or null. It is initialized to null.

  • canvasButtons is an empty array that will store references to the button elements representing each pixel on the canvas. Its type is HTMLButtonElement[], indicating an array of HTMLButtonElement objects.

By declaring these variables and their types, we establish a clear structure for our game data and UI elements. TypeScript's type system will help catch any potential errors where we try to use these variables or objects in an incorrect way.

Create the canvas

The createCanvas function generates the pixel grid dynamically:

function createCanvas(): void {
  const canvasSize = 8;
  const canvas = document.getElementById('canvas');

  for (let y = 0; y < canvasSize; y++) {
    for (let x = 0; x < canvasSize; x++) {
      const pixel: Pixel = { x, y, color: '#FFFFFF' };
      pixels.push(pixel);

      const pixelButton = createPixelButton(pixel);
      canvas?.appendChild(pixelButton);
    }
  }
}

Let's break this function down further:

function createCanvas(): void {
  const canvasSize = 8;
  const canvas = document.getElementById('canvas');

We declare a function called createCanvas that doesn't return any value (indicated by the void return type). The purpose of this function is to set up the canvas and pixel grid of the game to show the player.

Inside the function, we declare a constant canvasSize with a value of 8, representing the size of the pixel grid (8x8). We also retrieve the canvas element from the HTML document using document.getElementById('canvas').

for (let y = 0; y < canvasSize; y++) {
    for (let x = 0; x < canvasSize; x++) {
      const pixel: Pixel = { x, y, color: '#FFFFFF' };
      pixels.push(pixel);

Here, we use nested for loops to iterate over the rows (y) and columns (x) of the pixel grid. For each iteration, we create a Pixel object with the current x and y coordinates and a default color of '#FFFFFF' (white). The Pixel type is defined earlier in the code as:

type Pixel = {
  x: number;
  y: number;
  color: string;
};

This type definition specifies that a Pixel object should have properties x (a number representing the column index), y (a number representing the row index), and color (a string representing the color in hexadecimal format). By creating a Pixel object for each grid cell, we can keep track of its coordinates and color state. The pixel object is then pushed into the pixels array, which stores the state of all pixels in the game.

const pixelButton = createPixelButton(pixel);
      canvas?.appendChild(pixelButton);
    }
  }
}

After creating the Pixel object, we call the createPixelButton function (which we'll explain later) to create a button element for that pixel. The created pixelButton is then appended to the canvas element using canvas?.appendChild(pixelButton). The optional chaining operator ?. is used to check if canvas is not null before accessing its appendChild method.

By using the Pixel type and creating Pixel objects, we ensure that each pixel in the game has a consistent structure with well-defined properties.

This makes it easier to manage and manipulate the pixel data throughout the game logic. Additionally, TypeScript's type system helps catch errors during development if we try to use incorrect types or properties for the Pixel objects.

Create pixel buttons

The createPixelButton function creates a button element for each pixel and adds event listeners for coloring:

function createPixelButton(pixel: Pixel): HTMLButtonElement {
  const pixelButton = document.createElement('button');
  pixelButton.style.backgroundColor = pixel.color;
  pixelButton.dataset.x = pixel.x.toString();
  pixelButton.dataset.y = pixel.y.toString();
  pixelButton.disabled = true;
  pixelButton.addEventListener('click', () => {
    const currentColor = pixel.color;
    const newColor = currentColor === '#FFFFFF' ? '#FF0000' : '#FFFFFF';
    pixel.color = newColor;
    pixelButton.style.backgroundColor = newColor;
    checkPattern();
  });
  canvasButtons.push(pixelButton);
  return pixelButton;
}

Let's break this function down further:

function createPixelButton(pixel: Pixel): HTMLButtonElement {

We declares a function called createPixelButton that takes a Pixel object as a parameter and returns an HTMLButtonElement. The purpose of this function is to create a button element for each pixel in the game's grid.

const pixelButton = document.createElement('button');

We declare a constant called pixelButton which creates a new HTML <button> element.

pixelButton.style.backgroundColor = pixel.color;
pixelButton.dataset.x = pixel.x.toString();
pixelButton.dataset.y = pixel.y.toString();
pixelButton.disabled = true;

We continue by setting the initial properties of the pixelButton object:

  • pixelButton.style.backgroundColor sets the background color of the button to the color of the pixel object.

  • pixelButton.dataset.x and pixelButton.dataset.y set custom data attributes on the button element with the x and y coordinates of the pixel, respectively. These will be used later to identify which pixel the button represents.

  • pixelButton.disabled = true initially disables the button, preventing the player from interacting with it until the game starts.

pixelButton.addEventListener('click', () => {
    const currentColor = pixel.color;
    const newColor = currentColor === '#FFFFFF' ? '#FF0000' : '#FFFFFF';
    pixel.color = newColor;
    pixelButton.style.backgroundColor = newColor;
    checkPattern();
  });

We add a click event listener to the pixelButton object. When the button is clicked, the following code inside the event listener function will be executed:

  • const currentColor = pixel.color stores the current color of the pixel object in the currentColor variable.

  • const newColor = currentColor === '#FFFFFF' ? '#FF0000' : '#FFFFFF' determines the new color for the pixel. If the current color is white (#FFFFFF), the new color will be red (#FF0000); otherwise, the new color will be white (#FFFFFF).

  • pixel.color = newColor updates the color property of the pixel object with the new color.

  • pixelButton.style.backgroundColor = newColor updates the background color of the button to match the new color of the pixel.

  • checkPattern() calls a function (which will be implemented later) to check if the player's pattern matches the target pattern.

canvasButtons.push(pixelButton);
  return pixelButton;
}
  • canvasButtons.push(pixelButton) adds the created pixelButton to the canvasButtons array, which stores all the button elements for the pixels in the game.

  • return pixelButton returns the created pixelButton element.

This function is responsible for creating a button element for each pixel in the game grid, setting its initial properties (color, coordinates, and disabled state), and adding a click event listener to toggle the color of the pixel between red and white. The created button is then added to the canvasButtons array and returned from the function.

Initialize the game

The initializeGame function sets up the initial state of the game:

function initializeGame(): void {
  startButton = document.getElementById('startGame') as HTMLButtonElement | null;
  if (startButton) {
    startButton.addEventListener('click', startGame);
  }
  enableStartButton();
  disableCanvasButtons();
  clearTargetPattern();
}

Let's break the function down further:

function initializeGame(): void {

We declare a function called initializeGame that doesn't return any value (indicated by the void return type). The purpose of this function is to set up the initial state of the game when it first loads.

startButton = document.getElementById('startGame') as HTMLButtonElement | null;

our startButton variable retrieves an HTML element with the ID 'startGame' from the document and assigns it. The as HTMLButtonElement | null part is a type assertion in TypeScript, which tells the compiler that the element retrieved by getElementById can be either an HTMLButtonElement (if the element is found) or null (if the element is not found).

if (startButton) {
    startButton.addEventListener('click', startGame);
  }

This above block of code checks if the startButton variable is not null (i.e., the element with the ID 'startGame' was found). If startButton is not null, it adds a click event listener to the button element. When the button is clicked, the startGame function (which will be implemented later) will be called to start the game.

enableStartButton();
disableCanvasButtons();
clearTargetPattern();
}

We then call separate functions to set up the initial state of the game:

  • enableStartButton() is a function (which will be implemented later) that enables the start button, allowing the player to click it and start the game.

  • disableCanvasButtons() is a function (which will be implemented later) that disables all the buttons representing the pixels on the canvas, preventing the player from interacting with them until the game starts.

  • clearTargetPattern() is a function (which will be implemented later) that clears the display area for the target pattern, ensuring that no pattern is shown initially.

The initializeGame function is responsible for setting up the initial state of the game UI, including retrieving the start button element, adding a click event listener to it, enabling the start button, disabling the canvas buttons, and clearing the target pattern display area.

Start the game

The startGame function initializes the game state, generates the target pattern, and starts the timer:

function startGame(): void {
  if (isGameRunning) return;

  isGameRunning = true;
  timeRemaining = 30;
  score = 0;
  pixels.forEach(pixel => pixel.color = '#FFFFFF');
  targetPattern = generateTargetPattern();
  displayTargetPattern();
  updateTimer();
  updateScore();
  disableStartButton();
  enableCanvasButtons();
}

Let's break down this function:

function startGame(): void {

We declare a function called startGame that doesn't return any value (indicated by the void return type). The purpose of this function is to start the game and set up its initial state.

if (isGameRunning) return;

This if statement checks if the isGameRunning variable is true. If it is, the function immediately returns without executing any further code. This code is to prevent the game from starting multiple times simultaneously.

isGameRunning = true;
timeRemaining = 30;
score = 0;

We then set the initial state for the game:

  • isGameRunning = true sets the isGameRunning variable to true, indicating that the game is now running.

  • timeRemaining = 30 sets the initial value of the timeRemaining variable to 30.

  • score = 0 resets the player's score to 0 at the start of the game.

pixels.forEach(pixel => pixel.color = '#FFFFFF');

This line above iterates over the pixels array (which contains the state of all pixels in the game grid) and sets the color property of each pixel object to '#FFFFFF', which is the hexadecimal code for the color white. This effectively resets the color of all pixels to white at the start of the game.

targetPattern = generateTargetPattern();
  displayTargetPattern();

These lines above are responsible for generating and displaying the target pattern that the player needs to match:

  • targetPattern = generateTargetPattern() calls the generateTargetPattern function (which will be implemented later) and assigns the result to the targetPattern variable.

  • displayTargetPattern() calls a function (which will be implemented later) to render and display the target pattern on the game screen.

updateTimer();
updateScore();
disableStartButton();
enableCanvasButtons();
}

These lines above call separate functions to update the timer and score displays on the game screen:

  • updateTimer() is a function (which will be implemented later) that updates the display for the remaining time in the game.

  • updateScore() is a function (which will be implemented later) that updates the display for the player's current score.

  • disableStartButton() is a function (which will be implemented later) that disables the start button, preventing the player from starting the game again while it's already running.

  • enableCanvasButtons() is a function (which will be implemented later) that enables all the buttons representing the pixels on the canvas, allowing the player to interact with them and start playing the game.

Generate the target pattern

The generateTargetPattern function generates a random target pattern of red pixels:

function generateTargetPattern(): Pixel[] {
  const pattern: Pixel[] = [];
  const patternSize = Math.floor(Math.random() * 3) + 2;

  for (let i = 0; i < patternSize; i++) {
    const x = Math.floor(Math.random() * 8);
    const y = Math.floor(Math.random() * 8);
    const color = '#FF0000';
    pattern.push({ x, y, color });
  }

  return pattern;
}

Let's break down this function:

function generateTargetPattern(): Pixel[] {

We declare a function called generateTargetPattern that returns an array of Pixel objects. The purpose of this function is to generate a random pattern of pixels that the player needs to match in the game.

const pattern: Pixel[] = [];

We declare a constant variable pattern and initializes it as an empty array of Pixel objects. This array will store the generated target pattern.

const patternSize = Math.floor(Math.random() * 3) + 2;

The line above calculates a random pattern size for the target pattern. The Math.random() function generates a random floating-point number between 0 and 1. Multiplying it by 3 gives a random number between 0 and 3. Then, Math.floor rounds down the result to the nearest integer. Finally, adding 2 to the result ensures that the patternSize will be a random integer between 2 and 4 (inclusive).

for (let i = 0; i < patternSize; i++) {
    const x = Math.floor(Math.random() * 8);
    const y = Math.floor(Math.random() * 8);
    const color = '#FF0000';
    pattern.push({ x, y, color });
  }

The for loop iterates patternSize number of times to generate the individual pixels that make up the target pattern:

  • const x = Math.floor(Math.random() * 8) generates a random integer between 0 and 7 (inclusive), representing the column index (x-coordinate) of the pixel.

  • const y = Math.floor(Math.random() * 8) generates a random integer between 0 and 7 (inclusive), representing the row index (y-coordinate) of the pixel.

  • const color = '#FF0000' sets the color of the pixel to the hexadecimal code for the color red (#FF0000).

  • pattern.push({ x, y, color }) creates a new Pixel object with the generated x, y, and color values, and adds it to the pattern array using the push method.

return pattern;
}

Finally, the line above returns the pattern array containing the randomly generated Pixel objects that represent the target pattern for the game.

Display the target pattern

The displayTargetPattern function renders the target pattern on the game screen:

function displayTargetPattern(): void {
  const targetPatternElement = document.getElementById('targetPattern');
  if (targetPatternElement) {
    targetPatternElement.innerHTML = '';
    for (let y = 0; y < 8; y++) {
      for (let x = 0; x < 8; x++) {
        const cell = document.createElement('div');
        const targetPixel = targetPattern.find(pixel => pixel.x === x && pixel.y === y);
        if (targetPixel) {
          cell.style.backgroundColor = targetPixel.color;
        }
        targetPatternElement.appendChild(cell);
      }
    }
  }
}

Let's break down this function:

function displayTargetPattern(): void {

We declare a function called displayTargetPattern that doesn't return any value (indicated by the void return type). The purpose of this function is to render and display the target pattern on the game screen for the player to match.

const targetPatternElement = document.getElementById('targetPattern');

The line above retrieves an HTML element with the ID 'targetPattern' from the document and assigns it to the targetPatternElement variable. This element is a container where the target pattern will be displayed.

if (targetPatternElement) {
    targetPatternElement.innerHTML = '';

This if statement checks if the targetPatternElement variable is not null (i.e., the element with the ID 'targetPattern' was found). If it's not null, the code inside the block will be executed. The line targetPatternElement.innerHTML = '' clears the inner HTML content of the targetPatternElement, effectively removing any previous content from the target pattern display area.

for (let y = 0; y < 8; y++) {
      for (let x = 0; x < 8; x++) {
        const cell = document.createElement('div');
        const targetPixel = targetPattern.find(pixel => pixel.x === x && pixel.y === y);
        if (targetPixel) {
          cell.style.backgroundColor = targetPixel.color;
        }
        targetPatternElement.appendChild(cell);
      }
    }
  }
}

These nested for loops iterate over the rows (y) and columns (x) of an 8x8 grid, representing the pixels in the target pattern:

  • const cell = document.createElement('div') creates a new <div> element for each cell in the grid.

  • const targetPixel = targetPattern.find(pixel => pixel.x === x && pixel.y === y) searches the targetPattern array for a Pixel object with the current x and y coordinates. If a matching Pixel object is found, it is assigned to the targetPixel variable; otherwise, targetPixel will be undefined.

  • if (targetPixel) { cell.style.backgroundColor = targetPixel.color; } checks if targetPixel is not undefined. If it's not, the backgroundColor style of the cell element is set to the color property of the targetPixel object.

  • targetPatternElement.appendChild(cell) appends the cell element as a child of the targetPatternElement, effectively rendering it on the game screen.

The nested loops create a grid of <div> elements, where each element represents a pixel in the target pattern. The background color of each <div> element is set to red (#FF0000) if there is a corresponding Pixel object in the targetPattern array with the same coordinates; otherwise, the background color remains the default color white (#FFFFFF).

Check the pattern

The checkPattern function compares the player's pattern with the target pattern and updates the score if a match is found:

function checkPattern(): void {
  const isMatch = targetPattern.every(targetPixel => {
    const playerPixel = pixels.find(pixel =>
      pixel.x === targetPixel.x && pixel.y === targetPixel.y
    );
    return playerPixel?.color === targetPixel.color;
  });

  if (isMatch) {
    score++;
    targetPattern = generateTargetPattern();
    displayTargetPattern();
    updateScore();
    resetGrid(); // Reset the grid after a correct guess
  }
}

Let's break the function down further:

function checkPattern(): void {

We declare a function called checkPattern that doesn't return any value (indicated by the void return type). The purpose of this function is to check if the player's pattern matches the target pattern and update the game state accordingly.

const isMatch = targetPattern.every(targetPixel => {
    const playerPixel = pixels.find(pixel =>
      pixel.x === targetPixel.x && pixel.y === targetPixel.y
    );
    return playerPixel?.color === targetPixel.color;
  });

The code above declares a constant variable isMatch and assigns it the result of the every method called on the targetPattern array. The every method is a built-in JavaScript array method that tests whether all elements in the array pass a certain condition. In this case, the condition is defined by the arrow function targetPixel => { ... }.

For each targetPixel in the targetPattern array: - const playerPixel = pixels.find(pixel => pixel.x === targetPixel.x && pixel.y === targetPixel.y) searches the pixels array (which contains the player's current pattern) for a Pixel object with the same x and y coordinates as the targetPixel. If a matching Pixel object is found, it is assigned to the playerPixel variable; otherwise, playerPixel will be undefined. - return playerPixel?.color === targetPixel.color checks if the color property of the playerPixel (if it exists, hence the optional chaining ?.) is equal to the color property of the targetPixel. This expression returns true if the colors match, and false otherwise.

The every method will return true if the expression playerPixel?.color === targetPixel.color is true for all targetPixel objects in the targetPattern array. In other words, isMatch will be true if the player's pattern (represented by the pixels array) matches the target pattern exactly.

if (isMatch) {
    score++;
    targetPattern = generateTargetPattern();
    displayTargetPattern();
    updateScore();
    resetGrid(); // Reset the grid after a correct guess
  }

This if statement is executed if isMatch is true, meaning the player's pattern matches the target pattern:

  • score++ increments the player's score by 1.

  • targetPattern = generateTargetPattern() generates a new target pattern by calling the generateTargetPattern function and assigns the result to the targetPattern variable.

  • displayTargetPattern() renders the new target pattern on the game screen by calling the displayTargetPattern function.

  • updateScore() updates the score display on the game screen by calling the updateScore function.

  • resetGrid() resets the game grid to its initial state by calling the resetGrid function. This is done to clear the player's previous pattern and prepare for the next round.

Reset the grid

The resetGrid function resets the pixel colors to white and updates the canvas:

function resetGrid(): void {
  pixels.forEach(pixel => pixel.color = '#FFFFFF');
  updateCanvas();
}

Let's break this function down further:

function resetGrid(): void {

We declare a function called resetGrid that doesn't return any value (indicated by the void return type). The purpose of this function is to reset the game grid to its initial state.

pixels.forEach(pixel => pixel.color = '#FFFFFF');

This code above uses the forEach method to iterate over the pixels array, which contains the state of all pixels in the game grid. For each pixel in the pixels array, the expression pixel.color = '#FFFFFF' is executed. This line sets the color property of the pixel object to the hexadecimal value '#FFFFFF', which represents the color white. By setting the color of all pixels in the pixels array to white, we effectively reset the visual state of the game grid, clearing any previously colored pixels.

updateCanvas();
}

This code above calls the updateCanvas function, which is responsible for updating the visual representation of the game grid on the screen based on the state stored in the pixels array. While the previous line (pixels.forEach(pixel => pixel.color = '#FFFFFF')) updates the pixels array by setting all pixels to white, the updateCanvas function is responsible for reflecting these changes in the actual user interface (UI) by rendering the updated pixel colors on the game grid to the user.

Update the Canvas

The updateCanvas function updates the grid on player click to the colour red.

function updateCanvas(): void {
  pixels.forEach(pixel => {
    const pixelButton = document.querySelector(`#canvas button[data-x="${pixel.x}"][data-y="${pixel.y}"]`) as HTMLButtonElement;
    if (pixelButton) {
      pixelButton.style.backgroundColor = pixel.color;
    }
  });
}

Let's break this function down further:

function updateCanvas(): void {

We declare a function called updateCanvas that doesn't return any value (indicated by the void return type). The purpose of this function is to update the visual representation of the game grid on the screen based on the current state of the pixels array.

pixels.forEach(pixel => {
    const pixelButton = document.querySelector(`#canvas button[data-x="${pixel.x}"][data-y="${pixel.y}"]`) as HTMLButtonElement;

This code above uses the forEach method to iterate over the pixels array, which contains the state of all pixels in the game grid. For each pixel in the pixels array, the following code is executed:

  • const pixelButton = document.querySelector(#canvas button[data-x="${pixel.x}"][data-y="${pixel.y}"]) as HTMLButtonElement; selects an HTML button element from the document that represents the current pixel. The document.querySelector function uses a CSS selector to find the button element with the following criteria:

    • It is a descendant of an element with the ID 'canvas'.

    • It is a <button> element.

    • It has a data-x attribute value that matches the x coordinate of the current pixel.

    • It has a data-y attribute value that matches the y coordinate of the current pixel.

  • The as HTMLButtonElement part is a type assertion in TypeScript, which tells the compiler that the element returned by document.querySelector should be treated as an HTMLButtonElement (a button element).

if (pixelButton) {
      pixelButton.style.backgroundColor = pixel.color;
    }
  });
}

This if statement checks if the pixelButton variable is not null (i.e., a button element was found for the current pixel). If pixelButton is not null, the code inside the block is executed:

  • pixelButton.style.backgroundColor = pixel.color sets the backgroundColor style property of the pixelButton element to the value of the color property of the current pixel object.

By iterating over the pixels array and updating the backgroundColor of the corresponding button elements based on the color property of each pixel, this function updates the visual representation of the game grid on the screen to match the current state stored in the pixels array.

Update the timer

The updateTimer function updates the remaining time and ends the game if the time runs out:

typescriptCopyfunction updateTimer(): void {
  const timerElement = document.getElementById('timeRemaining');
  if (timerElement) {
    timerElement.textContent = timeRemaining.toString();
  }

  if (timeRemaining > 0) {
    timeRemaining--;
    setTimeout(updateTimer, 1000);
  } else {
    endGame();
    isGameRunning = false;
    enableStartButton();
  }
}

Let's break this function down further:

function updateTimer(): void {

We declare a function called updateTimer that doesn't return any value (indicated by the void return type). The purpose of this function is to update the timer display and handle the end of the game when the time runs out.

const timerElement = document.getElementById('timeRemaining');

The code above retrieves an HTML element with the ID 'timeRemaining' from the document and assigns it to the timerElement variable. This element is a container where the remaining time will be displayed.

if (timerElement) {
    timerElement.textContent = timeRemaining.toString();
  }

This if statement checks if the timerElement variable is not null (i.e., the element with the ID 'timeRemaining' was found). If it's not null, the code inside the block is executed:

  • timerElement.textContent = timeRemaining.toString() sets the text content of the timerElement to the string representation of the timeRemaining value. This displays the remaining time on the screen.
if (timeRemaining > 0) {
    timeRemaining--;
    setTimeout(updateTimer, 1000);
  } else {
    endGame();
    isGameRunning = false;
    enableStartButton();
  }

This if...else statement checks the value of the timeRemaining variable:

  • If timeRemaining is greater than 0:

    • timeRemaining-- decrements the timeRemaining value by 1.

    • setTimeout(updateTimer, 1000) sets a timeout to call the updateTimer function again after 1000 milliseconds (1 second). This creates a recurring loop that updates the timer every second.

  • If timeRemaining is 0 or less:

    • endGame() calls the endGame function, which handles the end of the game scenario.

    • isGameRunning = false sets the isGameRunning variable to false, indicating that the game is no longer running.

    • enableStartButton() calls the enableStartButton function, which enables the start button, allowing the player to start a new game.

Update the score

The updateScore function updates the score display:

function updateScore(): void {
  const scoreElement = document.getElementById('currentScore');
  if (scoreElement) {
    scoreElement.textContent = score.toString();
  }
}

Let's break this function down further:

function updateScore(): void {

We declare a function called updateScore that doesn't return any value (indicated by the void return type). The purpose of this function is to update the score display on the game screen.

const scoreElement = document.getElementById('currentScore');

The code above retrieves an HTML element with the ID 'currentScore' from the document and assigns it to the scoreElement variable. This element is a container where the player's current score will be displayed.

if (scoreElement) {
    scoreElement.textContent = score.toString();
  }

This if statement checks if the scoreElement variable is not null (i.e., the element with the ID 'currentScore' was found). If it's not null, the code inside the block is executed:

  • scoreElement.textContent = score.toString() sets the text content of the scoreElement to the string representation of the score value. This displays the player's current score on the screen.

Clear the target pattern

The clearTargetPattern function clears the target pattern display:

function clearTargetPattern(): void {
  const targetPatternElement = document.getElementById('targetPattern');
  if (targetPatternElement) {
    targetPatternElement.innerHTML = '';
  }
}

Let's break this function down further:

function clearTargetPattern(): void {

We declare a function called clearTargetPattern that doesn't return any value (indicated by the void return type). The purpose of this function is to clear the target pattern display area on the game screen.

const targetPatternElement = document.getElementById('targetPattern');

The code above retrieves an HTML element with the ID 'targetPattern' from the document and assigns it to the targetPatternElement variable. This element is a container where the target pattern is displayed on the game screen.

if (targetPatternElement) {
    targetPatternElement.innerHTML = '';
  }

This if statement checks if the targetPatternElement variable is not null (i.e., the element with the ID 'targetPattern' was found). If it's not null, the code inside the block is executed:

  • targetPatternElement.innerHTML = '' sets the inner HTML content of the targetPatternElement to an empty string. This effectively removes any existing content (e.g., HTML elements representing the target pattern) from the target pattern display area.

Reset the game

The resetGame function resets the game state to its initial values:

function resetGame(): void {
  resetGrid();
  targetPattern = [];
  score = 0;
  timeRemaining = 30;
  updateScore();
  clearTargetPattern();
  enableStartButton();
  disableCanvasButtons();
}

Let's break this function down further:

function resetGame(): void {

We declare a function called resetGame that doesn't return any value (indicated by the void return type). The purpose of this function is to reset the game state to its initial conditions, effectively starting a new game.

resetGrid();

The code above calls the resetGrid function, which is responsible for resetting the game grid to its initial state. This involves clearing all colored pixels and resetting the colors to the default value (e.g., white).

targetPattern = [];

This code above resets the targetPattern array to an empty array ([]). The targetPattern array stores the target pattern that the player needs to match in the game.

score = 0;

This code above resets the score variable to 0. The score variable keeps track of the player's score in the game.

timeRemaining = 30;

This code above sets the timeRemaining variable to 30. The timeRemaining variable represents the time remaining for the player to complete the game.

updateScore();
clearTargetPattern();
enableStartButton();
disableCanvasButtons();
}

The code above does the following:

  • Calls updateScore function to update the score display on the game screen to 0.

  • Calls clearTargetPattern function to clear the target pattern display area on the game screen.

  • Calls enableStartButton function to enable the start button, allowing the player to start a new game.

  • Calls disableCanvasButtons function to disable the input controls or buttons on the game canvas or grid.

End the game

The endGame function handles the end of the game, displaying the final score and allowing the player to play again:

function endGame(): void {
  const playAgain = confirm(`Game Over! Your score: ${score}\nDo you want to play again?`);
  if (playAgain) {
    resetGame();
  } else {
    isGameRunning = false;
    enableStartButton();
    disableCanvasButtons();
    clearTargetPattern();
  }
}

Let's break this function down further:

function endGame(): void {

We declare a function called endGame that doesn't return any value (indicated by the void return type). The purpose of this function is to handle the end of the game scenario, displaying the final score and providing an option for the player to start a new game or exit.

const playAgain = confirm(`Game Over! Your score: ${score}\nDo you want to play again?`);

This code above displays a modal dialog box with the message "Game Over! Your score: [current score value]" followed by a newline character \n and the question "Do you want to play again?". The confirm function returns a boolean value (true if the user clicks "OK" or false if the user clicks "Cancel"). The returned value is stored in the playAgain constant.

if (playAgain) {
    resetGame();
  } else {
    isGameRunning = false;
    enableStartButton();
    disableCanvasButtons();
    clearTargetPattern();
  }
}

The if...else statement checks the value of the playAgain constant:

  • If playAgain is true (i.e., the user clicked "OK" in the dialog box):

    • resetGame() is called, which resets the game state and starts a new game.
  • If playAgain is false (i.e., the user clicked "Cancel" in the dialog box):

    • isGameRunning = false sets the isGameRunning variable to false, indicating that the game is no longer running.

    • enableStartButton() is called, which enables the start button, allowing the player to start a new game if they change their mind.

    • disableCanvasButtons() is called, which disables the buttons or input controls on the game canvas or grid, preventing further interaction.

    • clearTargetPattern() is called, which clears the target pattern display area on the game screen.

Enable/disable buttons

These functions handle enabling and disabling the start and canvas buttons, this is to stop the player clicking on the canvas or the start button multiple times before the game starts:

function disableStartButton(): void {
  startButton = document.getElementById('startGame') as HTMLButtonElement | null;
  if (startButton) {
    startButton.disabled = true;
  }
}

function enableStartButton(): void {
  if (startButton) {
    startButton.disabled = false;
  }
}

function disableCanvasButtons(): void {
  canvasButtons.forEach(button => button.disabled = true);
}

function enableCanvasButtons(): void {
  canvasButtons.forEach(button => button.disabled = false);
}

Let's break down these functions further:

function disableStartButton(): void {
  startButton = document.getElementById('startGame') as HTMLButtonElement | null;
  if (startButton) {
    startButton.disabled = true;
  }
}

This function is responsible for disabling the start button on the game screen:

  • startButton = document.getElementById('startGame') as HTMLButtonElement | null; retrieves the HTML element with the ID 'startGame' from the document and assigns it to the startButton variable. The as HTMLButtonElement | null is a type assertion in TypeScript, indicating that the element can be either an HTMLButtonElement (a button element) or null (if the element is not found).

  • if (startButton) { startButton.disabled = true; } checks if the startButton variable is not null. If it's not null, it sets the disabled property of the button element to true, effectively disabling the button and preventing user interaction.

function enableStartButton(): void {
  if (startButton) {
    startButton.disabled = false;
  }
}

This function is responsible for enabling the start button on the game screen:

  • if (startButton) { startButton.disabled = false; } checks if the startButton variable is not null. If it's not null, it sets the disabled property of the button element to false, effectively enabling the button and allowing user interaction.
function disableCanvasButtons(): void {
  canvasButtons.forEach(button => button.disabled = true);
}

This function is responsible for disabling the buttons or input controls on the game canvas or grid:

  • canvasButtons.forEach(button => button.disabled = true); iterates over the canvasButtons array, which contains references to the button elements representing each pixel on the game canvas. For each button element in the array, it sets the disabled property to true, effectively disabling the button and preventing user interaction.
function enableCanvasButtons(): void {
  canvasButtons.forEach(button => button.disabled = false);
}

This function is responsible for enabling the buttons or input controls on the game canvas or grid:

  • canvasButtons.forEach(button => button.disabled = false); iterates over the canvasButtons array, which contains references to the button elements representing each pixel on the game canvas. For each button element in the array, it sets the disabled property to false, effectively enabling the button and allowing user interaction.

Initialize the game

Finally, the createCanvas function is called to generate the initial pixel grid, and the initializeGame function is called to set up the game:

createCanvas();
initializeGame();

startButton = document.getElementById('startGame') as HTMLButtonElement | null;
if (startButton) {
  startButton.addEventListener('click', startGame);
}

Let's break this last bit of the code down further:

createCanvas();
initializeGame();

The code above calls the createCanvas and initializeGame functions, respectively.

  • createCanvas() is responsible for generating the pixel grid on the game screen. It creates button elements for each pixel and appends them to the canvas container.

  • initializeGame() is responsible for setting up the initial state of the game. This includes retrieving the start button element, adding an event listener to it, enabling/disabling buttons, and clearing the target pattern display area.

startButton = document.getElementById('startGame') as HTMLButtonElement | null;

This code above retrieves the HTML element with the ID 'startGame' from the document and assigns it to the startButton variable. The as HTMLButtonElement | null is a type assertion in TypeScript, indicating that the element can be either an HTMLButtonElement (a button element) or null (if the element is not found).

if (startButton) {
  startButton.addEventListener('click', startGame);
}

This if statement checks if the startButton variable is not null. If it's not null, it means the start button element was found on the page. In that case, the code inside the block is executed:

  • startButton.addEventListener('click', startGame) adds a click event listener to the start button. When the button is clicked, the startGame function will be called. The startGame function is responsible for starting the game and setting up the necessary game state and logic.

By adding the event listener to the start button, the player can initiate the game by clicking the button. The startGame function will be executed when the button is clicked, kicking off the game logic and updating the game state accordingly.

Complete Typescript Code for src/index.ts

type Pixel = {
    x: number;
    y: number;
    color: string;
  };

  let pixels: Pixel[] = [];
  let targetPattern: Pixel[] = [];
  let timeRemaining = 0;
  let score = 0;
  let isGameRunning = false;
  let startButton: HTMLButtonElement | null = null;
  let canvasButtons: HTMLButtonElement[] = [];

  function createCanvas(): void {
    const canvasSize = 8;
    const canvas = document.getElementById('canvas');

    for (let y = 0; y < canvasSize; y++) {
      for (let x = 0; x < canvasSize; x++) {
        const pixel: Pixel = { x, y, color: '#FFFFFF' };
        pixels.push(pixel);

        const pixelButton = createPixelButton(pixel);
        canvas?.appendChild(pixelButton);
      }
    }
  }

  function createPixelButton(pixel: Pixel): HTMLButtonElement {
    const pixelButton = document.createElement('button');
    pixelButton.style.backgroundColor = pixel.color;
    pixelButton.dataset.x = pixel.x.toString();
    pixelButton.dataset.y = pixel.y.toString();
    pixelButton.disabled = true;
    pixelButton.addEventListener('click', () => {
      const currentColor = pixel.color;
      const newColor = currentColor === '#FFFFFF' ? '#FF0000' : '#FFFFFF';
      pixel.color = newColor;
      pixelButton.style.backgroundColor = newColor;
      checkPattern();
    });
    canvasButtons.push(pixelButton);
    return pixelButton;
  }

  function initializeGame(): void {
    startButton = document.getElementById('startGame') as HTMLButtonElement | null;
    if (startButton) {
      startButton.addEventListener('click', startGame);
    }
    enableStartButton();
    disableCanvasButtons();
    clearTargetPattern();
  }

  function startGame(): void {
    if (isGameRunning) return;

    isGameRunning = true;
    timeRemaining = 30;
    score = 0;
    pixels.forEach(pixel => pixel.color = '#FFFFFF');
    targetPattern = generateTargetPattern();
    displayTargetPattern();
    updateTimer();
    updateScore();
    disableStartButton();
    enableCanvasButtons();
  }

  function generateTargetPattern(): Pixel[] {
    const pattern: Pixel[] = [];
    const patternSize = Math.floor(Math.random() * 3) + 2;

    for (let i = 0; i < patternSize; i++) {
      const x = Math.floor(Math.random() * 8);
      const y = Math.floor(Math.random() * 8);
      const color = '#FF0000';
      pattern.push({ x, y, color });
    }

    return pattern;
  }

  function displayTargetPattern(): void {
    const targetPatternElement = document.getElementById('targetPattern');
    if (targetPatternElement) {
      targetPatternElement.innerHTML = '';
      for (let y = 0; y < 8; y++) {
        for (let x = 0; x < 8; x++) {
          const cell = document.createElement('div');
          const targetPixel = targetPattern.find(pixel => pixel.x === x && pixel.y === y);
          if (targetPixel) {
            cell.style.backgroundColor = targetPixel.color;
          }
          targetPatternElement.appendChild(cell);
        }
      }
    }
  }

  function checkPattern(): void {
    const isMatch = targetPattern.every(targetPixel => {
      const playerPixel = pixels.find(pixel =>
        pixel.x === targetPixel.x && pixel.y === targetPixel.y
      );
      return playerPixel?.color === targetPixel.color;
    });

    if (isMatch) {
      score++;
      targetPattern = generateTargetPattern();
      displayTargetPattern();
      updateScore();
      resetGrid(); // Reset the grid after a correct guess
    }
  }

  function resetGrid(): void {
    pixels.forEach(pixel => pixel.color = '#FFFFFF');
    updateCanvas();
  }

  function updateCanvas(): void {
    pixels.forEach(pixel => {
      const pixelButton = document.querySelector(`#canvas button[data-x="${pixel.x}"][data-y="${pixel.y}"]`) as HTMLButtonElement;
      if (pixelButton) {
        pixelButton.style.backgroundColor = pixel.color;
      }
    });
  }

  function updateTimer(): void {
    const timerElement = document.getElementById('timeRemaining');
    if (timerElement) {
      timerElement.textContent = timeRemaining.toString();
    }

    if (timeRemaining > 0) {
      timeRemaining--;
      setTimeout(updateTimer, 1000);
    } else {
      endGame();
      isGameRunning = false;
      enableStartButton();
    }
  }

  function updateScore(): void {
    const scoreElement = document.getElementById('currentScore');
    if (scoreElement) {
      scoreElement.textContent = score.toString();
    }
  }

  function clearTargetPattern(): void {
    const targetPatternElement = document.getElementById('targetPattern');
    if (targetPatternElement) {
      targetPatternElement.innerHTML = '';
    }
  }

  function resetGame(): void {
    resetGrid();
    targetPattern = [];
    score = 0;
    timeRemaining = 30;
    updateScore();
    clearTargetPattern();
    enableStartButton();
    disableCanvasButtons();
  }

  function endGame(): void {
    const playAgain = confirm(`Game Over! Your score: ${score}\nDo you want to play again?`);
    if (playAgain) {
      resetGame();
    } else {
      isGameRunning = false;
      enableStartButton();
      disableCanvasButtons();
      clearTargetPattern();
    }
  }

  function disableStartButton(): void {
    startButton = document.getElementById('startGame') as HTMLButtonElement | null;
    if (startButton) {
      startButton.disabled = true;
    }
  }

  function enableStartButton(): void {
    if (startButton) {
      startButton.disabled = false;
    }
  }

  function disableCanvasButtons(): void {
    canvasButtons.forEach(button => button.disabled = true);
  }

  function enableCanvasButtons(): void {
    canvasButtons.forEach(button => button.disabled = false);
  }

  createCanvas();
  initializeGame();

  startButton = document.getElementById('startGame') as HTMLButtonElement | null;
  if (startButton) {
    startButton.addEventListener('click', startGame);
  }

Step 4: Build and run the game

  1. Open a terminal or command prompt and navigate to the project directory.

  2. Run the following command npx tsc --noEmitOnError to compile the TypeScript code:

npx tsc --noEmitOnError
  • This command compiles the TypeScript code in the src/index.ts file and generates the corresponding JavaScript code in the src/index.js file. We use the --noEmitOnError flag to stop it generating a index.js file if there are any errors.

After compiling the TypeScript code, you can open the index.html file in a web browser to play the game. The game will load, and you will see the pixel grid.

  • Click the Start Game button to begin playing. This will show you the target grid with the pattern you need to match.

  • Try to match the target pattern by clicking on the pixels to color them.

  • The game will end when the time runs out or when you have matched all the patterns.

  • Your final score will be displayed, and you will have the option to play again.

The Challenge Continues

Congratulations! You've created a functional Pixel Match Game using TypeScript. But the challenge doesn't end here. Here are some ways you can extend the game:

1. Add difficulty levels that change the grid size or time limit

2. Implement a high score system using local storage

3. Add sound effects for successful matches and game over

Wrapping Up!

Congratulations, brave coder! You've conquered the second boss by creating a fully functional Pixel Match Game. Let's reflect on what you've accomplished:

1. You've used TypeScript's type system to create a Pixel type, ensuring type safety throughout your code.

  • You've implemented complex game logic using functions, demonstrating your mastery of TypeScript's function syntax.

  • You've interacted with the DOM using TypeScript, showing how TypeScript can enhance web development.

Remember, each challenge you overcome makes you stronger. Keep practicing, keep coding, and get ready for the next level of your TypeScript adventure!

Happy Coding!