Learn OOP the fun way! Build a command line “Frogger” game

Liz Fedak
15 min readNov 14, 2020

--

Disclaimer: Don’t read this guide as an OOP design guide, but instead as a way to practice writing OOP code and reviewing OOP concepts in context.

Step 1. Write a description of the problem

OOP deals with objects using every day language, so we’ll start by writing a description of the game and what we are trying to build. From the description, we’ll take the important nouns and verbs, which we’ll use to build a skeleton of the game.

  • Player — Frog
  • Obstacles
  • Road/River — Lanes
  • Landscape
  • Move
  • Get hit

Step 2: Code!

The code will be presented in 7 “lessons” and you’ll see notes on the code as we go and a lot of refactoring as we go as well to be more “OOP”.

Lesson 1

In this lesson, we’re going to start small. You can see the full code at the bottom of the lesson. The code uses the pseudo-classical OOP pattern, so we have constructor functions that can build player, lane, landscape, and game instances, and methods added to those function’s prototype properties.

  1. Add a function to create player objects. The object should have one parameter, icon, with a default value for the player icon.

Solution: We know the player will need an icon. Since the game is called Frogger, the player constructor function is Frog. To make selecting an icon optional, a default value is set for the parameter.

2. Add a constructor function that can create ‘lanes’ that Frogger will travel up and down on. Lane should take a parameter for the total width of the board and should have a method for displaying the lane. For this exercise set, we’re going to represent our location with a nested array structure, but feel free to modify that for your solution. It’s also not necessary to instantiate the lane code in a makeLane method — you could do that directly on a this.lane property in the Lane constructor.

Let’s take a closer look at what is going on with Lane. On line 5 above in the makeLane method, we are using the new operator to make an Array object. This shows that built-in objects also have constructor functions same as the ones we’re making today. Behind the scenes, this is making an array of length max. That looks something like this:

> new Array(20)
[ <20 empty items> ]

Then we use the array method fill to replace the empty items with a string “_” to represent the road/lane.

On line 8, we access the Lane function’s prototype property and add a function value for the properties makeLane and display. This is what that looks like. The pink box is the Lane constructor function, and Lane.prototype is a property of Lane. play is a property of the Lane.prototype object with a function value.

You can also pop open DevTools and look at the properties with the Console:

> function Lane() {}
undefined
> Lane.prototype.makeLane = function() {
console.log('example');
}

ƒ () {
console.log('example');
}

> Lane.prototype
{makeLane: ƒ, constructor: ƒ}

3. Spoiler alert! We’ll remove this later, so just add this StartLane function in for now and don’t question it. ;)

Create a constructor function for the starting lane. This function should inherit from Lane and pass an argument for max to Lane. After that, the lane property should be modified so the player icon is in the middle of the lane. We didn’t make these objects collaborate yet, so just type in the icon symbol as a string for the value.

From the solution, what I want to call out is the use of the function method call on line 2 — Lane.call(this, max) in the StartLane function then later StartLane.prototype = Object.create(Lane.prototype) and StartLane.prototype.constructor = StartLane, which altogether set up inheritance. Now StartLane can use the makeLane function from Lane’s prototype object.

4. Create a constructor function for the landscape. As we mentioned earlier, we’ll use a nested array structure for the lanes and player movement, so if you’re following along with that model, add in a property lanes to represent the lanes as a currently empty array. After that, instantiate a startLane object and pass a max value that is 1 less than your default max value. (i.e. if you’re matching our default length of 20, pass 19 as the value). Next in this function, instantiate around 5 new regular lanes to make up the rest of the board and push them into the lanes array. We specifically need to push the lane property from the new Lane objects.

You might see a problem already with the code below, why isn’t startlane in the array? We’ll fix that later.

Make a method display that displays each lane in Landscape’s lanes property. The method should join the elements of each array and log the lanes to the console. After you log each of the lanes in the array, log the startlane in the same way if you didn’t add that to your array.

5. Add a constructor function for game play named FroggerGame. The function should have a property landscape, which is a new Landscape object. Add a method play which for now just invokes the display method from Landscape. Instantiate a FroggerGame object and assign it to a variable game, then invoke the play method.

