Eloquent JS Project Robot Explained — Part 3

Sayed Alwedaie
13 min readJan 10, 2023

--

This is part 3 of a three-part tutorial series, going into depth to explain the robot project from Eloquent Javascript chapter 7.

In part 1 we built and ran a simple world with a simple robot. In part 2 we improved the code and introduced a smarter robot. In this final part, we’ll move the code ever closer to the book’s version by converting the village state to a class. We’ll also write code to procedurally generate a random world instead of a hard-coded one.

You can access the other parts here:

Eloquent JS Project Robot Explained — Part 1
Eloquent JS Project Robot Explained — Part 2

Converting ‘move’ to a Method

What do you think of the relationship between the move function and villageState? Wouldn’t it be convenient to relocate the move function to be a method inside villageState? Let’s do that.

Challenge

  • Relocate the move function to be a method inside villageState.
  • Inside runRobot, fix the reference to move to make it accessible from inside villageState.
  • Test the code. It should work normally but you can still optimize it. Inside move, what can you do about the references to villageState?

Hints

  • When moving the function, make the necessary changes to its syntax now that it’s going to be an object property.
  • To call move as a method, you would need to access the object it’s contained within first.
  • For the optimization task, have you thought of this?

Solution

This is mostly copy and paste, with two minor edits.

  1. Change function move(destination) to move: function(destination) and then literally copy and paste the whole function inside villageState. Nothing crazy here.
  2. The other fix is to make runRobot be able to call this method as a property of villageState. We can write villageState.move(action.direction) but remember that runRobot is receiving villageState as an argument using the state parameter. So all we need to do is put state. in front of the call to move.
  3. Finally, since move is now inside villageState, we can reference the object using this. So just replace all instances of villageState inside move to this and you’ll be good to go!
let villageState = {
currentRobotPlace: "Post Office",
remainingParcels: PARCELS,

// move function expressed as a method
move: function(destination) {
if (roadGraph[this.currentRobotPlace].includes(destination)) {
let parcels = this.remainingParcels

let movedParcels = parcels.map(parcel => {
if (parcel.place != this.currentRobotPlace) return parcel
return { place: destination, address: parcel.address }
})

let undeliveredParcels = movedParcels.filter(
parcel => parcel.place != parcel.address
)

this.currentRobotPlace = destination
this.remainingParcels = undeliveredParcels
}
}
}
// inside runRobot
// ...
state.move(action.direction)
// ...

Converting move to a method was a precursor to what we’re going to do next.

Converting ‘villageState‘ to a Class

Unlike the book’s version, we have defined the village’s state as a simple object. My goal was to make the different components of the project easier to understand before encapsulating them in classes. But why would we want to encapsulate anyway?

The author explains that generally modifying global states is not a good idea. We have a villageState object that get’s modified at every turn of the robot. What if another part of the code wanted to access the village’s state or have a history of the state? The code thus far does not have an answer.

The goal is to have a class called VillageState that constructs a new state every time the robot moves. So instead of modifying a global variable, we would return a new state on the fly. When called with the new keyword, this class will build and return an object similar to villageState we defined ourselves, but with extra bells and whistles like the move method inside its prototype.

Note: I highly recommend reviewing the Class basic syntax tutorial to understand how classes work.

Challenge

Build a VillageState class (capital V) with the following logic:

  • It has a constructor with two parameters: place and parcels.
  • Initialize the constructor with currentRobotPlace and remainingParcels set to the parameters.
  • Add the move method to the class.
  • Instead of modifying a global variable, have the move method return a new instance of the constructor, passing in the appropriate arguments.
  • To test the class, comment out the call to runRobot, and run new VillageState("post Office", PARCELS)
  • Test two moves: one to a location that’s connected to Post Office (like the Marketplace) and one to an unconnected one (like the Farm.)
  • Can you fix the problem in the second scenario?

Note: This class will completely replace the old villageState object, so you would need to delete it.

Hints

  • The constructor should reference the properties with this.
  • Yes, we don’t initialize the constructor with “Post Office” and PARCELS because we’re building a general purpose class. It would build new instances using arguments.
  • To fix the last part, think how the fist if statement inside move handles the result.

Solution

class VillageState {
constructor(place, parcels) {
this.currentRobotPlace = place;
this.remainingParcels = parcels;
}

move(destination) {
if (roadGraph[this.currentRobotPlace].includes(destination)) {
let parcels = this.remainingParcels

let movedParcels = parcels.map(parcel => {
if (parcel.place != this.currentRobotPlace) return parcel
return { place: destination, address: parcel.address }
})

let undeliveredParcels = movedParcels.filter(
parcel => parcel.place != parcel.address
)

return new VillageState(destination, undeliveredParcels)
}
}
}

