Frogger in JavaScript
Let’s build a game of Frogger in HTML, CSS and JavaScript. Here’s a snapshot of part of what the classic game looked like. There is a frog on a blue sidewalk near the bottom. There’s a road above that, with five lanes in it. Each lane has a vehicles at different speeds traveling in alternating directions right or left. Then, there’s another sidewalk at the top. There was a mote beyond this, but I have removed it to keep it simple.
I’ll try to copy the sprites movements as closely as I can. Here’s what I could gather:
- The lane below the truck is least occupied, but the vehicle in it is the fastest.
- The second fastest, I think are the truck and the racing car in the second lane from the bottom.
- The first and third lanes from the bottom appear to have the same speed.
- The first three lanes from the bottom each have a bunch of three equidistant vehicles, followed by a bigger space, then another bunch of three equidistant vehicles.
Creating the board and the sprites
I’ll consider the game grid to be composed of many rectangles 20px wide by 20px high. Each of our shapes will be a multiple of these. Let’s start with the truck. I want the truck to be a bit bigger than the other shapes. I’m creating it out of three segments, two for the cargo, and one for the cabin.
<!DOCTYPE html>
<html>
<head>
<title>Frogger</title>
<link rel="stylesheet" href="style.css"/>
</head>
<body>
<div id="container">
<div class="cargo"></div>
<div class="cargo"></div>
<div class="cabin"></div>
</div>
<script type="text/javascript" src="app.js"></script>
</body>
</html>
Here are the CSS style definitions for the cargo and cabin:
#container {
border: 1px solid black;
width: 900px;
height: 200px;
margin: 0 auto;
overflow: hidden;
position: relative;
display: flex;
flex-wrap: wrap;
}
.cabin {
width: 20px;
height: 20px;
background-color: #888;
border-radius: 5px;
}
.cargo {
width: 20px;
height: 20px;
background-color: #aaa;
}
I’ve used CSS flex and wrap setting on the container so that the div elements placed inside it are nicely aligned and wrap over to the next row, once one is filled.
Let’s render a car as well. The car will be composed of two segments, the front and the back, so it’ll be 40 px wide by 20 px high. Here’s the updated HTML:
<!DOCTYPE html>
<html>
<head>
<title>Frogger</title>
<link rel="stylesheet" href="style.css"/>
</head>
<body>
<div id="container">
<div id="frog"></div>
<div class="car-back"></div>
<div class="car-front"></div>
<div class="cargo"></div>
<div class="cargo"></div>
<div class="cabin"></div>
</div>
</div>
<script type="text/javascript" src="app.js"></script>
</body>
</html>
The CSS styling for the car front and back are as follows:
.car-front {
width: 20px;
height: 20px;
background-color: #333;
position: relative;
border-radius: 0 5px 5px 0;
}
.car-back {
width: 20px;
height: 20px;
background-color: #333;
position: relative;
border-radius: 5px 0 0 5px;
}
At this point, the car is just a dark 40 x 40 rectangle with rounded edges. You can’t tell which side is the front and which is the back. Let’s add a little bit of detail to it, in the form of what might look like the windshields.
.car-front {
width: 20px;
height: 20px;
background-color: #333;
position: relative;
border-radius: 0 5px 5px 0;
}
.car-front::before {
content: '';
width: 5px;
height: 15px;
background-color: gray;
position: absolute;
top: 2px;
left: 5px;
}
.car-back {
width: 20px;
height: 20px;
background-color: #333;
position: relative;
border-radius: 5px 0 0 5px;
}
.car-back::before {
content: '';
width: 5px;
height: 15px;
background-color: gray;
position: absolute;
top: 2px;
left: 5px;
}
At the front, I’ve created a pseudo element using ::before
. That element’s content is the empty string. It has a gray background color, and is 5 px wide, and 15 px high. I kept the height less than the height of the car. I’ve used position: absolute;
, left: 5px;
, and top: 2px;
so that it appears 5 pixels from the left edge of its parent element (i.e., the div with classcar-front
). Now, the shape will look a bit more like the top view of a sedan.
Here’s what the page should look like at the moment:
Let’s do the frog now. We’ll create the following style for the frog:
.frog {
width: 20px;
height: 20px;
background-color: green;
border-radius: 50%;
position: relative;
overflow: hidden; /* To hide overflowed content */
}
This will created a circular shape with a green fill. So, if we do a <div class=”frog”></div>
, we’ll see it on the screen. Let’s give it a couple of eyes: one using the ::before
pseudo element and the other using the ::after
pseudo element.
.frog {
width: 20px;
height: 20px;
background-color: green;
border-radius: 50%;
position: relative;
overflow: hidden; /* To hide overflowed content */
}
.frog::before {
content: '';
width: 4px;
height: 4px;
background-color: white;
border-radius: 50%;
position: absolute;
top: 25%;
left: 20%;
}
.frog::after {
content: '';
width: 4px;
height: 4px;
background-color: white;
border-radius: 50%;
position: absolute;
top: 25%;
left: 60%;
}
The eyes are 4 x 4 pixel circular shapes with white backgrounds. Both are set 25% from the top edge of the parent element. One is set 25% from the left edge, while the other is set at 60%. I think it looks reasonable at this point.
Creating the game layout
Let’s now set up the arena: the row where the frog starts, the row where it wants to reach, one row in the middle where it can rest for a bit, the road with multiple lanes and the moat. We’ll set up the arena as a 2D grid, with each cell being a div
element. Rather than hard-coding so many div
elements, we’ll write JavaScript code to add the div
elements to the page. Then, we’ll apply styles for the cars, the trucks etc.
const rows = 10
const columns = 45
const container = document.getElementById('container')
let frogLocation = (rows - 1) * columns + Math.floor(columns / 2)
function initialLayout() {
for (let i = 0 ; i < rows ; i++) {
for (let j = 0 ; j < columns ; j++) {
const elem = document.createElement('div')
container.appendChild(elem)
}
}
const divs = container.children
// Make the top row grass
for (let i = 0 ; i < columns ; i++) {
divs[i].classList.add('grass')
}
// Make the bottom row grass
for (let i = 0 ; i < columns ; i++) {
divs[(rows - 1) * columns + i].classList.add('grass')
}
// Show the frog
divs[frogLocation].classList.add('frog')
}
initialLayout()
First, we’ve defined variables to hold the number of rows and columns on the screen. Notice that I had set up the container
element to be 820 px wide, which will accommodate 45 div
elements, each with a width of 20 px. That’s an odd number of div
elements, so that we can position the frog smack in the middle, horizontally.
Next, we obtain a reference to the div
element with an id
of container
. We calculate where we would position the frog initially. To figure it out, we’ve done a calculation. We skip over all rows, except one, which means (rows — 1)*columns
div elements. Then, we skip another column/2
elements.
Next, we’ve run a for
loop for every row, and a nested for
loop for every column. In each iteration, we create a div
element and add it to the container
element.
We obtain the div
elements that we just created, using the children
property of the container
element. Then, we applied a class of grass
to the top row, and the bottom row. Finally, in order to show the frog, we referenced the div
element at the centre of the bottom row, removed grass
from its classList
, and added frog
to its classList
. This should result in what you see below, except the frog’s colour will resemble that of the grass:
The reason is that the grass
class property of background-color
is overriding that of the class frog
. In order to force the frog
class background colour to take precedence, we can defined a more specific selector for frog
like so:
div.frog {
background-color: green;
}
.frog {
width: 20px;
height: 20px;
border-radius: 50%;
position: relative;
overflow: hidden; /* To hide overflowed content */
z-index:1000;
}
.frog::before {
content: '';
width: 4px;
height: 4px;
background-color: white;
border-radius: 50%;
position: absolute;
top: 25%;
left: 20%;
}
.frog::after {
content: '';
width: 4px;
height: 4px;
background-color: white;
border-radius: 50%;
position: absolute;
top: 25%;
left: 60%;
}
We took out the background-color property from the definition of .frog, and moved it to the definition of div.frog. That should do the trick and get the frog in the foreground, so to speak.
We’ll do a similar thing for the car, and truck as well, resulting in:
div.car-front, div.car-back {
background-color: #333;
}
.car-front {
width: 20px;
height: 20px;
background-color: #333;
position: relative;
border-radius: 0 5px 5px 0;
}
.car-front::before {
content: '';
width: 5px;
height: 15px;
background-color: gray;
position: absolute;
top: 2px;
left: 5px;
}
.car-back {
width: 20px;
height: 20px;
background-color: #333;
position: relative;
border-radius: 5px 0 0 5px;
}
.car-back::before {
content: '';
width: 5px;
height: 15px;
background-color: gray;
position: absolute;
top: 2px;
left: 5px;
}
div.cabin {
background-color: #888;
}
.cabin {
width: 20px;
height: 20px;
border-radius: 5px;
}
div.cargo {
background-color: #aaa;
}
.cargo {
width: 20px;
height: 20px;
}
Back to setting up the game board. We’ll apply a class of water
to three rows after the top (grass) row. We’ll also apply a class of grass
to the row right after that — this is where the forg can take a breather after crossing “the road”.
We’ll add the following lines to initialLayout()
.
// Rows 2, 3, and 4 are water
for (let i = 0 ; i < 3 ; i++) {
for (let j = 0 ; j < columns ; j++) {
divs[(i + 1) * columns + j].classList.add('water')
}
}
// Row 5 is grass
for (let i = 0 ; i < columns ; i++) {
divs[4 * columns + i].classList.add('grass')
}
// Rows 6, 7, 8, and 9 are road
for (let i = 0 ; i < 4 ; i++) {
for (let j = 0 ; j < columns ; j++) {
divs[(i + 5) * columns + j].classList.add('road')
}
}
Along with this, the style for road
, is set as:
.road {
background-color: #555555;
width:20px;
height: 20px;
}
This results in the following:
At this point, I refactored the code a little bit to the following — factoring out the code to apply a certain style to one or more consecutive rows:
const rows = 10
const columns = 45
const container = document.getElementById('container')
let frogLocation = (rows - 1) * columns + Math.floor(columns / 2)
console.log(frogLocation)
function initialLayout() {
for (let i = 0 ; i < rows ; i++) {
for (let j = 0 ; j < columns ; j++) {
const elem = document.createElement('div')
container.appendChild(elem)
}
}
// Make the top row grass
setUpRows(0, 0, 'grass')
// Make the bottom row grass
setUpRows(9, 9, 'grass')
// Show the frog
const divs = container.children
divs[frogLocation].classList.remove('grass')
divs[frogLocation].classList.add('frog')
// Rows 2, 3, and 4 are water
setUpRows(1, 3, 'water')
// Row 5 is grass
setUpRows(4, 4, 'grass')
// Rows 6, 7, 8, and 9 are road
setUpRows(5, 8, 'road')
}
function setUpRows(startingRow, endingRow, style) {
const divs = container.children
for (let i = startingRow ; i <= endingRow ; i++) {
for (let j = 0 ; j < columns ; j++) {
divs[i*columns + j].classList.add(style)
}
}
}
initialLayout()
Now, let’s show some cars in the row above the frog:
function showCarRow1() {
const divs = container.children
let car1Rear = 8*columns
let car2Rear = car1Rear + 5
for (let i = 0 ; i < 3 ; i++) {
divs[car1Rear].classList.remove('road')
divs[car1Rear + 1].classList.remove('road')
divs[car1Rear + 1].classList.add('car-front')
divs[car1Rear].classList.add('car-back')
divs[car2Rear].classList.remove('road')
divs[car2Rear + 1].classList.remove('road')
divs[car2Rear + 1].classList.add('car-front')
divs[car2Rear].classList.add('car-back')
car1Rear += 13
car2Rear = car1Rear + 5
}
}
showCarRow1()
I’m going to make the cars travel in pairs. The distance between cars within a pair will be five blocks (of 20 x 20 px). The total number of div
elements in a row is 45. Since there are a total of 6 cars in a row, with two div
elements per car, we have 45–12 = 33 div
elements left. We need to evenly use the remaining space in a row for the space between the cars within a pair, and the space between two pairs of cars. Suppose, we leave 5 div
elements between the two cars within a pair, we are left with 33–15 = 18 div
elements. This space needs to be evenly divided as space between each consecutive pair of cars, which amounts to 6 div
elements between each pair.
Let’s call the two cars in a pair car1, and car2, with car1 being behind car2. I’ve initialised car1’s rear to be at the left of the road, i.e., 8*columns
. Since car2 is supposed to be five blocks ahead, I’ve initialised it’s rear at car1Rear + 5
. By some hit and trial, I figured that we can accommodate three pairs of cars in the lane, so I ran a loop three times. In each iteration, I remove the road
style from the div
elements at index car1Rear1
, and car1Rear+1
as well as car2Rear
, and car2Rear+1
. Then, I added the car-front
, and car-back
classes to the respective div
elements. Then, for the next pair of cars, I incremented car1Rear
and car2Rear
. Now, you should see the following:
An improvement to the car display is in order. I’d rather hold the car’s location in a variable and have one function focus on just displaying the cars, while another function would be focused on updating the car’s position (later). That is, one focused on display, the other on animation.
You can define an array to hold all the car’s positions. Or, you could create one variable to store one car’s position and calculate all the other car’s positions based on that. I went with a middle-ground. I created an array to hold the positions of one pair of cars. Here’s the code:
const rows = 10
const columns = 45
const container = document.getElementById('container')
let carRow1 = []
let carRow2 = []
function displayCars() {
carRow1.forEach( c => displayCarRight(c))
carRow2.forEach( c => displayCarRight(c))
}
function displayCarRight(rear) {
const divs = container.children
const rowStart = getRowIndex(rear)
divs[rear].className = 'car-back ' + divs[rear].className
divs[rowStart + rear + 1].className = 'car-front ' + divs[rowStart + rear + 1].className
}
function setUpCarRows() {
setUpCarRow1()
setUpCarRow2()
}
function setUpCarRow1() {
let car1Rear = 8*columns
for (let i = 0 ; i < 3 ; i ++) {
carRow1 = [...carRow1, car1Rear, car1Rear + 5]
car1Rear += 13
}
}
function setUpCarRow2() {
let car1Rear = 8*columns - 2
for (let i = 0 ; i < 3 ; i ++) {
carRow2 = [...carRow2, car1Rear, car1Rear - 5]
car1Rear -= 13
}
}
function initialLayout() {
for (let i = 0 ; i < rows ; i++) {
for (let j = 0 ; j < columns ; j++) {
const elem = document.createElement('div')
container.appendChild(elem)
}
}
// Make the top row grass
setUpRows(0, 0, 'grass')
// Make the bottom row grass
setUpRows(9, 9, 'grass')
// Show the frog
const divs = container.children
divs[frogLocation].classList.remove('grass')
divs[frogLocation].classList.add('frog')
// Rows 2, 3, and 4 are water
setUpRows(1, 3, 'water')
// Row 5 is grass
setUpRows(4, 4, 'grass')
// Rows 6, 7, 8, and 9 are road
setUpRows(5, 8, 'road')
// Set up the cars arrays
setUpCarRows()
}
function getRowIndex(index) {
return columns * Math.floor(index/columns)
}
initialLayout()
The function displayCarRight()
is focused on displaying one car, given the index of the div
element that has its rear end. I defined a function getRowIndex()
to calculate and return the index of the first div
element in the row where the div
with the index
passed as argument resides. Since the number of columns in each row is given by the value of the variable columns
, we divide the div
element index by columns
and take the floor of the result to convert to an integer. We add the class of car-back
to the div
element at the index rear
, and the class of car-front
to the div
element at the index rear + 1
.
We called the displayCarRight()
function once for every car in both car rows through the function displayCarsRight()
using the forEach()
function on the arrays carRow1
and carRow2
. But those arrays are initialised empty, so we need to do something about that.
I created the function setUpCarRow1
that sets up the values in the array carRow1
. I want the last car in that row to be initially positioned at the very left edge of the screen. That’s why I start with car1Rear
initialised to 8*columns
, which is the starting index of the penultimate row on the screen. I append this index car1Rear
as well as car1Rear + 5
to the array carRow1
. I’ve used the JavaScript spread operator to achieve this. Since the array was initially empty, it now has two values 8*columns
and 8*columns + 5
. The second car’s rear will be 5 div
elements away from the rear of the first car. Since we want to create three pairs of cars like this, I’ve run a for
loop three times.
I created another function setUpCarRow2()
for the other row of cars. This time, I wanted to place one car at the right edge of the screen and have them move from right to left, as opposed to the first row of cars that start at the left edge and will move towards the right. Finally, I created a function setUpCarRows()
that calls both of these functions: setUpCarRow1()
and setUpCarRow2()
. I then inserted a call to this wrapper function in the initialLayout()
function.
Now, if you refresh the page, you’ll see two rows of cars near the bottom row.
Wheels in motion
Let’s animate the cars. We’ll need to run some code after every few milliseconds, so we’ll need to use setInterval()
. Within the callback that setInterval()
will invoke, we need to move each car forward.
Movement towards the right can be achieved by:
- Incrementing the rear end coordinates of every car.
- Erasing the cars from their current positions.
- Calling the
displayCars()
function, to render the cars on their new location
While incrementing the car’s rear coordinates, we need to be careful near the edges. If a car’s rear end is at the right edge of the row, incrementing it will make it wrap around to the start of the next row. But, we want the cars to appear to be looping around endlessly in the same row. To achieve that, we’ll wrap the index around in the same row using the remainder operator. Here’s the code to update the cars’ positions to make a row of cars appear to be moving towards the right:
function updateCarsRight() {
carRow1 = carRow1.map(c => getRowIndex(c) + (c + 1) % columns)
}
We are using Array.map()
to create an updated version of the original array. For every element in that array, representing the rear end coordinates of a car, we increment it by 1 and then take the remainder after division with the width of the row, i.e., the value of the variable columns
. However, the result of this calculation will always lie in the top row of the game, i.e., the one with the grass. This is because the result will be an integer between 0
and columns — 1
. That is not what we require. To fix this, we need to add the offset of the current row to the result. For that, we’ve added the result of getRowIndex(c)
to the remainder.
We also needed to remove the cars from their current position before showing them in their new position. For that we’ll defined another function removeCars()
to remove all the cars from the screen, and add a call to it into displayCars()
:
function displayCars() {
removeCars()
carRow1.forEach( c => displayCarRight(c))
carRow2.forEach( c => displayCarRight(c))
}
function displayCarRight(rear) {
const divs = container.children
const rowStart = getRowIndex(rear)
divs[rear].className = 'car-back ' + divs[rear].className
divs[rowStart + (rear + 1) % columns].className = 'car-front ' + divs[rowStart + (rear + 1) % columns].className
}
function removeClass(clas) {
const divs = document.querySelectorAll('div.' + clas);
// Iterate through the selected div elements and remove the "log" class
divs.forEach(div => {
div.classList.remove(clas);
})
}
function removeCars() {
removeClass('car-front')
removeClass('car-back')
}
I’ve defined a function removeClass()
that can be used to remove a given CSS class from all div
elements on the page. I could’ve just hard-coded things for car-front
and car-back
classes, but we’ll have to remove trucks, logs, and the frog, later, so this will avoid some code redundancy.
Now, if you add a timer to invoke updateCarsRight()
, you’ll see the last row of cars moving from left to right and wrapping around in the same row:
setInterval(updateCarsRight, 80)
setInterval(displayCars, 50)
Now, to have the other row animated from left to right, we can add similar functions to update the code to:
function displayCars() {
removeCars()
carRow1.forEach( c => displayCarRight(c))
carRow2.forEach( c => displayCarRight(c))
}
function displayCarRight(rear) {
const divs = container.children
const rowStart = getRowIndex(rear)
divs[rear].className = 'car-back ' + divs[rear].className
divs[rowStart + (rear + 1) % columns].className = 'car-front ' + divs[rowStart + (rear + 1) % columns].className
}
function removeClass(clas) {
const divs = document.querySelectorAll('div.' + clas);
// Iterate through the selected div elements and remove the "log" class
divs.forEach(div => {
div.classList.remove(clas);
})
}
function removeCars() {
removeClass('car-front')
removeClass('car-back')
}
function updateCars() {
updateCarsRight()
updateCarssLeft()
}
function updateCarsRight() {
carRow1 = carRow1.map(c => getRowIndex(c) + (c + 1) % columns)
}
function updateCarssLeft() {
carRow2 = carRow2.map(c => getRowIndex(c) + (c - 1) % columns)
}
function updateCars() {
updateCarsRight()
updateCarssLeft()
}
function updateCarsRight() {
carRow1 = carRow1.map(c => getRowIndex(c) + (c + 1) % columns)
}
function updateCarssLeft() {
carRow2 = carRow2.map(c => getRowIndex(c) + (c - 1) % columns)
}
setInterval(updateCars, 80)
setInterval(displayCars, 50)
Trucks
Let’s add a couple of rows with trucks in them. Let’s start with the trucks. The first row of trucks is the sixth row from the top. The second one is the sixth. In the first row, I’d like to show trucks heading right.
Let’s start with displaying a truck heading right, given its front div
element index. Here’s the code:
function displayTruckRight(front) {
const divs = container.children
const rowStart = getRowIndex(front)
divs[front].className = 'cabin ' + divs[front].className
divs[rowStart + (front-1) % columns].className = 'cargo ' + divs[rowStart + (front-1) % columns].className
divs[rowStart + (front-2) % columns].className = 'cargo ' + divs[rowStart + (front-2) % columns].className
}
We add the class of cabin
to the div
at index front
. Then, at the two consecutive indices before that, we assign the class of cargo
. Note the remainder operator and getRowIndex()
, to have the trucks appear to wrap around from the right edge to the left.
I want to place two trucks in the fifth row. The first truck, I’d like to have on the left edge, and the second one 17 div
elements ahead of it. Then, I’d like to have them move from left to right. Here’s how I could define the array holding the front indices of the two trucks:
let truckRow1 = [5*columns + 2, 5*columns + 17]
let truckRow2 = [7*columns - 3, 7*columns - 1 - 17]
The array truckRow1
holds the truck front indices for the fifth row from the top and the array truckRow2
holds the indices of the front of the trucks in the sixth row. Why didn’t we say truckRow1 = [5*columns, 5*columns + 17]
? We’ve added two to both of the array elements because we are storing the indices of the front of the truck, and we want to leave space for two div
elements for the cargo compartments. If the rear of the first truck is at 5*columns
, then the front would be at 5*columns+2
.
We could have stored the rear indices as well. It’s just that I had defined the truck display function already, which expected the front index. Instead of changing that function’s implementation, I decided to go with storing the front indices of the trucks.
To display a truck heading towards, the left, we’ll have to change the order of the cargo
and cabin
classes for the div
elements. Here’s the code:
function displayTruckLeft(front) {
const divs = container.children
const rowStart = getRowIndex(front)
divs[front].className = 'cabin ' + divs[front].className
divs[rowStart + (front+1) % columns].className = 'cargo ' + divs[rowStart + (front+1) % columns].className
divs[rowStart + (front+2) % columns].className = 'cargo ' + divs[rowStart + (front+2) % columns].className
}
To display both of the rows of trucks, let’s add wrapper functions and the timer to invoke it periodically:
const rows = 10
const columns = 45
const container = document.getElementById('container')
let truckRow1 = [5*columns + 2, 5*columns + 17]
let truckRow2 = [7*columns - 3, 7*columns - 1 - 17]
function displayTruckRight(front) {
const divs = container.children
const rowStart = getRowIndex(front)
divs[front].className = 'cabin ' + divs[front].className
divs[rowStart + (front-1) % columns].className = 'cargo ' + divs[rowStart + (front-1) % columns].className
divs[rowStart + (front-2) % columns].className = 'cargo ' + divs[rowStart + (front-2) % columns].className
}
function displayTruckLeft(front) {
const divs = container.children
const rowStart = getRowIndex(front)
divs[front].className = 'cabin ' + divs[front].className
divs[rowStart + (front+1) % columns].className = 'cargo ' + divs[rowStart + (front+1) % columns].className
divs[rowStart + (front+2) % columns].className = 'cargo ' + divs[rowStart + (front+2) % columns].className
}
function displayTrucks() {
removeTrucks()
truckRow1.forEach( t => displayTruckRight(t))
truckRow2.forEach( t => displayTruckLeft(t))
}
function removeClass(clas) {
const divs = document.querySelectorAll('div.' + clas);
// Iterate through the selected div elements and remove the "log" class
divs.forEach(div => {
div.classList.remove(clas);
})
}
function removeTrucks() {
removeClass('cargo')
removeClass('cabin')
}
setInterval(displayTrucks, 50)
function getRowIndex(index) {
return columns * Math.floor(index/columns)
}
Finally, let’s add update functions that animate the trucks:
const rows = 10
const columns = 45
const container = document.getElementById('container')
let truckRow1 = [5*columns + 2, 5*columns + 17]
let truckRow2 = [7*columns - 3, 7*columns - 1 - 17]
function displayTruckRight(front) {
const divs = container.children
const rowStart = getRowIndex(front)
divs[front].className = 'cabin ' + divs[front].className
divs[rowStart + (front-1) % columns].className = 'cargo ' + divs[rowStart + (front-1) % columns].className
divs[rowStart + (front-2) % columns].className = 'cargo ' + divs[rowStart + (front-2) % columns].className
}
function displayTruckLeft(front) {
const divs = container.children
const rowStart = getRowIndex(front)
divs[front].className = 'cabin ' + divs[front].className
divs[rowStart + (front+1) % columns].className = 'cargo ' + divs[rowStart + (front+1) % columns].className
divs[rowStart + (front+2) % columns].className = 'cargo ' + divs[rowStart + (front+2) % columns].className
}
function displayTrucks() {
removeTrucks()
truckRow1.forEach( t => displayTruckRight(t))
truckRow2.forEach( t => displayTruckLeft(t))
}
function updateTrucks() {
updateTrucksRight()
updateTrucksLeft()
}
function updateTrucksRight() {
truckRow1 = truckRow1.map(t => getRowIndex(t) + (t + 1) % columns)
}
function updateTrucksLeft() {
truckRow2 = truckRow2.map(t => getRowIndex(t) + (t - 1) % columns)
}
function removeClass(clas) {
const divs = document.querySelectorAll('div.' + clas);
// Iterate through the selected div elements and remove the "log" class
divs.forEach(div => {
div.classList.remove(clas);
})
}
function removeTrucks() {
removeClass('cargo')
removeClass('cabin')
}
setInterval(displayTrucks, 50)
setInterval(updateTrucks, 120)
function getRowIndex(index) {
return columns * Math.floor(index/columns)
}
Logs
Now, let’s add the code to show and animate the logs. Each log consists of 7 div
elements. I want three rows of logs, starting right after the first row. Each row will have two logs. The first row of logs will go from left to right, the next one right to left, and the third one from left to right.
The following code sets up the logs, displays them, and animates them. This time, in the update functions, you’ll see nested loops. The outer loop runs over the logs in a row, and the inner loop runs for all the 7 div
elements within a single log:
const rows = 10
const columns = 45
const container = document.getElementById('container')
let logRow1 = [columns, columns + 22]
let logRow2 = [3*columns - 7, 3*columns - 7 - 22]
let logRow3 = [3*columns + 8, 3*columns + 30]
function updateLogsRows() {
updateLogsRowRight()
updateLogsRowLeft()
}
function updateLogsRowRight() {
const logRows = [logRow1, logRow3]
logRows.forEach( logRow => {
for (let i = 0 ; i < logRow.length; i++) {
const rowStart = getRowIndex(logRow[i])
logRow[i] = rowStart + (logRow[i] + 1) % columns
}
})
}
function updateLogsRowLeft() {
const rowStart = getRowIndex(logRow2[0])
for (let i = 0 ; i < logRow2.length; i++) {
const rowStart = getRowIndex(logRow2[0])
logRow2[i] = rowStart + (logRow2[i] - 1) % columns
}
}
function refreshLogsRows() {
removeLogs()
refreshLogsRowsRight()
}
function refreshLogsRowsRight() {
logRow1.forEach(l => displayLog(l))
logRow2.forEach(l => displayLog(l))
logRow3.forEach(l => displayLog(l))
}
function refreshLogsRowsLeft() {
logRow2.forEach(l => displayLogLeft(l))
}
function removeClass(clas) {
const divs = document.querySelectorAll('div.' + clas);
// Iterate through the selected div elements and remove the "log" class
divs.forEach(div => {
div.classList.remove(clas);
})
}
function removeLogs() {
removeClass('log')
}
function displayLog(logRear) {
const divs = container.children
const rowStart = getRowIndex(logRear)
for (let i = logRear ; i < logRear + 7; i++) {
divs[rowStart + (i % columns)].classList.add('log')
}
}
setInterval(updateLogsRows, 120)
setInterval(refreshLogsRows, 50)
function getRowIndex(index) {
return columns * Math.floor(index/columns)
}
With these additions, the three rows of logs will show up animated, moving in alternating directions.
The function displayLog()
displays one log given its rear end index. The function updateLogsRowRight()
updates the log indices for the rows in which the logs will appear to be moving towards the right. Similarly, we’ve defined updateLogsRowLeft()
. We wrapped these in updateLogsRows()
.
I’m putting two logs in each row. Each log is seven div
elements in size. Two logs occupy 7 + 7 = 14 div
elements. That leaves us with 45–14 = 31 div
elements. We need to split this space roughly equally in two parts, as shown below: to the right of each log.
We could split the remaining space into 15 and 16 div
elements. So, if the left log’s rear end (the one of the left) is at index 0, then the right log’s rear end would start at 7 + 15 = 22 div
elements. That should explain the values in the arrays holding the log rear indices.
I’ve used setInterval()
to periodically invoke the update and display functions so that the logs are displayed and appear animated.
That’s great. Now, let’s handle the frog movement.
Frog movement
We’ll add an event handler for the keydown
event and based on the arrow key pressed, we’ll update the frog’s current location. Recall that we’ve already declared a variable to hold the frog’s current location. Here’s the code:
document.addEventListener('keydown', moveFrog)
function getRowType(index) {
const styles = ['grass', 'water', 'water', 'water', 'grass', 'road', 'road', 'road', 'road', 'grass']
const rowNumber = Math.floor(index / columns)
return styles[rowNumber]
}
function moveFrog(e) {
const divs = container.children
const key = event.key
switch(key) {
case 'ArrowLeft':
if(frogLocation % columns !== 0) {
frogLocation--
}
break
case 'ArrowRight':
if(frogLocation % columns !== columns - 1) {
frogLocation++
}
break;
case 'ArrowUp':
if (Math.floor(frogLocation / columns) !== 0) {
frogLocation -= columns
}
break;
case 'ArrowDown':
if (Math.floor(frogLocation / columns) !== (rows - 1)) {
frogLocation += columns
}
break;
}
}
After hooking up a keydown
event handler, we defined a function that returns the type of the row that a particular div
is at, given its index. This will come in handy later.
In the event handler, we obtain the key pressed and handle its possible values using a switch
statement. If the left arrow key is pressed, we will decrement the frog’s location. If the frog is currently at the div
element at the index frogLocation
, it should move to the div
element to its left, which has the index frogLocation-1
. Unless, of course, the frog is already at the left most position of the board.
A similar strategy is followed for other keys. We’ve implemented the logic to update the frog’s position. Now, we need to implement the logic to periodically display the frog at the correct location. Also, there’s no need for displaying the frog in the initialLayout() function, so I removed that line of code:
setInterval(updateFrog, 50)
function updateFrog() {
removeFrog()
divs = container.children
divs[frogLocation].classList.add('frog')
}
function removeFrog() {
removeClass('frog')
}
function initialLayout() {
for (let i = 0 ; i < rows ; i++) {
for (let j = 0 ; j < columns ; j++) {
const elem = document.createElement('div')
container.appendChild(elem)
}
}
// Make the top row grass
setUpRows(0, 0, 'grass')
// Make the bottom row grass
setUpRows(9, 9, 'grass')
// Rows 2, 3, and 4 are water
setUpRows(1, 3, 'water')
// Row 5 is grass
setUpRows(4, 4, 'grass')
// Rows 6, 7, 8, and 9 are road
setUpRows(5, 8, 'road')
// Set up the cars arrays
setUpCarRows()
}
We call our removeClass()
function to remove the frog
class from all div
elements, and then add the class frog
to the frog’s current location. Now, you’ll be able to move the frog across the screen, but it wouldn’t die. We still have to implement that logic.
Making the frog float with the logs
If the frog gets on a log, it should float with it. To do that, we’ll have to modify our log update functions as follows:
function updateLogsRowRight() {
const logRows = [logRow1, logRow3]
logRows.forEach(logRow => {
const rowStart = getRowIndex(logRow[0])
// Do we need to move the frog?
logRow.forEach(l => {
for (let i = l ; i < l + 7 ; i++) {
if (rowStart + (i % columns) === frogLocation) {
frogLocation = rowStart + ((frogLocation + 1) % columns)
break
}
}
})
})
logRows.forEach( logRow => {
for (let i = 0 ; i < logRow.length; i++) {
const rowStart = getRowIndex(logRow[i])
logRow[i] = rowStart + (logRow[i] + 1) % columns
}
})
}
function updateLogsRowLeft() {
const rowStart = getRowIndex(logRow2[0])
// Do we need to move the frog?
logRow2.forEach(l => {
for (let i = l ; i < l + 7 ; i++) {
if (rowStart + (i % columns) === frogLocation) {
frogLocation = rowStart + ((frogLocation - 1) % columns)
break
}
}
})
for (let i = 0 ; i < logRow2.length; i++) {
const rowStart = getRowIndex(logRow2[0])
logRow2[i] = rowStart + (logRow2[i] - 1) % columns
}
}
Essentially, in each of the update functions, we are checking for every one of the seven segments of a log if it also houses the frog. If so, we update the frogLocation
variable as well.
Did we win?
Now, let’s write some code to check whether we won or lost. We win if we reach the top row. We lose if time runs out, or if we hit a vehicle or drown.
Let’s start by declaring an array contain all the classes for the vehicles. If the frog lands on a div
with one of these styles (along with the style of road
), it’s dead. We can use the Array.some()
function, here. This function invokes a callback function passed to it as argument on the elements of the array one by one. It returns true
, if the callback function returns true
for any of the array elements. Here’s how we could check if the frog was hit:
const deadStyles = ['car-back', 'car-front', 'cabin', 'cargo']
const hit = deadStyles.some(style => divs[frogLocation].classList.contains(style))
How about the frog landing on a div
with class water
? If the frog lands on a log, it is fine, otherwise it is dead.
With the above strategies, we can update the updateFrog()
function as follows:
function updateFrog() {
removeFrog()
divs = container.children
const rowType = getRowType(frogLocation)
const hit = deadStyles.some(style => divs[frogLocation].classList.contains(style))
const water = divs[frogLocation].classList.contains('water')
const log = divs[frogLocation].classList.contains('log')
if (hit || (water && !log)) {
// The frog is dead
}
divs[frogLocation].classList.add('frog')
}
If either of two conditions is true, i.e., either the frog was hit by a vehicle, or the frog was in the water without a log, the frog dies. In such a case, we should stop the game play. That involves clearing all the timers to stop the animations, and removing the key down event handler.
To clear the timers, we need handles to each timer. We didn’t save the values of those previously, so we’ll have to update all the calls to setInterval()
. Here’s how we can store all the timers in an array and clear them out all at once, followed by removal of the key handler.
let timers = []
timers = [setInterval(updateLogsRows, 120), ...timers]
timers = [setInterval(displayLogsRows, 50), ...timers]
timers = [setInterval(displayTrucks, 50), ...timers]
timers = [setInterval(updateTrucks, 120), ...timers]
timers = [setInterval(displayCars, 50), ...timers]
timers = [setInterval(updateCars, 80), ...timers]
timers = [setInterval(updateFrog, 50), ...timers]
function updateFrog() {
removeFrog()
divs = container.children
const rowType = getRowType(frogLocation)
const hit = deadStyles.some(style => divs[frogLocation].classList.contains(style))
const water = divs[frogLocation].classList.contains('water')
const log = divs[frogLocation].classList.contains('log')
if (hit || (water && !log)) {
timers.forEach(t => {
clearInterval(t)
})
document.removeEventListener('keydown', moveFrog)
}
divs[frogLocation].classList.add('frog')
}
Now, let’s also add the logic determine a winner. If the frog reaches the top row, then we’ve won.
function updateFrog() {
removeFrog()
divs = container.children
const rowType = getRowType(frogLocation)
const hit = deadStyles.some(style => divs[frogLocation].classList.contains(style))
const water = divs[frogLocation].classList.contains('water')
const log = divs[frogLocation].classList.contains('log')
const won = frogLocation < columns
if (won || hit || (water && !log)) {
timers.forEach(t => {
clearInterval(t)
})
document.removeEventListener('keydown', moveFrog)
}
divs[frogLocation].classList.add('frog')
}
Now, if you make it to the top row, you’ve won and the game animation will stop. Let’s now show the game status somewhere, i.e., whether we won or lost.
I updated the HTML file to the following:
<!DOCTYPE html>
<html>
<head>
<title>Frogger</title>
<link rel="stylesheet" href="style.css"/>
</head>
<body>
<h2>Frogger</h2>
<div id="outer">
<div id="timer">Time left: <span id="time"></span> seconds</div>
<div id="result"></div>
<div id="container"></div>
</div>
<script type="text/javascript" src="app.js"></script>
</body>
</html>
I added the following classes to the CSS file:
#outer {
position: relative;
}
#timer, #result {
width: 100vw;
text-align: left;
}
#result {
width: 100vw;
text-align: left;
}
Then I made the following modifications to the code:
let timeReamining = 200
const timeReaminingSpan = document.getElementById('time')
function initialLayout() {
for (let i = 0 ; i < rows ; i++) {
for (let j = 0 ; j < columns ; j++) {
const elem = document.createElement('div')
container.appendChild(elem)
}
}
// Make the top row grass
setUpRows(0, 0, 'grass')
// Make the bottom row grass
setUpRows(9, 9, 'grass')
// Rows 2, 3, and 4 are water
setUpRows(1, 3, 'water')
// Row 5 is grass
setUpRows(4, 4, 'grass')
// Rows 6, 7, 8, and 9 are road
setUpRows(5, 8, 'road')
// Set up the cars arrays
setUpCarRows()
timeReaminingSpan.innerHTML = timeReamining
}
Now, you should be able to see a message declaring that you have 200 second remaining, near the top of the screen when the game load. Let’s animate the time remaining part:
timers = [setInterval(updateTimeRemaining, 1000), ...timers]
function updateTimeRemaining() {
timeReamining--
timeReaminingSpan.innerHTML = timeReamining
}
We’ve hooked up a timer to trigger every second, updating the value of the timeRemaining
variable, and update the display. Now, you should see the timer counting down. We’ve stored this timer handle in the timers array as well, so that if we loose, the timer stops counting down.
Let’s also add a check to see if the timer reaches zero, we stop all timers and declare that we lost. We can do this in updateFrog()
, too. That’d be convenient since we already have the timer clearing functionality defined there.
const result = document.getElementById('result')
function updateFrog() {
removeFrog()
divs = container.children
const rowType = getRowType(frogLocation)
const hit = deadStyles.some(style => divs[frogLocation].classList.contains(style))
const water = divs[frogLocation].classList.contains('water')
const log = divs[frogLocation].classList.contains('log')
const won = frogLocation < columns
const timeout = timeReamining <= 0
if (timeout || won || hit || (water && !log)) {
timers.forEach(t => {
clearInterval(t)
})
document.removeEventListener('keydown', moveFrog)
}
if (won) {
result.innerHTML = 'You won'
}
else if (timeout || hit || (water && !log)) {
result.innerHTML = 'Game over'
}
divs[frogLocation].classList.add('frog')
}
We’ve obtained a handle to the div
element with the id
of result
. We added a condition of timeout
to the if
statement that clears the timers. Finally, we displayed the result, if the game was either won or lost.
That’s all folks
That wraps up this blog. If you want, you can download the entire code from this repository. Play with it and try to extend it. Here are some ideas:
- At the moment, the sprites move very fast. If we increase the values of the timers in
setInterval()
calls, then the sprites move with jerks. At the same time, if we could redefine the shapes by building them usingdiv
elements with smaller widths, the animation would be smoother. In other words, rather than creating a car out of twodiv
elements of 20 x 20 px, create it out of, say, fourdiv
elements of 5x20 px. - Add “Start game”, “Pause game”, “New game” buttons.