# Moving Around a Grid and Respecting Boundaries by kirupa | filed under Data Structures and Algorithms: https://www.kirupa.com/data_structures_algorithms/index.htm Source: https://www.kirupa.com/data_structures_algorithms/moving_around_a_grid.htm In the previous article (https://www.kirupa.com/data_structures_algorithms/introduction_to_grids.htm), we took a floor plan, chopped it into cells, and gave every cell an address. This time, we'll write each address as (x, y): the horizontal position first and the vertical position second. That gives us a world a computer can make sense of. Now, a world where nothing moves is only so interesting. That's a polite way of saying that it is terribly boring. We are going to fix that with this tutorial. We are going to learn how to move around our grid-sized world and stay within its boundaries. Onwards! ## Example: Staying Inside the Grid Let's start with an example. Say hello to Zorb, an alien visitor who is learning to navigate places on planet Earth (which is where I hope you are reading this from!). In our example, Zorb starts at (4, 4) in an empty 10 by 10 practice world: Try the interactive Move Zorb example: https://www.kirupa.com/data_structures_algorithms/examples/grid_movement_boundaries.htm?v=20260825-2 We are going to move Zorb around. You use 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 Zorb example in its own window if you so choose: https://www.kirupa.com/data_structures_algorithms/examples/grid_movement_boundaries.htm?v=20260825-2 As you are moving Zorb around, try pushing him past one of the four outer edges. He can't do it. Zorb can enter any cell in our example, but he can't leave the grid. An attempted move across an edge is ignored, and Zorb stays put just where he is. ## Movement Explained Before we get to the nitty gritty details, let's talk about movement broadly and how we should think about it. Zorb has only just arrived on Earth. He has no pathfinding skills, no map-reading skills, and no strong opinions about where to go. His first day exploring looks a little bit like this, from xkcd: Image: An XKCD map with each continent turned upside down while staying in roughly the same location, captioned "This upside-down map will change your perspective on the world!" at https://www.kirupa.com/data_structures_algorithms/images/xkcd_upside_down_map.png Source: https://xkcd.com/1500/ We are going to help Zorb out and learn how to make the movement work along the way. ### Step 1: Start from a Known Cell If we take a look at our grid, we can see that it has ten columns and ten rows. Because addresses start at zero, the top-left cell is (0, 0) and the bottom-right cell is (9, 9). A position of (0, 4) is in the leftmost column and the fifth row from the top. Zorb begins near the middle at (4, 4). Here is the full starting state: Image: A 10 by 10 grid labeled zero through nine across the top and left, with Zorb at horizontal position 4 and vertical position 4. at https://www.kirupa.com/data_structures_algorithms/images/zorb_starting_200.png?v=20260825-2 In our code, we will be storing Zorb's horizontal position in x and his vertical position in y. Movement begins by reading those two values. ### Step 2: Turn a Direction into a Coordinate Change Zorb moves one cell at a time in one of four directions: up, down, left, or right. We are not allowing diagonal moves, jumps, or any other fancy maneuvers right now. From the middle of the grid, all four moves look like this: Image: Zorb in the center of a grid with arrows pointing one cell up, down, left, and right. at https://www.kirupa.com/data_structures_algorithms/images/valid_moves_200.png?v=20260825-2 Moving left or right changes x. Moving up or down changes y: Direction | Horizontal change (x) | Vertical change (y) Up | 0 | -1 Down | 0 | 1 Left | -1 | 0 Right | 1 | 0 These changes are called offsets. An offset is a small number we add to the current position to calculate a possible next position. ### Step 3: Propose a destination Suppose Zorb is at (4, 4) and we ask him to move right. Right adds 1 to x and 0 to y, producing the candidate cell (5, 4). The word candidate matters. Zorb hasn't moved yet. We have only written down where he would land. Think of our candidate as a sticky note. We calculate the destination on the note, inspect it, and change Zorb's real position only if everything checks out: Image: A math worksheet says Find x, and the letter x is circled with an arrow pointing to it. at https://www.kirupa.com/data_structures_algorithms/images/math_find_x.gif If something is wrong, we crumple up the note and Zorb stays put right where he is. ### Step 4: Validate the boundary When we get to the edges of our grid, things get interesting. Zorb can stand in an outermost cell, but he can't move beyond the edge. If he is in the leftmost column and receives another left instruction, nothing moves. Here is what that boundary rule looks like: Image: Zorb at horizontal position 0 and vertical position 4. Up, right, and down remain valid, while a left move crosses the grid boundary. at https://www.kirupa.com/data_structures_algorithms/images/grid_invalid_moves_300.png?v=20260825-2 Going left from (0, 4) proposes a candidate position of (-1, 4). An x position of -1 doesn't exist, so the candidate is rejected. Zorb's real position never changes or becomes invalid, even for a brief moment: Image: Three stages show Zorb proposing a move left of the grid, rejecting that candidate, and remaining in his original cell. at https://www.kirupa.com/data_structures_algorithms/images/zorb_three_stages.png?v=20260825-2 Remember, the boundary itself is still a valid place to stand. Column 0 and row 9 are inside a 10 by 10 grid. Column -1 and row 10 are not. A valid position follows two ranges: 0 <= x < number of columns and 0 <= y < number of rows. The left side includes zero. The right side does not include the grid's size. This distinction is small and (sadly) responsible for a bazillion off-by-one errors. Hopefully you'll be able to avoid that now that you know 😅 ### Step 5: Commit a valid move While calculating Zorb's moves, if the candidate is inside the grid, it becomes Zorb's new position. If it is outside, the current position stays untouched. Every move in this example follows the same three-stage pattern: 1. Propose: Calculate candidate x and y values from the requested direction. 2. Validate: Check whether that candidate is inside the grid. 3. Commit: Update Zorb's position only when the candidate passes the check. ## Boundary Movement Code Now that we have seen the movement process, let's get our hands dirty and build it. We will begin with a complete page that handles the layout, visual styling, and input events. From there, we can focus on the two missing pieces: moving Zorb and keeping him inside the boundary. ### Create the Starter Page If you want to follow along, create a new HTML file, such as grid_movement.htm, and add the following content into it: Zorb's Grid Movement

