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.
- 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 display
ing 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.
- Add in some properties to the
Frog
object.
position
with valuenull
lives
with value3
row
with value6
column
with value9
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:
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.
- 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
andcolumn
parameters to theFrog
function - Pass
row
+column
args when we invokeFrog
- 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.
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.