Most of this class is pretty much the same as the villageState object. Let’s see what changed:

  1. The two properties moved inside the constructor method.
  2. These properties are no longer hard-coded to “Post Office” and PARCELS. Now we can build states by passing them as arguments.
  3. The constructor is really a function. That’s why we assign properties inside a constructor with = instead of :.
  4. The move method stays as is until the last two lines. We delete them and instead return a new instance with destination and undeliveredParcels. Notice that the logic has not changed. Only instead of modifying a global variable, we return new states. And yes, we’re using our newly defined class inside the class itself!

Testing the Solution

Make sure you comment out the call to runRobot.

let firstState = new VillageState("Post Office", PARCELS)
console.log(firstState)
// VillageState {currentRobotPlace: 'Post Office', remainingParcels: Array(5)}

Bam! We have a new state with the post office as the starting point and PARCELS for the tasks.

Let’s move around:

let nextState = firstState.move('Marketplace')
console.log(nextState)
// VillageState {currentRobotPlace: 'Marketplace', remainingParcels: Array(5)}

Bam! We’re now in the marketplace.

Let’s look back and see if anything changed:

console.log(firstState.currentRobotPlace)
// Post Office
console.log(nextState.currentRobotPlace)
// Marketplace

Cool. Each state lives its own life and we can still move around and keep track of the history.

Let’s test a move from the post office to the farm:

firstState.move('Farm')
// undefined

Darn it!

Remember when the state was an object, we wanted the state to be updated if the location we were moving towards was connected to the current place, and had the function do nothing otherwise. And that’s exactly what’s happening here: nothing 😄

To fix it, we have to tell the move method to just return the current state for this particular scenario. Do you remember how can we access the current state from within a class?

Yes. this.

So we need to add return this AFTER the if statement. It tells the move method to just return the current state as is if the condition above did not trigger.

And this is how the VillageState class looks like at the end:

class VillageState {
constructor(place, parcels) {
this.currentRobotPlace = place;
this.remainingParcels = parcels;
}

move(destination) {
if (roadGraph[this.currentRobotPlace].includes(destination)) {
let parcels = this.remainingParcels

let movedParcels = parcels.map(parcel => {
if (parcel.place != this.currentRobotPlace) return parcel
return { place: destination, address: parcel.address }
})

let undeliveredParcels = movedParcels.filter(
parcel => parcel.place != parcel.address
)

return new VillageState(destination, undeliveredParcels)
}
return this // return current state if the condition above did not trigger
}
}

And to test it:

firstState.move('Farm')
// VillageState {currentRobotPlace: 'Post Office', remainingParcels: Array(5)}

The robot stays at the post office.

Running the Robot with the ‘VillageState‘ class

In the object version of the code, we ran the robot like this:

runRobot(villageState, routeRobot, mailRoute)

We need to pass in a state. We used to pass the global variable villageState but now that the state is defined by a class, we need to initialize a first state for this call to work:

let newVill = new VillageState("Post Office", PARCELS)

newVill is an instance of the village state that behaves exactly like the villageState of the old code. But before we can use it in runRobot to start the program, we need to make one final change to runRobot. I promise this will be the last time we edit existing code 😅

Please have a look at the current runRobot function

function runRobot(state, robot, routeMemory) {
let turns = 0
while (state.remainingParcels.length > 0) {
let action = robot(state, routeMemory)
state.move(action.direction)
routeMemory = action.routeMemory
console.log(`Moved to ${action.direction}`)
turns++
}
console.log(`Done in ${turns} turns`)
}

Do you remember when we added state. in front of the call to move? In the old code, this call modified the global variable villageState; in other world, it wasn’t a pure function. We needed it only for its side effect of updating the global state. But now, we have newly generated states any time move is called. Therefore, to update the state property inside the loop, we need to re-assign the state instead of just calling state.move. Can you think how?

The good news is that it’s as easy as simply adding state = in front of that call.

// inside runRobot
// ...
state = state.move(action.direction)
// ...

To further explain the logic, think what did state.move(action.direction) used to do in the old code versus now. Previously, this call modified the global villageState. Every time runRobot called this function, villageState was instantly updated and therefore the next call had access to the updated state. In the class version, state.move(action.direction) does not modify anything, but returns a new state. Therefor, in the same way that we update a counter with i=i+1, we capture and store the updated state for the next call in the loop.