Here’s the full code from this lesson:

Lesson 2

In this lesson, we’ll add properties to Frog, add a player to the FroggerGame, and add in some functionality to move the player around the ‘board’/landscape.

  1. Add in some properties to the Frog object.
  • position with value null
  • lives with value 3
  • row with value 6
  • column with value 9

2. Fix our array structure and add in the ability to ‘move’ the player around the nested arrays and therefore around on the board.

Let’s fix the startLane issue first. Revisit Landscape and modify it such that we can add the startLane property as the final array in the nested arrays and remove the explicit console.log of startLane in the display method.

Now, let’s reason about how to move around on our landscape. Right now, this is what all of our lanes look like as the landscape:

We know that the board looks approximately like the image, and we want to move Frogger up and down and left to right.

In array form, that is something like this in term of indices.

let array = [
0: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
1: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
2: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
3: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
4: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
5: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
6: [0,1,2,3,4,5,6,7,8,✅,10,11,12,13,14,15,16,17,18,19,20],
]

So, to move left, we need to replace array[currentRow][currentColumn ] of the player icon’s position with a lane marker, decrease the column property of the player icon, then redraw the player icon in the array. In other words:

  • array[6][9] = “-”
  • array[6][9–1] = “✅”
  • Add in player.column -= 1 somewhere

So, let’s make some move methods that will do just that! This example will start with 4 methods to cover each direction, but we’ll DRY up our code later. (DRY stands for Don’t Repeat Yourself.)

I’ll show you how to make the moveLeft method and you can extrapolate from that how to set up the rest of the directions.

As we mentioned above, this method will look at an array of arrays and basically just modify some string values, then update a property on the Frog player object.

To write the moveLeft method on the FroggerGame prototype, write a method with one parameter named lanes, an array of arrays that represents the lanes. In the body of the code, first reassign the current player position to match the rest of the landscape by modifying the value of lanes[currentRow][currentColumn ] using property access on the player object. Since we are moving left, the player’s column property will be 1 smaller, so for the next line of code, reassign lanes[currentRow][currentColumn - 1] to the player icon. Finally, modify the actual column property on the player, then return the new lanes array. Next, go on to write a similar method for the other 3 directions. We’ll worry about things like the player going off of the board later.

3. Invoke a few of the new methods in the play method to test the functionality. Don’t forget to show the new board after you make a move.

Here’s the full code from “Lesson 2”:

Lesson 3

In lesson 3, we will add some game play messages, add in a way to use the keyboard to choose the movement direction, and start to add in some obstacles.

  1. Making Obstacles

What do we know about obstacles in Frogger?

  • When we initialize the board, obstacles appear at random on the grid
  • Obstacles can only move in one direction
  • Obstacles should have a default icon/ icons
  • New obstacles enter the screen at the right most side of the landscape and move left

With that information, we can write our Obstacle constructor function and a move method. The function should have three parameters, row, column, and an icon. The move method should change the depth of the calling object by -1.

2. How can we keep track of lots of obstacles?

We need a way to track what obstacles have been added to the board. They have similar column and row properties as the player object. There are many ways to set this up. For our solution, add a static property obstacles in the Obstacles function with an empty object value. Then, add a for loop that will add 5 new Obstacle instances to the object. Set it up such that the key is a string value of the current iteration and the value is the new obstacle with a column and row argument. Those values can all be the same for now; we’ll use the Math object to randomize it later.

Bonus: Notice how we use String(counter) on line 4 below. This is using the String function to create a new string, but not with the new operator, so it is a primitive string instead of a String object.

3. Initialize a board with distributed obstacles

Now we need to populate the board with our obstacles. To me that sounds like “initialize obstacles,” which seems to be a method that should be in the Obstacles.prototype object or the FroggerGame.prototype object. This example will put the method in the Obstacles.prototype object, but you’ll see while writing the code why it might make more sense in the FroggerGame.prototype object.

The initializeObstacles method will need access to the object of obstacles, the array of lanes, and the obstacle icon. Let’s plan to add this method using Object.assign() to FroggerGame too. Thus, when we call the method, we’ll pass two arguments, the array of arrays, and a reference to the icon.

