Guide Zorb
Click or tap any open cell. Zorb will find his own way there.
- Route preview
- Route taken
- Rock
# Auto-navigating our Grid using Breadth-First Search (BFS) by kirupa (https://www.kirupa.com/me/index.htm) | filed under Data Structures and Algorithms (https://www.kirupa.com/data_structures_algorithms/index.htm) By now, we've got the basics of working with our grid covered. We learned how to represent our grid, move Zorb around it, deal with boundaries, and even got a bit of collision detection figured out. What we are going to do here is kick what we've learned up a few notches. Instead of us moving and nudging Zorb along ourselves, we are going to give Zorb a destination and have him navigate the grid himself. He will do this navigation using our old friend, Breadth-First Search (BFS) (https://www.kirupa.com/data_structures_algorithms/dfs_bfs.htm). This is going to be a hoot! Onwards! ## What We're Going to Build As always, before we get fully into it, let's play with the finished example to give us an idea of what we'll be working towards (open in new window (https://www.kirupa.com/data_structures_algorithms/examples/grid_movement_pathfinding.htm?v=20261009-2)): Interactive example: Click a destination cell and watch Zorb walk around the rocks to reach it: https://www.kirupa.com/data_structures_algorithms/examples/grid_movement_pathfinding.htm?v=20261009-2 Hover over any open cell and click / tap to have Zorb navigate to it. Notice how Zorb navigates. He avoids all of the obstacles. We can see this clearly when we hover over a destination, since that shows the path Zorb will take. In the rest of the article, we'll learn how to build the logic that makes this navigation work. ## Some Obvious Ideas That Don't Work Before reaching for the final algorithm that Zorb can use to navigate our obstacle-filled grid, let's start with what seems like an obvious (yet incorrect) approach to take. At each turn, Zorb will take a step that moves him closer to the goal. To dive deeper into this obvious insight: 1. If the destination is below, Zorb tries to move down 2. If the destination is above, Zorb tries to move up 3. If the destination is to the left, Zorb tries to move left 4. If the destination is to the right, Zorb tries to move right 5. Keep repeating the above steps until Zorb reaches his destination This seems like a viable plan, though, right? As it turns out, not really. To see where this approach falls apart, let's shrink the world back down to the 5-by-5 grid we used last time: Image: A 5 by 5 grid labeled zero through four across the top and left, with rocks at (3, 1), (2, 2), (0, 3), and (4, 4). at https://www.kirupa.com/data_structures_algorithms/images/55grid_example_200.png We are going to put Zorb at (2, 1) and put his destination at (3, 4), down in the bottom row: Image: Zorb stands at (2, 1) in the 5 by 5 rock grid, with a flag marking his destination at (3, 4) in the bottom row. at https://www.kirupa.com/data_structures_algorithms/images/zorb_destination_200.png The gap between Zorb and the destination is one column to the right and three rows down: Image: Red arrows measure the gap between Zorb at (2, 1) and the flag at (3, 4): one column to the right and three rows down. at https://www.kirupa.com/data_structures_algorithms/images/zorb_destination_annotated_200.png To get to the destination, Zorb attempts to move down one step. As we can see, down from (2, 1) is (2, 2), and (2, 2) is a rock (maybe a rock....lobster? (https://www.youtube.com/watch?v=pn26ZJhSJ94)). So, moving down doesn't work. Let's have him move right instead. That plan is also foiled, because to the right of Zorb at (3, 1) is another rock. Both of his attempts to move closer to the destination are blocked right at the start. The only two moves he has left are to go up to (2, 0) or left to (1, 1): Image: The cells above Zorb at (2, 0) and to his left at (1, 1) are highlighted as the only two moves open to him. at https://www.kirupa.com/data_structures_algorithms/images/zorb_proposed_paths_200.png Both of them increase the distance to the goal. To get to (3, 4) at all, Zorb has to start by walking away from it. Going away for a brief bit before moving towards your destination isn't a bad strategy. It may be the only strategy if you are blocked like Zorb is right now. What makes this tricky is that it now adds more rules and complications. For example, Zorb now needs to remember which cells he has already tried. Otherwise, he will try the same corner repeatedly. The other detail to keep in mind, and this is a HUGE detail, is that what Zorb sees is only the cells in his immediate vicinity and any he has already explored: Image: Only Zorb's cell and its four neighbors are clearly visible, including the rocks to his right and below him, while the rest of the grid and the flag are faded out. at https://www.kirupa.com/data_structures_algorithms/images/zorb_what_sees_200.png Zorb doesn't know how big the grid is. He doesn't know where the other obstacles are. He doesn't know where the destination is. In many real-world scenarios, an important navigation activity is to figure out if there is even a destination. Only we, as human observers, can see the full grid and where all the obstacles are, and visually plot a path to the destination. That too fails for large, complex grids where even you and I can't fully make sense of the full environment. So basically, having Zorb try to optimize his way to the destination won't work unless we tell him where the destination is. The only other solution is to have Zorb try every possible walk through the grid and keep the best one. That works, but this brute-force version is far too slow to be useful. This isn't a problem for a small surface like our 5-by-5 grid. In real-world situations, your grid may be 10s or 100s or even 1,000s of times larger than our grid here. There won't be enough seconds in the universe (insert dramatic pause) to exhaustively plot every path. The solution we want will sit snugly between our two approaches. We want something that explores, like the brute-force version, but we also want it to be smart enough to never explore the same cell twice and to stop the moment it finds the destination. ## Breadth-First Search to the Rescue For a moment, imagine Zorb sends out a series of drones to explore the area and report back what they find. As they go from cell to cell, each drone records how far away from Zorb it is. The drone that arrives at the destination first is the one that got there by the shortest route available. If this idea sounds like a great one, that's because what we just described is how our old friend breadth-first search, commonly shortened to BFS, behaves. It works on a grid like this: 1. Mark Zorb's cell as reached in 0 steps. 2. Find every open neighbor of that cell and mark those as reached in 1 step. 3. Find every open, unmarked neighbor of those cells and mark them as 2 steps. 4. Keep going, one full ring at a time, until the destination gets marked. The critical word in there is unmarked. Once a cell has a number, it keeps it. We never come back and overwrite it, because any later visit arrived on a route at least as long as the first one. #### Note: You May Have Seen This Before If BFS rings a bell, it's because you and I may have talked about it already in Depth-First Search and Breadth-First Search (https://www.kirupa.com/data_structures_algorithms/dfs_bfs.htm). The only difference is that we looked at it in the context of graphs instead of grids. As we will talk about a bit later, a grid is a graph. In a grid, every open cell is a node, and every pair of open neighbors is an edge. Let's run that on our 5-by-5 grid and actually watch it happen. Zorb starts at (2, 1), so that cell gets a 0: Image: Zorb's starting cell at (2, 1) is marked 0, and every other cell is still unmarked. at https://www.kirupa.com/data_structures_algorithms/images/zorb_destination_zero_200.png Now the first ring. Zorb's four neighbors are (2, 0) above, (2, 2) below, (1, 1) to the left, and (3, 1) to the right. Two of those neighbors are rocks, so we ignore them. The other two non-rock, open spaces get a 1: Image: The open cells at (2, 0) and (1, 1) are marked 1, while the rocks to Zorb's right and below him are highlighted and skipped. at https://www.kirupa.com/data_structures_algorithms/images/zorb_destination_one_200.png Our explorer drones continue, and they will take off and explore from the cells marked as 1. From (2, 0) we reach (1, 0) and (3, 0). From (1, 1) we reach (0, 1) and (1, 2). This results in us discovering four new cells, and we mark them with a 2: Image: Four more cells at (1, 0), (3, 0), (0, 1), and (1, 2) are marked 2. at https://www.kirupa.com/data_structures_algorithms/images/zorb_destination_two_200.png This pattern now repeats, with drones from each of the cells marked with a 2 exploring their immediate neighbors. This brings us to ring 3, which adds (0, 0), (4, 0), (0, 2), and (1, 3). Ring 4 adds (4, 1), (2, 3), and (1, 4). Ring 5 adds (4, 2), (3, 3), (0, 4), and (2, 4). And on ring 6, one of the cells we mark is (3, 4), which is where we were going: Image: Every open cell is labeled with its step count from Zorb, and the destination at (3, 4) is outlined with a 6. at https://www.kirupa.com/data_structures_algorithms/images/zorb_destination_reached_200.png Every open cell on the grid now has a number that signifies the number of steps it sits from Zorb. Our destination has a number of 6. This means that our destination is 6 steps away from Zorb. When we look at this grid, let's pay attention to where the large values are. For example, look at (3, 2), which sits diagonally next to Zorb, with a 6 on it. It's physically close to Zorb yet one of the last cells our drones reached. This is because the only ways in are around through the bottom or down the right-hand side. Now that we've added obstacles, using the straight-line distance as a heuristic doesn't work anymore. ## Leaving Breadcrumbs At this moment, we know the trip takes six steps. What we don't know is which six steps will lead Zorb to the destination. The solution is to leave behind a digital breadcrumb. Every time we (or our drones!) explore a new cell, we will write down which cell we came from. We can visualize this by using arrows. So when we explore (2, 0) and (1, 1) as part of ring 1, we will also record that they were both reached from Zorb's starting position of (2, 1): Image: The cells marked 1 at (2, 0) and (1, 1) each have an arrow pointing back to Zorb at (2, 1). at https://www.kirupa.com/data_structures_algorithms/images/zorb_destination_one_arrow_200.png We can repeat this process for every cell we explore. At the end of ring 2, we'll see something like this: Image: Each cell marked 2 has an arrow pointing back to the cell marked 1 that discovered it. at https://www.kirupa.com/data_structures_algorithms/images/zorb_destination_two_arrow_200.png When we do that for every cell, we can see a clear path from each cell back to Zorb: Image: Every open cell shows its step count and an arrow pointing back to the neighbor it was reached from, forming trails that lead to Zorb. at https://www.kirupa.com/data_structures_algorithms/images/zorb_destination_arrowed_200.png This visualization is a bit too busy, so let's simplify it and emphasize the path from Zorb to our destination: Image: The arrows are faded except for the highlighted route from the destination at (3, 4) through (2, 4), (1, 4), (1, 3), (1, 2), and (1, 1) back to Zorb. at https://www.kirupa.com/data_structures_algorithms/images/zorb_destination_arrowed_simple_200.png Reading the route is now just a fun exercise in following arrows. Start at the destination and keep asking each cell where it came from: (3, 4) came from (2, 4) (2, 4) came from (1, 4) (1, 4) came from (1, 3) (1, 3) came from (1, 2) (1, 2) came from (1, 1) (1, 1) came from (2, 1) <--- Zorb, so we stop That gives us the trip backwards. Flip it around and we have the path Zorb should walk to reach his destination: (2, 1) → (1, 1) → (1, 2) → (1, 3) → (1, 4) → (2, 4) → (3, 4) What we just did was take the 6-move "answer" we got in the previous section and make it actionable by adding the breadcrumbs to each cell with the detail on how someone landed there. Now that we've seen how BFS can help with our pathfinding problem, it is time to go one level deeper and look at the implementation. ## Putting it All Together Building the grid, the rocks, and Zorb's walking animation is mostly frontend work, and building all of that from scratch would bury the search code we actually came here for. So we won't. Instead, we'll start from a copy of the finished example with its pathfinding scooped out. Grab the starter file from GitHub (https://github.com/kirupa/kirupa/blob/master/examples/grid_movement_pathfinding_start.htm), save it as grid_movement_pathfinding.htm, and open it in your browser. Click any open cell. Zorb stays right where he is, and the line under the grid reads something like Zorb will eventually walk to (7, 2). Click a rock and you get the same cheerful promise. Hovering does nothing at all. That's the correct amount of broken for where we are, since everything works except the part where Zorb figures out how to get somewhere. ### A Quick Tour of the Starter If you followed along with Moving Around a Grid and Avoiding Obstacles (https://www.kirupa.com/data_structures_algorithms/moving_around_a_grid_with_obstacles.htm), a lot of this will look familiar. The direction buttons are gone, since nobody is steering Zorb by hand anymore, but the world itself is exactly the same. The syncParentTheme function at the very top of the script only kicks in when the example is embedded inside a kirupa.com page, where it borrows the site's colors, so you can skip right past it. Just below it, we define our world: const rows = 10; const cols = 10; const start = { x: 4, y: 4 }; const zorb = { ...start }; const obstacles = new Set([ "1,1", "2,1", "3,1", "6,1", "7,1", "8,1", "3,2", "8,2", "0,3", "1,3", "3,3", "5,3", "6,3", "8,3", "5,4", "1,5", "2,5", "3,5", "5,5", "7,5", "8,5", "3,6", "7,6", "1,7", "5,7", "7,7", "1,8", "2,8", "3,8", "5,8" ]); Our grid is 10 cells wide and 10 cells tall, and Zorb starts at (4, 4). Just like before, x counts columns from the left and y counts rows from the top, so moving up means subtracting 1 from y. Every rock is an "x,y" string in a Set, so "5,4" is the rock sitting just to Zorb's right. The zorb object tracks where he is right now, while start remembers where he began so that the Reset button knows where to put him back. A little further down are four small helpers that we'll lean on: function key(x, y) { return `${x},${y}`; } function inBounds(x, y) { return x >= 0 && x < cols && y >= 0 && y < rows; } function isObstacle(x, y) { return obstacles.has(key(x, y)); } function cellAt(x, y) { return cells[y * cols + x]; } key turns a coordinate into that "x,y" string format. inBounds tells us whether a coordinate is on the grid at all, and isObstacle tells us whether a rock fills it. cellAt hands back the button for a coordinate. It can do that with a little math because every cell is stored in the cells array in reading order, left to right and then top to bottom, which puts the cell at (x, y) at position y * cols + x. Right below cellAt is a comment that reads // Our breadth-first search code goes here. Hold on to that thought, because most of our new code is going there. The cells themselves are built by a loop near the bottom of the script. Each one is a real button instead of a plain div, so it can take keyboard focus, and pressing Enter or Space on it counts as a click without any extra code from us. Every button also stores its coordinates in data-x and data-y, so when an event fires, the cell can tell us which square it is. Rock cells get an obstacle class, and the CSS draws a rock inside them. Zorb is a single element floating above the cells, and placeZorb slides him to whatever zorb.x and zorb.y say. His element is exactly one cell in size, so moving him one column over is a shift of 100%, and a CSS transition handles the animation for us. That brings us to walk. Hand it an array of cells, and it moves Zorb along them one cell every 160 milliseconds, numbering each cell as he goes. The smaller helpers around it, setStatus, clearPreview, clearTrail, and markTrail, update the message under the grid and add or remove the highlights on our cells. Nothing in the starter calls walk yet, but nothing stops us from calling it ourselves. Open your browser's developer console. Don't worry if it's already showing an error about a font from kirupa.com. Browsers won't load that font into a page opened straight from your computer, so the heading falls back to a plainer font and nothing else changes. Now paste in this path we wrote by hand. If your browser warns you about pasting into the console, type allow pasting like it asks and then paste again: walk([ { x: 4, y: 4 }, { x: 4, y: 3 }, { x: 4, y: 2 }, { x: 5, y: 2 } ]); Zorb walks up two cells and then right one, numbering the cells 0 through 3 as he goes. Take a good look at the shape of that array, because producing arrays like it is the entire job of the code we're about to write. It starts on the cell Zorb is already standing on, ends on the destination, and each entry is one step up, down, left, or right from the one before it. Also, walk trusts us completely. Hand it a path that cuts through a rock and Zorb will happily walk right over it, so keeping him off the rocks is our search's job. Click the Reset button to send Zorb back to (4, 4) when you're done. The last major piece is the event handling. Clicking a cell calls goTo(x, y). Hovering over a cell with a mouse, or moving to it with the keyboard, calls showPreview(x, y). Touch screens skip the hover, since a finger can't hover over anything. Moving the mouse off the grid clears the preview, the arrow keys move focus from cell to cell, and the Reset button puts Zorb back where he started. Here are the two functions those events call, and both are placeholders for now: function showPreview(x, y) { // Once Zorb can find his own way, this highlights his route to (x, y). } function goTo(x, y) { // Once Zorb can find his own way, this sends him walking to (x, y). setStatus(`Zorb will eventually walk to (${x}, ${y}).`); } That's why the starter makes promises it can't keep. Our job is to add the search code where that placeholder comment sits and then fill in these two functions. ### Labeling the Destination In our pictures, the destination is the cell with the flag in it. Our search needs a way to recognize that cell when it gets there, without ever being told where it is. Think back to our drones. They don't fly toward the flag, because they don't know where it is. They spread out, and the first drone to land on the flag's cell is the one that found the shortest route. So we need two things: a label that marks which cell holds the flag, and a question the search can ask about whatever cell it's on. Replace the // Our breadth-first search code goes here. comment with the following: // Which cell holds the flag. findPath never reads this. It only asks // isDestination about each cell it reaches. let destination = null; function isDestination(x, y) { return key(x, y) === destination; } destination holds the flag's cell as an "x,y" string, the same format our rocks use. It starts out as null, because there's no flag until someone picks a cell. isDestination answers true or false for one cell at a time, and that's the only peek our search ever gets at where the flag is. It can't ask where is the flag? It can only ask is the flag right here? ### Searching for a Path Next, our search needs to know which ways it's allowed to step. If you went through the obstacle tutorial, you'll recognize this table. Add it right below isDestination: const DIRECTIONS = { up: [0, -1], down: [0, 1], left: [-1, 0], right: [1, 0] }; Each pair says how much x and y change when we take one step in that direction. Since y counts down from the top, up is -1. Now for the search itself. Add findPath right below DIRECTIONS: // Breadth-first search. It never looks up where the destination is. It fans // out one ring of neighbors at a time and only recognizes the destination // when it reaches that cell, so the first arrival is in the fewest steps. function findPath(startX, startY) { const cameFrom = new Map([[key(startX, startY), null]]); const frontier = [{ x: startX, y: startY }]; while (frontier.length > 0) { const current = frontier.shift(); if (isDestination(current.x, current.y)) { return buildPath(cameFrom, current); } for (const [xOffset, yOffset] of Object.values(DIRECTIONS)) { const nextX = current.x + xOffset; const nextY = current.y + yOffset; const nextKey = key(nextX, nextY); if (!inBounds(nextX, nextY)) { continue; } if (isObstacle(nextX, nextY)) { continue; } if (cameFrom.has(nextKey)) { continue; } cameFrom.set(nextKey, current); frontier.push({ x: nextX, y: nextY }); } } return null; } Look at what findPath takes: the coordinates of the cell to start from, and nothing else. The destination isn't one of its inputs. Everything it learns about the flag comes from asking isDestination about the cell it's currently looking at. Two collections do all the work. The frontier is the list of cells we've found but haven't explored from yet. Think of it as the line of cells waiting for their drones to take off. cameFrom is our breadcrumb map. For every cell we've reached, it records which cell we came from, and it pulls double duty as our record of which cells we've already marked. That's why the starting cell goes into cameFrom right away, pointing at null. It counts as marked from the very beginning, so the search never wanders back into Zorb's cell. Each trip through the while loop takes the cell at the front of the frontier and checks whether it's the destination. If it isn't, Object.values(DIRECTIONS) hands us the four direction pairs, and const [xOffset, yOffset] pulls the two numbers out of each pair so we can work out the neighbor in that direction. Any neighbor that makes it past our checks gets a breadcrumb pointing back to the current cell and joins the back of the frontier. Those three continue statements are the ripple rules, one line each. Skip anything off the grid. Skip anything filled with rock. Skip anything we've already marked, because it already has a number at least as small as the one we could give it now. The ring-by-ring behavior comes from shift. Because we pull cells off the front and push new ones onto the back, everything one step away gets processed before anything two steps away does. That queue ordering is the breadth-first part. Swap shift for pop and you have depth-first search, which will happily march into a far corner before checking the cell next door. Notice where the destination check sits: at the top of the loop, on the cell we just took off the front of the list. Cells come off in ring order, so the moment the destination comes off, we know no other route could get there in fewer steps. Checking here also covers the case where Zorb is already standing on the flag, since his cell is the very first one to come off. Notice the two ways out of this function. It returns a path the moment the destination comes off the frontier, and it returns null when the frontier empties without ever finding it. That second case matters. An empty frontier means the ripple has touched every cell it can reach and the destination wasn't among them, so there genuinely is no route. That's exactly what happens when the flag sits on a rock. The search never steps onto a rock, so it explores all 70 open cells, comes up empty, and hands back null. #### Note: Why Turn Cells Into Strings? You might wonder why cameFrom uses keys like "4,3" instead of the { x, y } objects themselves. A Map treats two objects as different keys even when they hold the exact same numbers, so a { x: 4, y: 3 } created twice would count as two different cells. Two strings that read "4,3" always match, so turning each cell into a string gives it exactly one spot in our map. #### Note: When Two Routes Tie There can be more than one shortest route. To reach (9, 2), Zorb can go over the top of the grid, or he can dip below the rocks to his right and come up the right-hand side. Both routes take 11 steps. Our search keeps whichever one reaches the destination first, and that comes down to the order of the entries in DIRECTIONS. Shuffle that order and you may get a different route of the same length. ### Reading the Breadcrumbs findPath hands its answer off to a buildPath function we haven't written yet. Add it right below findPath: // Walk the breadcrumb trail backwards from the goal to the start. function buildPath(cameFrom, goal) { const path = []; let current = goal; while (current) { path.push(current); current = cameFrom.get(key(current.x, current.y)); } return path.reverse(); } This is our arrow-following exercise from earlier, written as code. Starting at the destination, we keep asking cameFrom which cell each one came from. We seeded cameFrom with the starting cell pointing at null, so the loop stops on its own when it walks back to Zorb. The reverse flips the trail into travel order, which gives us an array that starts at Zorb and ends at the destination. That's the exact shape walk wants. Save, refresh, and try it from the console before wiring up any clicks: destination = key(9, 2); findPath(4, 4).length; // 12 destination = key(5, 4); findPath(4, 4); // null, that's a rock Notice that we set destination before calling findPath. The search has no other way of knowing which cell holds the flag, so the label has to be in place before the search starts. The first call reports twelve cells, which is eleven steps plus the cell Zorb is already standing on. That off-by-one is worth burning in now, because it shows up every time we print a step count: a path of n cells is n - 1 moves. Because findPath returns exactly what walk wants, the two snap right together. Put the flag back on (9, 2) and let Zorb loose: destination = key(9, 2); walk(findPath(zorb.x, zorb.y)); Zorb heads up to the top row, runs along it, and drops down to (9, 2), all on his own. Click the Reset button when he gets there. ### Showing the Route on Hover Our search doesn't change anything. It reads the rocks and the flag, then hands back an answer without moving Zorb or touching the grid. That means we can call it while the mouse is just passing over a cell, which is exactly what the hover preview does. Replace the placeholder showPreview with this version: function showPreview(x, y) { clearPreview(); if (walkTimer !== null) { return; } if (x === zorb.x && y === zorb.y) { setStatus(`Zorb is already standing at (${x}, ${y}).`); return; } destination = key(x, y); const path = findPath(zorb.x, zorb.y); if (!path) { cellAt(x, y).classList.add("preview-blocked"); setStatus( isObstacle(x, y) ? `A rock fills (${x}, ${y}). Zorb has nowhere to stand.` : `There is no open route to (${x}, ${y}).`, true ); return; } for (const step of path) { cellAt(step.x, step.y).classList.add("preview"); } cellAt(x, y).classList.add("preview-goal"); const steps = path.length - 1; setStatus( `Route to (${x}, ${y}): ${steps} ${steps === 1 ? "step" : "steps"}.` ); } Going from the top, we first wipe away the old preview. If Zorb is in the middle of a walk, we stop right there and skip the preview. He changes cells every 160 milliseconds, so any route we drew would be out of date almost immediately. Hovering over Zorb's own cell gets a status message and nothing more. Next, we set destination to the hovered cell and search from wherever Zorb is right now, using zorb.x and zorb.y instead of his starting cell. After a walk, Zorb is somewhere new, and the next search has to start from there. If the search comes back null, we give the cell a preview-blocked class, which shades it gray, and use isObstacle to pick the right message. In our grid, every open cell can be reached from every other open cell, so you'll only see the no open route message if you add rocks that wall off part of the grid. Otherwise, every cell on the route gets a preview class, the destination gets a ring on top, and the status message reports path.length - 1 steps, thanks to our off-by-one rule. The stylesheet tints the preview a fainter blue than the trail, so the route you're considering never looks like the route you took. Save and refresh, then run your mouse around the grid. The route should snap into place under the cursor and bend around every rock it meets. Hover over (9, 2) and you'll see the 11-step route along the top. Hover over a rock and the line under the grid tells you there's nowhere for Zorb to stand. If you'd rather use the keyboard, tab into the grid and move around with the arrow keys, and the preview will follow along. ### Walking the Path The last placeholder is goTo. A click runs the same search and then plays the result back one cell at a time. Replace the placeholder goTo with this: function goTo(x, y) { if (x === zorb.x && y === zorb.y) { setStatus(`Zorb is already standing at (${x}, ${y}).`); return; } destination = key(x, y); const path = findPath(zorb.x, zorb.y); if (!path) { stopWalking(); clearPreview(); cellAt(x, y).classList.add("preview-blocked"); setStatus( isObstacle(x, y) ? `A rock fills (${x}, ${y}). Pick an open cell instead.` : `There is no open route to (${x}, ${y}).`, true ); return; } walk(path); } This is the same search our preview runs. The only difference is what we do with the answer. Instead of highlighting the route, we hand it straight to walk. If the answer is null, we stop any walk in progress, shade the cell gray, and explain why. Clicking a new cell while Zorb is still walking works too. The search starts from whatever cell Zorb is on at that moment, and the first thing walk does is call stopWalking, which clears the pending setTimeout so the old trip can't keep stepping underneath the new one. Save, refresh, and click a cell on the far side of the grid. Zorb should set off immediately, thread through the gaps, and leave a numbered trail showing the order he visited each square. Click somewhere else while he's still moving and he should change course without stopping. ### The Full Source That's every change. Here's the full source for grid_movement_pathfinding.htm with all of our pieces in place:
Click or tap any open cell. Zorb will find his own way there.