And now, we can run our virtual world built using classes:

runRobot(newVill, routeRobot, mailRoute)

Procedurally Generating a Random Village State

Have you noticed running our world with the routeRobot always finished in 13 turns?

It’s because we’ve been using a hard-coded PARCELS array since the beginning which happened to finish in 24 turns. In the book’s example, a random array is procedurally generated each time the program is run. This functionality is achieved by a static method inside the VillageState class. This piece of code has nothing to do with the robot’s logic and is just there to illustrate the concept of adding static methods to classes.

Note: I recommend reading the Static properties and methods tutorial from javascript.info to learn more about static methods.

To summarize what a static method is, at least in the context of this project, it is a method that’s accessed through the class itself and not the instance of the class. For example, move could be called by any instance of VillageState, which is newVill in our code. You won’t do VillageState.move as it’s meaningless. However, the random method we’re going to build next belongs to the class itself. It’s the class that’s going to build a random state and not any one instance of the class.

There are two ways to add such methods to a class: either using the static keyword in front of the method inside the class, or by directly adding a property to the constructor:

className.staticMethod = function(){}

we’re going to build a new method called random that generates an array of parcels with a given length. Remember that the content of each parcel is nothing but one of the 11 places assigned to two properties called place and address.

Challenge

Add a static method called random to VillageState with the following logic:

  • It has a parcelCount parameter. Give the parameter a default value (like 5).
  • The method should pick any two places (from the 11 available places in town) in random and return them in the format similar to {place: "Town Hall", address: "Marketplace"}
  • You need to account for the possibility of the two random places being the same. We don’t want that; a parcel that has the same location and delivery address is meaningless.
  • Have the function make as many objects like these as the parcelCount and add them to an array
  • Make the method return a new instance of VillageState, passing in any starting position as its first parameter and the resulting array from the previous step as its second.
  • To test the code, comment out the call to runRobot and run VillageState.random() a couple of times. Examine the remainingParcels array to make sure the content is random.

Here’s the staring code for you to complete:

VillageState.random = function(parcelCount = 5) {

}

Hints

  • Use the helper function randomPick and the roadGraph constant to pick the random place.
  • You only need to access the keys of roadGraph. Use Object.keys() to do that.
  • Think of using a do...while loop to deal with the two random places being the same issue. Basically keep picking random places until you get a place that is NOT equal to the first randomly picked place.

Solution

Let’s start by initializing an empty array. We’ll be adding parcels to this array and return it in the end:

VillageState.random = function(parcelCount = 5) {
let parcels = []
}

We’ll run a loop that’s parcelCount in length, then pick two random places and push them to the array during each iteration:

VillageState.random = function(parcelCount = 5) {
let parcels = [];
for (let i = 0; i < parcelCount; i++) {
// pick two random locations
let address = randomPick(Object.keys(roadGraph));
let place = randomPick(Object.keys(roadGraph));

// push them to the array
parcels.push({place, address});
}
}

Now we have a parcels array which holds random places for the place and address properties. Simply pass it as an argument, together with your desired starting position, like “Post Office” to create a new instance of the VillageState class:

VillageState.random = function(parcelCount = 5) {
let parcels = [];
for (let i = 0; i < parcelCount; i++) {
let address = randomPick(Object.keys(roadGraph));
let place = randomPick(Object.keys(roadGraph));

parcels.push({place, address});
}
// return a new instance
return new VillageState("Post Office", parcels);
}

Testing the Solution

VillageState.random()
// VillageState {currentRobotPlace: 'Post Office', remainingParcels: Array(5)}

We got an instance of the village state class that’s exactly like the hard-coded simple object we’ve been using so far. In fact, let’s look inside and see:

The content of a random world

You see a currentRobotPlace, a remainingParcels, and if you look inside the prototype, you’ll find the move method.

If you run this line enough times, you will come across parcels with matching place and address. We need to tweak the code to deal with this edge case:

VillageState.random = function(parcelCount = 5) {
let parcels = [];
for (let i = 0; i < parcelCount; i++) {
let address = randomPick(Object.keys(roadGraph));
let place = randomPick(Object.keys(roadGraph));

// the tweak is here
do {
place = randomPick(Object.keys(roadGraph));
} while (place == address);
// end of tweak

parcels.push({place, address});
}
return new VillageState("Post Office", parcels);
}

Basically, after we declare the place variable, we keep re-assigning it until the while condition returns true. We can’t use an if here because the call to pickRandom may pick the same matching locations.

Now you can rest assured you won’t be having redundant parcels.