For the sake of keeping our code organized, declare a local variable and assign the value of this.obstacles.obstacles. We’ll call the function from the execution context of the FroggerGame instance, which will have a this.obstacles property.

Write a for/in loop that iterates over the keys in the obj object. Inside of the loop, declare two local variables, one for the column property and one for the row property. Again, this is just to make it easier to read the code. Lastly, reassign the value at the array[row][column] to be the icon that we passed to the function. After the loop, return the updated obj.

Don’t forget to use the Object.assign() method to add initializeObstacles method to FroggerGame.

We’ll invoke the method in the play method in FroggerGame.

4. Optional: Refactor how we pass the icon to create a new Obstacle

Since we’ll be creating obstacles of the same type depending on the landscape (i.e. a river => logs and turtles, a road => cars and trucks), I decided to makeObstacle solely deal with grid placement, while Obstacles deals with generating and maintaining the type of obstacles we have on the board and how many at any given time. I’d love your feedback on this design decision! These changes are fairly straight forward, so I won’t explain further than what is commented in the code snippet below.

This is the updated version:

5. Lastly–interactivity. The game doesn’t do much … yet!

We’ll need some way for the player to dictate where frogger should move. Wouldn’t it be ideal just to use the keyboard arrows? To do so, we’ll use readline-sync for player input, but will need some way to tell the game that an arrow click equals a move.

Use readline-sync to ask the user to click an arrow to move the Frog icon, and assign the response to a variable.

