Original notes for JavaScript360.org — project concept inspired by Eloquent JavaScript (4th ed.) by Marijn Haverbeke, built here with an original town layout, code, and naming. See the full attribution note.
Chapter 7 · Project: A Delivery Robot
Estimated time: ~40 minutes · Chapter type: Guided project
This is the first of five project chapters. Project chapters are deliberately less hand-holdy than concept chapters — the goal is to make you combine tools from earlier chapters (objects, arrays, functions, closures) to solve one meaningfully-sized problem, the way you would on the job.
The scenario
We're modeling a small town as a graph: places (nodes) connected by roads (edges). A delivery robot starts somewhere in town holding a bag of parcels, each addressed to a different place, and has to visit every destination and drop off the matching parcel — in as few moves as possible.
Step 1: model the town
A plain object mapping each place to its neighbors is enough to represent the graph:
const roads = {
PostOffice: ['Market', 'Library'],
Market: ['PostOffice', 'TownHall', 'Bakery'],
Library: ['PostOffice', 'School'],
TownHall: ['Market', 'School', 'Park'],
Bakery: ['Market', 'Park'],
School: ['Library', 'TownHall', 'Station'],
Park: ['TownHall', 'Bakery', 'Station'],
Station: ['School', 'Park'],
};
Step 2: represent state immutably
A recurring, valuable pattern: instead of mutating one shared "world" object as the simulation runs, each step produces a brand-new state object, leaving the old one untouched. This makes it trivial to log every step of a run, replay it, or write tests that check "does state A correctly transition to state B" without worrying about hidden mutation.
class TownState {
constructor(place, parcels) {
this.place = place; // where the robot currently is
this.parcels = parcels; // array of { address } still to deliver
}
moveTo(destination) {
if (!roads[this.place].includes(destination)) {
throw new Error(`No direct road from ${this.place} to ${destination}`);
}
const remaining = this.parcels.filter((p) => p.address !== destination);
return new TownState(destination, remaining);
}
get isDone() {
return this.parcels.length === 0 && this.place === 'PostOffice';
}
}
Notice moveTo doesn't change this — it returns a new TownState. parcels.filter drops off any parcel addressed to the destination we just arrived at.
Step 3: a naive robot
The simplest possible strategy: pick a random reachable neighbor each turn. It'll eventually finish (assuming the graph is connected), but with no regard for efficiency:
function randomRobot(state) {
const options = roads[state.place];
const choice = options[Math.floor(Math.random() * options.length)];
return { direction: choice };
}
Step 4: run a simulation
A runDelivery function repeatedly asks the robot for its next move, applies it, and counts turns until the state is done:
function runDelivery(state, robot, maxTurns = 1000) {
for (let turn = 0; turn < maxTurns; turn++) {
if (state.isDone) {
console.log(`Delivered everything in ${turn} turns.`);
return turn;
}
const { direction } = robot(state);
state = state.moveTo(direction);
}
console.log('Robot ran out of turns.');
return -1;
}
Try it with a couple of parcels and the naive robot — you'll typically see it take dozens of turns for what should be a handful of moves, because it has no memory and no plan.
Step 5: pathfinding with breadth-first search
A smarter robot needs a route planner — given a starting place and a target, find the shortest sequence of roads between them. Breadth-first search (BFS) is the standard tool: explore the graph one "ring" of distance at a time, and the first time you reach the target, you've found a shortest path.
function findRoute(from, to) {
const work = [{ place: from, route: [] }];
const visited = new Set([from]);
while (work.length > 0) {
const { place, route } = work.shift();
if (place === to) return route;
for (const neighbor of roads[place]) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
work.push({ place: neighbor, route: [...route, neighbor] });
}
}
}
return []; // no route found
}
findRoute('PostOffice', 'Station');
// e.g. ["Library", "School", "Station"]
work acts as a queue (shift() pulls from the front); visited prevents revisiting a place and looping forever. Because BFS explores in order of distance, the first route it finds to any given place is guaranteed to be the shortest.
Step 6: a planning robot
A smarter robot keeps a plan — an internal to-do list of moves — as part of its own memory (separate from TownState, which only tracks the world). When the plan is empty, it computes a fresh route to the nearest undelivered parcel; otherwise, it just follows the next queued move.
function planningRobot(state, memory = []) {
if (memory.length === 0) {
if (state.parcels.length > 0) {
const target = state.parcels[0].address;
memory = findRoute(state.place, target);
} else {
memory = findRoute(state.place, 'PostOffice'); // head home once empty-handed
}
}
const [direction, ...rest] = memory;
return { direction, memory: rest };
}
Compare the turn counts between randomRobot and planningRobot on the same set of parcels — the planning robot should finish in roughly the number of moves the shortest possible route actually requires, while the random one takes many times longer on average.
What this project actually taught you
- Modeling a problem as data (a graph as an object of arrays) before writing any logic
- Representing changing state immutably, producing new snapshots instead of mutating shared state
- A first taste of a genuinely useful algorithm (BFS) applied to a concrete problem, not an abstract exercise
- Separating "the world's state" from "an agent's private memory" — a pattern that shows up again in Chapter 16's platform game
Extend it yourself
- Give the robot multiple parcels and have it plan a route that visits them in the order that minimizes total travel, not just "nearest first."
- Add "traffic" — some roads cost 2 turns instead of 1 — and adapt
findRouteto account for weighted edges (this turns BFS into Dijkstra's algorithm). - Visualize a run: log the robot's position after each turn and render it as a simple text-based trace.