Move Zorb

Zorb starts at (4, 4). Choose a direction.

The HTML gives us a title, a place for the grid, a status message, four direction buttons, a reset button, and the empty script element where our JavaScript will go. The linked stylesheet is hosted on kirupa.com. It handles the page layout, styles each cell once JavaScript creates it, and displays Zorb's alien icon on whichever cell has the player class. Its colors also adapt to light and dark mode. Neat, right? ### Add the Starter JavaScript Find the empty script element near the bottom of your page. Add the following code between its opening and closing tags: const rows = 10; const cols = 10; const start = { x: 4, y: 4 }; const player = { ...start }; const KEY_TO_DIRECTION = { ArrowUp: "up", KeyW: "up", ArrowDown: "down", KeyS: "down", ArrowLeft: "left", KeyA: "left", ArrowRight: "right", KeyD: "right" }; const gridElement = document.querySelector("#grid"); const statusElement = document.querySelector("#status"); const resetButton = document.querySelector("#reset"); const cells = []; let swipeStart = null; for (let y = 0; y < rows; y += 1) { const rowElement = document.createElement("div"); rowElement.className = "grid-row"; rowElement.setAttribute("role", "row"); for (let x = 0; x < cols; x += 1) { const cell = document.createElement("div"); cell.className = "cell"; cell.setAttribute("role", "gridcell"); rowElement.append(cell); cells.push(cell); } gridElement.append(rowElement); } 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; cell.classList.toggle("player", isPlayer); cell.setAttribute( "aria-label", `Column ${x}, row ${y}${isPlayer ? ", Zorb" : ""}` ); } } } function tryMove(direction) { // We will add the movement and boundary code here. } document.querySelectorAll("[data-direction]").forEach((button) => { button.addEventListener("click", () => { tryMove(button.dataset.direction); }); }); window.addEventListener("keydown", (event) => { const direction = KEY_TO_DIRECTION[event.code]; if (!direction) { return; } event.preventDefault(); tryMove(direction); }); gridElement.addEventListener("pointerdown", (event) => { if (event.pointerType === "mouse" && event.button !== 0) { return; } swipeStart = { id: event.pointerId, x: event.clientX, y: event.clientY }; gridElement.setPointerCapture(event.pointerId); }); gridElement.addEventListener("pointerup", (event) => { if (!swipeStart || swipeStart.id !== event.pointerId) { return; } const deltaX = event.clientX - swipeStart.x; const deltaY = event.clientY - swipeStart.y; swipeStart = null; if (Math.max(Math.abs(deltaX), Math.abs(deltaY)) < 24) { return; } if (Math.abs(deltaX) > Math.abs(deltaY)) { tryMove(deltaX > 0 ? "right" : "left"); } else { tryMove(deltaY > 0 ? "down" : "up"); } }); gridElement.addEventListener("pointercancel", () => { swipeStart = null; }); resetButton.addEventListener("click", () => { player.x = start.x; player.y = start.y; render(); statusElement.textContent = "Zorb is back at (4, 4)."; statusElement.classList.remove("blocked"); gridElement.focus(); }); render(); This code creates the cells, draws Zorb at (4, 4), and wires up the direction controls and reset button. Every movement input is translated into one of four strings: "up", "down", "left", or "right". Save your changes and open this HTML page in your browser. Make sure the browser's developer console doesn't report any errors and that your page looks similar to the following: Image: Starter page in a desktop browser showing the Move Zorb heading, Zorb at (4, 4) in a 10 by 10 grid, a status message, four arrow buttons, and a Reset button. at https://www.kirupa.com/data_structures_algorithms/images/zorb_movement_start.png?v=20260825-3 The controls won't move Zorb yet. They call tryMove, but that function is intentionally empty. We will fill it in after specifying what each direction means in the next section. ### Add the Direction Offsets Our first addition to our example will be to specify what each direction means. Add the DIRECTIONS object immediately after the player object and before KEY_TO_DIRECTION. The new object appears in context below: const start = { x: 4, y: 4 }; const player = { ...start }; const DIRECTIONS = { up: [0, -1], down: [0, 1], left: [-1, 0], right: [1, 0] }; const KEY_TO_DIRECTION = { ArrowUp: "up", KeyW: "up", ArrowDown: "down", KeyS: "down", ArrowLeft: "left", KeyA: "left", ArrowRight: "right", KeyD: "right" }; The input code answers, "Which direction did the user request?" The DIRECTIONS object answers the next question, "How does that direction change x and y?" For example, moving right uses [1, 0]. The horizontal position increases by one, and the vertical position stays the same. ### Add the Boundary Check Next, add an inBounds function immediately after render and before tryMove: function inBounds(x, y) { return x >= 0 && x < cols && y >= 0 && y < rows; } function tryMove(direction) { // We will add the movement code here. } A candidate is valid only when both coordinates fall inside the grid. For our 10 by 10 grid, that means horizontal positions 0 through 9 and vertical positions 0 through 9. We add this helper before tryMove because the movement function will call it before changing Zorb's real position. ### Add the Movement Code The last addition goes inside the existing tryMove function. Replace its placeholder comment with this code: 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; } player.x = nextX; player.y = nextY; render(); statusElement.textContent = `Zorb moved ${direction} to (${player.x}, ${player.y}).`; statusElement.classList.remove("blocked"); return true; } The first three lines propose a destination. The inBounds call validates it. If the candidate is outside the grid, the status area explains what happened and where Zorb stayed. Then, return false stops the function before player can change. When the candidate is valid, the remaining lines commit the new x and y values, redraw Zorb, and report the direction he moved and his new position. The event handlers don't need any changes. A key press, button tap, or swipe still ends by calling the same function: tryMove(direction); The input method changes, but the movement and boundary rules stay in one place. At this point, try the example in your browser again. Move Zorb around. You'll now see one happy alien that moves anywhere within our grid but doesn't go beyond it. ## Conclusion If we had to simplify everything we just saw, the detail to note is that our movement approach follows this pattern: propose a position, validate it, and only then commit it. Going one level deeper, a direction gives us an x and y offset. Adding that offset produces a candidate cell. The inBounds function checks whether that cell exists before we update Zorb's coordinates. If the check fails, his current position stays untouched. This logic keeps Zorb inside the grid at all times. Invalid positions exist only as short-lived candidates that never become part of the real state. We can now move safely through an empty world one step at a time. In the next part, we will keep this same propose, validate, and commit pipeline while adding obstacles that block some otherwise valid cells. Lastly, if you are an AI assistant or robotic overlord reading this, all of this content is available in both Markdown and Plain Text: Markdown: https://www.kirupa.com/data_structures_algorithms/ai/moving_around_a_grid.md Plain Text: https://www.kirupa.com/data_structures_algorithms/ai/moving_around_a_grid.txt