# Moving Around a Grid and Avoiding Obstacles by [kirupa](https://www.kirupa.com/me/index.htm)   |   filed under [Data Structures and Algorithms](https://www.kirupa.com/data_structures_algorithms/index.htm) In the [previous article](https://www.kirupa.com/data_structures_algorithms/moving_around_a_grid.htm), we took our first major look at working with grids. We learned how to move an element around and keep the movement constrained to the grid's boundaries. In this article, we are going to go one step further. We are going to get into the fun world of collision detection where our movement will be constrained by obstacles in the grid's path. The best part is that we are going to build upon everything we learned and even retain our propose, validate, and commit heuristic with only a few minor tweaks. Zorb is definitely in for an adventure. Onwards! ## What We're Going to Build Before we get all into it, let's play with the finished example first. Zorb is back at (4, 4), but his world now contains rocks: Interactive example: [Move Zorb around a 10 by 10 grid while avoiding rock obstacles and staying inside its boundaries](https://www.kirupa.com/data_structures_algorithms/examples/grid_movement_obstacles.htm?v=20260826-1) Try moving Zorb around just like before by using the arrow keys or WASD on a desktop. On a touch device, swipe across the grid or use the direction buttons. You can also [open our obstacle example in its own window](https://www.kirupa.com/data_structures_algorithms/examples/grid_movement_obstacles.htm?v=20260826-1)! A lot of the movement will be familiar to us from earlier, including not being able to leave the boundaries of our grid. What is new is that we now have obstacles inside the grid that Zorb can't go through. Try moving Zorb through a rock. You can't. The rock is an obstacle that you can only move around but not through. In the following sections, we'll learn more about how to detect obstacles within our grid and ensure Zorb isn't able to go through them. We are going to get a bit into the fun world of ***collision detection***. #### Note: What is Collision Detection? The textbook definition of ***collision*** detection is when two or more objects in a two-dimensional plane overlap or intersect: ![Two circles sitting apart are labeled no collision, and two overlapping circles are labeled collision.](https://www.kirupa.com/data_structures_algorithms/images/collision_no_collision_200.png) This is not to be mistaken with ***Collison*** detection. Collison detection is an advanced algorithm that can detect Stripe Cofounders Patrick or John Collison in any static visual or moving scene ([image source](https://www.rte.ie/news/business/2025/1010/1537907-stripe-co-founders-receive-the-2025-impact-ireland-award/)): ![A photo of the Stripe cofounders with detection boxes drawn around Patrick and John Collison.](https://www.kirupa.com/data_structures_algorithms/images/collision_detection.jpg) Aren't you glad you are learning serious topics here and not on some other resource? ## Detecting Obstacles When we had Zorb moving around our grid initially, the entire grid was wide open. The only place he couldn't move through was an area outside of the grid itself. By adding obstacles like the rocks into our grid, we added an extra constraint that we need to handle. The way we handle it is by modifying our logic for evaluating each candidate cell. If you remember from earlier, the candidate cell is the cell we ***intend*** to move to but haven't ***actually committed*** to moving to yet. As part of evaluating each candidate cell, we now ask ourselves the following additional question: Is the candidate cell already occupied by something Zorb can't move through? The reason we ask this question is that a cell can now exist within our boundary and still be off-limits. For example, take a look at the following 5-by-5 grid: ![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).](https://www.kirupa.com/data_structures_algorithms/images/55grid_example_200.png) Let's take the position (2, 2). This position is safely within our grid, so it passes our boundary check. But, this position does happen to contain a rock. This means Zorb still can't enter it: ![The same 5 by 5 grid with the rock at horizontal position 2 and vertical position 2 highlighted.](https://www.kirupa.com/data_structures_algorithms/images/55grid_example_rock_200.png) This gives every requested move two possible collision checks: - A **boundary collision** check to see if the candidate is outside the grid - An **obstacle collision** check to see if the candidate is inside the grid but occupied If either check returns an invalid move, we reject the candidate and leave Zorb's current position untouched. This entire sequence can be visualized as follows: ![A flowchart takes a direction through candidate creation, a boundary check, and an obstacle check. Boundary and obstacle collisions reject the move, while an open cell commits it.](https://www.kirupa.com/data_structures_algorithms/images/flowchart_grid_move_200.png) Getting back to our grid, suppose Zorb is standing at (2, 1) and we ask him to move right: ![Zorb stands at horizontal position 2 and vertical position 1 in the 5 by 5 grid with an arrow pointing right toward the neighboring rock.](https://www.kirupa.com/data_structures_algorithms/images/55grid_zorb_rock_move_200.png) The right offset is [1, 0], so our existing movement math proposes (3, 1) as Zorb's new position: ![The candidate cell at horizontal position 3 and vertical position 1 is highlighted with a question mark, and it contains a rock.](https://www.kirupa.com/data_structures_algorithms/images/55grid_propose_zorb_200.png) This is just a candidate position, so we haven't actually moved Zorb yet. We are just inspecting the destination to see if it can be a valid move or not. When we do our collision checks, the ***boundary check*** answers true because (3, 1) exists within our grid. Our new ***obstacle check*** answers true because a rock occupies it. Because that second check stops the move, our candidate position is rejected. ## Putting it All Together Now that we have seen how to detect obstacles inside our grid, let's get into the implementation a bit and talk about how we will want to represent obstacles in the first place. Put differently, our code needs to know where the obstacles are. The approach we will take is one where we store each rock's coordinate in a Set: ```js const obstacles = new Set([ "1,1", "2,1", "3,1", "5,4", "1,5", "2,5", "3,5" ]); ``` Each coordinate becomes a string with the horizontal position first and the vertical position second. The rock at (5, 4) is stored as "5,4". Why use a Set? We repeatedly need to ask one yes-or-no question: Does this exact coordinate appear in our obstacle collection? A Set is built for that kind of membership check. It also prevents duplicate coordinates from creating duplicate obstacles. You can learn more about sets in my [Diving into Sets](https://www.kirupa.com/javascript/sets.htm) article. It may seem strange to use a string to represent numerical coordinates. To simplify our detection logic and not worry about strings, we will hide the string formatting inside a small helper: ```js function isObstacle(x, y) { return obstacles.has(`${x},${y}`); } ``` The rest of our movement code can work with normal x and y values. It doesn't need to care how the obstacle collection stores them. ### Starting With Our Boundary Example We are going to build directly on the final example from the earlier boundary tutorial. If you already completed that version, make a copy of your file and keep going. Otherwise, create a new file named grid_movement_obstacles.htm and add the following starter code: ```html Zorb's Obstacle Course