Run a Randomly Generated World

To run the world with randomly generated parcels, use the random static method we just defined instead of passing newVill

runRobot(VillageState.random(), routeRobot, mailRoute)

Now you will see the number of turns change randomly every time you run the program.

And with that I end my explanation of Eloquent Javascript chapter 7.

Conclusion

I had a lot of fun writing this tutorial. Many concepts started to deeply sink in as I was reverse engineering the code to be as simple as possible. I had multiple aha moments myself which is the beauty of trying to explain the code you think you already know.

As for the book itself, I love it tremendously. Granted, it’s more about teaching programming concepts and algorithims than Javascript, but I firmly believe it’s awesome that we have options like these. They fill the gap between pure theory and the application. Nonetheless, I acknowledge that this book is not totally beginner friendly. But I promise the more you push the easier it becomes. I found javascript.info to be an excellent companion. There I usually find a nicely written practical tutorial whenever I come across a concept that’s not fully explored in the book. Don’t lose hope and keep pushing.

Good luck.

The Full Code (Part 3)

// Global Variables
const ROADS = [
"Alice's House-Bob's House",
"Alice's House-Cabin",
"Alice's House-Post Office",
"Bob's House-Town Hall",
"Daria's House-Ernie's House",
"Daria's House-Town Hall",
"Ernie's House-Grete's House",
"Grete's House-Farm",
"Grete's House-Shop",
"Marketplace-Farm",
"Marketplace-Post Office",
"Marketplace-Shop",
"Marketplace-Town Hall",
"Shop-Town Hall",
]

const PARCELS = [
{ place: "Daria's House", address: "Post Office" },
{ place: "Bob's House", address: "Ernie's House" },
{ place: "Post Office", address: "Alice's House" },
{ place: "Ernie's House", address: "Farm" },
{ place: "Alice's House", address: "Shop" },
]

const mailRoute = [
"Alice's House", "Cabin", "Alice's House", "Bob's House",
"Town Hall", "Daria's House", "Ernie's House",
"Grete's House", "Shop", "Grete's House", "Farm",
"Marketplace", "Post Office"
]

class VillageState {
constructor(place, parcels) {
this.currentRobotPlace = place
this.remainingParcels = parcels
}

move(destination) {
if (roadGraph[this.currentRobotPlace].includes(destination)) {
let parcels = this.remainingParcels

let movedParcels = parcels.map(parcel => {
if (parcel.place != this.currentRobotPlace) return parcel
return { place: destination, address: parcel.address }
})

let undeliveredParcels = movedParcels.filter(
parcel => parcel.place != parcel.address
)

return new VillageState(destination, undeliveredParcels)
}

return this
}
}

function buildGraph(roads) {
let graph = Object.create(null)
const modifiedRoads = roads.map(road => road.split("-"))

for (let road of modifiedRoads) {
let [start, end] = road
graph[start] ? graph[start].push(end) : (graph[start] = [end])
graph[end] ? graph[end].push(start) : (graph[end] = [start])
}
return graph
}

const roadGraph = buildGraph(ROADS)

function runRobot(state, robot, routeMemory) {
let turns = 0
while (state.remainingParcels.length > 0) {
let action = robot(state, routeMemory)
state = state.move(action.direction)
routeMemory = action.routeMemory
console.log(`Moved to ${action.direction}`)
displayParcels(state.remainingParcels)
turns++
}
console.log(`Done in ${turns} turns`)
}

// Robot Functions
function randomRobot(state) {
return { direction: randomPick(roadGraph[state.currentRobotPlace]) }
}

function routeRobot(state, routeMemory) {
if (routeMemory.length == 0) {
routeMemory = mailRoute
}
return { direction: routeMemory[0], routeMemory: routeMemory.slice(1) }
}

// Helper Functions
function randomPick(array) {
let choice = Math.floor(Math.random() * array.length)
return array[choice]
}

function displayParcels(parcels) {
for (let parcel of parcels) {
console.log(
` Parcel ${parcel.address[0]} > Place: ${parcel.place}, Address: ${parcel.address}`
)
}
}

VillageState.random = function (parcelCount = 5) {
let parcels = []
for (let i = 0; i < parcelCount; i++) {
let address = randomPick(Object.keys(roadGraph))
let place = randomPick(Object.keys(roadGraph))
do {
place = randomPick(Object.keys(roadGraph))
} while (place == address)

parcels.push({ place, address })
}
return new VillageState("Post Office", parcels)
}

// Run the World
runRobot(VillageState.random(), routeRobot, mailRoute)

--

--