TypeScript Adventure: Conquering the Second Boss
Upgrade your tools for the second boss
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
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
- 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 namedtsconfig.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
- 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
- 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.
- 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
- 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 typenumber
, which represents the column index of the pixely
of typenumber
, which represents the row index of the pixelcolor
of typestring
, 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 emptyarray
that will store the state of all pixels in the game.targetPattern
is also an emptyarray
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 anumber
that represents the remaining time in the game, initialized to0
.score
is anumber
that represents the player's score, also initialized to0
.isGameRunning
is aboolean
that indicates whether the game is currently running or not, initially set tofalse
.
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 isHTMLButtonElement | null
, which means it can either be anHTMLButtonElement
object ornull
. It is initialized tonull
.canvasButtons
is an emptyarray
that will store references to the button elements representing each pixel on the canvas. Its type isHTMLButtonElement[]
, 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
andpixelButton.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 thepixel
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 theisGameRunning
variable totrue
, indicating that the game is now running.timeRemaining = 30
sets the initial value of thetimeRemaining
variable to30
.score = 0
resets the player's score to0
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 thegenerateTargetPattern
function (which will be implemented later) and assigns the result to thetargetPattern
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 between0
and7
(inclusive), representing the column index (x-coordinate) of the pixel.const y = Math.floor(Math.random() * 8)
generates a random integer between0
and7
(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 newPixel
object with the generatedx
,y
, and color values, and adds it to thepattern
array using thepush
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 thetargetPattern
array for aPixel
object with the currentx
andy
coordinates. If a matchingPixel
object is found, it is assigned to thetargetPixel
variable; otherwise,targetPixel
will beundefined
.if (targetPixel) {
cell.style
.backgroundColor = targetPixel.color; }
checks iftargetPixel
is notundefined
. If it's not, thebackgroundColor
style of the cell element is set to the color property of thetargetPixel
object.targetPatternElement.appendChild(cell)
appends the cell element as a child of thetargetPatternElement
, 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 by1
.targetPattern = generateTargetPattern()
generates a new target pattern by calling thegenerateTargetPattern
function and assigns the result to thetargetPattern
variable.displayTargetPattern()
renders the new target pattern on the game screen by calling thedisplayTargetPattern
function.updateScore()
updates the score display on the game screen by calling theupdateScore
function.resetGrid()
resets the game grid to its initial state by calling theresetGrid
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. Thedocument.querySelector
function uses aCSS
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 thex
coordinate of the current pixel.It has a
data-y
attribute value that matches they
coordinate of the current pixel.
The
as HTMLButtonElement
part is atype assertion
in TypeScript, which tells the compiler that the element returned bydocument.querySelector
should be treated as anHTMLButtonElement
(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 thebackgroundColor
style property of thepixelButton
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 thetimerElement
to the string representation of thetimeRemaining
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 by1
.setTimeout(updateTimer, 1000)
sets a timeout to call theupdateTimer
function again after1000
milliseconds (1 second). This creates a recurring loop that updates the timer every second.
If
timeRemaining
is0
or less:endGame()
calls theendGame
function, which handles the end of the game scenario.isGameRunning = false
sets theisGameRunning
variable tofalse
, indicating that the game is no longer running.enableStartButton()
calls theenableStartButton
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 thescoreElement
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 thetargetPatternElement
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 to0
.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
istrue
(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
isfalse
(i.e., the user clicked"Cancel"
in the dialog box):isGameRunning = false
sets theisGameRunning
variable tofalse
, 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 thestartButton
variable. Theas HTMLButtonElement | null
is atype assertion
in TypeScript, indicating that the element can be either anHTMLButtonElement
(a button element) ornull
(if the element is not found).if (startButton) { startButton.disabled = true; }
checks if thestartButton
variable is not null. If it's not null, it sets the disabled property of the button element totrue
, 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 thestartButton
variable is not null. If it's not null, it sets the disabled property of the button element tofalse
, 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 thecanvasButtons
array, which contains references to the button elements representing each pixel on the game canvas. For eachbutton
element in the array, it sets the disabled property totrue
, effectivelydisabling
the button andpreventing
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 thecanvasButtons
array, which contains references to the button elements representing each pixel on the game canvas. For eachbutton
element in the array, it sets the disabled property tofalse
, effectivelyenabling
the button andallowing
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 createsbutton
elements for each pixel and appends them to the canvascontainer
.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, thestartGame
function will be called. ThestartGame
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
Open a terminal or command prompt and navigate to the project directory.
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 thesrc/index.js
file. We use the--noEmitOnError
flag to stop it generating aindex.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!