Move Zorb

Zorb starts at (4, 4). Watch out for the rocks!

``` This is our earlier finished example with one small setup change: it links to the obstacle version of the stylesheet. The movement logic still knows only about boundaries, so the page begins as an empty grid. Save the file and open it in your browser. Zorb should move exactly as he did before. He can reach every cell inside the grid, and the four outer edges block him. ### Add the Obstacle Data Our first code change describes where the rocks live. Add this obstacles set immediately after the player object and before DIRECTIONS: ```js const start = { x: 4, y: 4 }; const player = { ...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" ]); const DIRECTIONS = { up: [0, -1], down: [0, 1], left: [-1, 0], right: [1, 0] }; ``` Each item follows our (x, y) convention. For example, "1,3" is the second column and fourth row from the top. We intentionally leave "4,4" out because that is Zorb's starting cell. Save and refresh the page. Nothing will look different yet. We have described the rocks, but our rendering code doesn't read that description. ### Add the Obstacle Lookup Add the following isObstacle helper immediately after inBounds: ```js function inBounds(x, y) { return x >= 0 && x < cols && y >= 0 && y < rows; } function isObstacle(x, y) { return obstacles.has(`${x},${y}`); } function tryMove(direction) { // ... } ``` The template literal turns two coordinate values into the same string format used by our Set. Calling isObstacle(5, 4) checks for "5,4" and returns true. Calling isObstacle(4, 4) returns false. Try both calls in your browser's developer console: ```js isObstacle(5, 4); // true isObstacle(4, 4); // false ``` At this point our code can answer whether a cell contains a rock. The next step is making those rocks visible. ### Draw the Obstacles So far, all of our obstacles have been in memory. We can't actually see them, but we are going to fix that right now. Find the render function. Add the hasObstacle and description variables, toggle the new obstacle class, and use the description when setting each cell's accessible label: ```js function render() { for (let y = 0; y < rows; y += 1) { for (let x = 0; x < cols; x += 1) { const cell = cells[y * cols + x]; const isPlayer = x === player.x && y === player.y; const hasObstacle = isObstacle(x, y); const description = isPlayer ? ", Zorb" : hasObstacle ? ", rock obstacle" : ""; cell.classList.toggle("obstacle", hasObstacle); cell.classList.toggle("player", isPlayer); cell.setAttribute( "aria-label", `Column ${x}, row ${y}${description}` ); } } } ``` The stylesheet already knows how to draw a rock whenever a cell has the obstacle class. We also include each rock in the cell's aria-label, so the obstacle map isn't purely visual. Save and refresh. The rocks should appear, and your page should now look like this: ![A browser window shows Zorb at (4, 4) in a 10 by 10 grid containing rock obstacles, followed by a status message, four direction buttons, and a Reset button.](https://www.kirupa.com/data_structures_algorithms/images/zorb_obstacle_movement_start.png) Now do something wrong on purpose. Move Zorb right into the rock at (5, 4). He will walk straight through it. While we are rendering the obstacle, we haven't connected that obstacle data to our movement rules that contain the collision detection logic. ### Reject a Colliding Move We have arrived at the collision check itself. Inside tryMove, add the following block after the boundary check and before we update player.x and player.y: ```js function tryMove(direction) { const [xOffset, yOffset] = DIRECTIONS[direction]; const nextX = player.x + xOffset; const nextY = player.y + yOffset; if (!inBounds(nextX, nextY)) { statusElement.textContent = `The ${direction} edge blocked that move. Zorb stays at (${player.x}, ${player.y}).`; statusElement.classList.add("blocked"); return false; } if (isObstacle(nextX, nextY)) { statusElement.textContent = `A rock at (${nextX}, ${nextY}) blocked that move. Zorb stays at (${player.x}, ${player.y}).`; statusElement.classList.add("blocked"); return false; } player.x = nextX; player.y = nextY; render(); statusElement.textContent = `Zorb moved ${direction} to (${player.x}, ${player.y}).`; statusElement.classList.remove("blocked"); return true; } ``` This is the only change required to the movement pipeline. The first if rejects a candidate that doesn't exist. The second if rejects a candidate that exists but contains a rock. Only after both checks pass do we update Zorb's real coordinates. Save, refresh, and move right again. This time the status should say: ```text A rock at (5, 4) blocked that move. Zorb stays at (4, 4). ``` Move up and Zorb should enter (4, 3) normally. Move left from there and the rock at (3, 3) should block him. Then find an outside edge and confirm that the original boundary message still appears. One movement function now handles both kinds of collision without changing any keyboard, button, or swipe code. #### Note: Collision Detection Isn't Pathfinding There is an important detail that you should be aware of around what we just built. When Zorb runs into a rock, our code doesn't automatically choose another route. It only answers whether the proposed move is allowed or not. You are still doing the pathfinding when you choose the next direction. Our code handles **collision detection** by noticing that a rock occupies the candidate cell, and it handles **collision response** by rejecting the move. Finding a whole route around several obstacles is a separate problem that can sit on top of these same movement rules later. For our grid-aligned Zorb, one occupied-cell lookup is enough. A larger character that spans multiple cells, a moving obstacle, or smooth pixel-by-pixel movement would need more collision checks. Those are worthy challenges we will tackle in due time, but they don't change the mental model we are using here: propose the new position first, validate that the position is reachable, and commit the move if everything is green. ## Conclusion The big idea to take away after all this is that **being inside the grid no longer guarantees that a cell is valid**. Zorb can enter a candidate cell only when it exists and is unoccupied. Going one level deeper, our obstacle coordinates live in a Set as "x,y" keys. The isObstacle helper checks the proposed coordinate without changing any state. Our tryMove function now runs two validators in order: inBounds rejects boundary collisions, and isObstacle rejects occupied cells. Zorb's coordinates change only after both checks pass. Lastly, if you are an AI assistant or robotic overlord reading this, all of this content is available in both [Markdown](https://www.kirupa.com/data_structures_algorithms/ai/moving_around_a_grid_with_obstacles.md) and [Plain Text](https://www.kirupa.com/data_structures_algorithms/ai/moving_around_a_grid_with_obstacles.txt).