Frogger in JavaScript

Muhammad Saqib Ilyas
27 min readAug 27, 2023

--

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:

The car and the truck

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:

Initial screen layout

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:

Adding the water and the road

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:

The first row of cars initialised

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.

Calculating the dimensions of the space around the logs

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 using div elements with smaller widths, the animation would be smoother. In other words, rather than creating a car out of two div elements of 20 x 20 px, create it out of, say, four div elements of 5x20 px.
  • Add “Start game”, “Pause game”, “New game” buttons.

--

--

Muhammad Saqib Ilyas

A computer science teacher by profession. I love teaching and learning programming. I like to write about frontend development, and coding interview preparation