To keep it simple, we’ll use a switch statement with the corresponding ANSI escape sequence to match the arrow key click to a move method.

  • ‘[A’: up
  • ‘[B’: down
  • ‘[C’: right
  • ‘[D’: left

One last thing, let’s add a displayWelcomeMessage method to FroggerGame so we can greet the user. Log some sort of greeting in the body of the method.

Here’s the full code for Lesson 3:

Lesson 4

This lesson will be light-weight and focused on cleaning up some earlier code.

  • Remove startLane
  • Add a method for initializing the Frog player position

To remove startLane, we need to:

  • Add row and column parameters to the Frog function
  • Pass row + column args when we invoke Frog
  • Remove the startLane function
  • Remove the startLane invocation in Landscape
  • Add in a method that handles the player start position
  • Invoke the method that handles the player start position

Go ahead and update the parameters on the Frog function. As for passing arguments, how can we make this work for any size board? We know the Frogger player always starts in the last row in the middle, so we can use our lanes array and pass the length minus 1 for the row and the length of an inner array divided by 2 for the column.

For handlePlayerStartPosition, add it as a method on FroggerGame. We can access the row and column properties from the Frog player object and use those to reassign that position in the lanes array to the value of the player’s icon property.

Here is what that looks like:

And here is the code for the full lesson:

Lesson 5

Remember a while back I promised that we’d randomize the obstacles? That time is now. Here’s what to expect in this lesson:

  • Randomization of the obstacle locations at board initialization
  • Randomization of the row when new obstacles enter
  • A move method to handle obstacle movement
  • A method to add more obstacles each round
  • A way to account for the players being out of bounds
  • Update the methods to move the player object and account for being “out of bounds”

Since a lot is happening in Lesson 5, I recommend reading through the overview before implementing anything, unless you’d like to write code then refactor it.

First, we will add in a random number generator method so we can use it as needed in any Obstacles instances.

We’ll use the new getRandom method so we can randomize how many obstacles we start the board with. Pass a max parameter of 10 so we don’t add too many obstacles. ;)

Since we’re only going to bulk add obstacles all over the board at the start of the game and will otherwise add them from the right most part of the screen, add a property to track state if it is the beginning of the game. Set a default parameter as false so we don’t have to pass the argument unless it is in fact the beginning of the game.

You might notice that we’re using the getRandom method here too. We’ll cover that next.

We want to randomize where the obstacles appear, but the getRadom method is in Obstacles. Let’s move the randomization into a mix-in so Obstacle + Obstacles can have shared behavior. Using mix-ins is a way to model multiple inheritance in JavaScript, where by we just use Object.assign to give access to the properties of this object from other objects. Object.assign copies all enumerable properties into the target object.

Now we can remove the row/column from the parameters and just make the properties using getRandom for the row and the this.newGame property + logic for the column.

Alright, now it’s starting to look like a real Frogger game! The rest of the Lesson is more complicated, so I’ve just pasted in the code and summarized it.

Giving movement to the obstacles

Let’s go back to the Obstacles.prototype.initializeObstacles method now–we’ve been hard coding in a column and row argument, so all of the obstacles are in the same location. Instead, we’ll randomize that value. To do that, we’ll use the getRandom method in the Obstacle method itself.

Let’s add in logic to check if the player is out of bounds. We are using an array of arrays, so we know if the player tries to go up into the first array, the player has won.

If the player tries to go left past the 0 index, the player is out of bounds.

If the player tries to go right past the last index, the player it out of bounds.

If the player tries to go down past the last array, the player is out of bounds.

Thus, we can add this in with simple if statements.

Full Lesson 5 code

Lesson 6

There is a small bug in the moveRight method. Update to reflect the code below:

For the rest of this tutorial, please infer what updates are made from the code snippet + title if more details are not provided. Feel free to leave a comment if you have any questions! I’ll write out more explicit details later.

Update to account for collisions with our obstacles

We’ll continue to encapsulate obstacle behavior on Obstacles. Since we call the gameOver method in the testForCollision method, we need to pass it as an argument and pass a this context.

Looking at the code below, on line 9 we’re calling the gameOver method, which is passed as the func argument, and we use the function method .call to use an explicit this context as thisArg (passed as this from the game object). On line 29 is where we invoke this method on the obstacles collaborator object in the play method. You can see we are passing the lanes array as this.lanes, the gameOver method from FroggerGame, and the current this value.

This testForCollision method works by looping through the array of arrays to see if the player’s icon is still on the board somewhere. The reason this works is because we call it after updating the obstacles and if an obstacle moves into the player’s current position, the player icon is overwritten with the obstacle icon and won’t be in any array anymore.

A while loop was also added for game play so the game ends now if Frogger wins or is hit by a collision. The gameOver method simply updates the keepPlaying property as false, which breaks the game out of the while loop.

Bonus Features

It’s grayed out for now for debugging purposes, but eventually we’ll start to use console.clear() to keep the game clean and in the same spot in the command line.

Lesson 7

In this lesson, we’ll make the game more game-like. By that I mean, we’ll add things like:

  • Adding points when the player wins a round
  • Adding the ability to play more rounds
  • Adding logic for reseting the board for another round
  • Tracking lives

For each new round, the player can have a collision up to 3 times, with the game ending on the third KO. When the game ends due to a collision, the game ends altogether.

Since we have rounds of rounds, to be explicit: each set of 3 lives is the same as 1 round in the game.

So, we have 1 game, which is made up of rounds. Each round is made up of 3 lives. Each time a round ends via a win, the player can opt to player another round. When the game ends due to losing 3 lives, the overall main game ends as does the current round. Let’s get started!

Player changes

We need a way to reset the player’s position when we reset the board and a way to track lives. Three methods on Frog can handle resetting the player position, removing a life, and resetting lives. resetPlayerPosition will be invoked when we reset the board for a new game. removeLife will be used when there is an obstacle collision. resetLives is used when the player wins and a new game is started.

Obstacles changes

There is a lot going on below and there is most likely a better way to handle this, but this method is a great opportunity to practice passing context + using the call method. The code below is basically the same as:

Test if the frog icon is still on the board. If it is not, enter the second if block and see if we should end the game. If yes, invoke that the game is over. Otherwise, invoke that there was a collision and reset the board.

Let’s look at the functions we’re passing to testForCollision.

Displaying points

Updating messaging and making it more user friendly

That wraps up the tutorial, but there are many things you can do to improve this code or to make it more DRY. Post a Github link below to share your version! Here’s the full repo.

--

--

Liz Fedak
Liz Fedak

Written by Liz Fedak

Journalist and endlessly curious person. One half of @hatchbeat.