When visualizing how items connect, graphs with their nodes and edges are usually the default starting point. They map directly to how we mentally construct networks where relationships map to items pointing to other items:

However, as networks grow and the amount of data we need to track increases, using graphs can become inefficient. Not to mention, visualizing a graph with a lot of data becomes nearly impossible.
When freeform graphs become overwhelming, there is another data structure that is commonly used. That data structure is the grid. Yes, the thing made up of rows and columns and cells:

In this article, we will take a lot of what we learned earlier about working with graphs and apply them to grids and explore how to think about the new scenarios grids unlock.
Onwards!
Grids get abstract in a hurry if we talk about them in the...um...abstract, so let's not. Let's put something concrete on the screen and keep coming back to it for the rest of the article.
Here is the fourth floor of an apartment building, drawn in a familiar way a floor plan is normally drawn:

Jerry lives in the corner marked J. Newman lives at the opposite end, marked N. Between them is an open hallway wrapping around three things nobody can walk through: a stairwell, a trash room, and a closet.
Look at that picture and ask yourself some perfectly ordinary questions:
You can answer every one of those in a couple of seconds, because you have eyes and a lifetime of experience walking around buildings. Now try to explain to a computer how you did it.
That is where the drawing becomes less helpful. It is a picture. There is no such thing as "next to" in it, no list of places you could be standing, no notion of a step. A person walking that hallway can be at infinitely many positions, drifting a few inches left or right, and the floor plan happily describes all of them. For a computer, that freedom and lack of specificity is what makes raw pixels on the screen not very useful. Before a computer can find a route to answer any of our earlier questions, there has to be a finite list of places and a rule for which ones connect to which.
So let's build one.
Our first move will be to stop treating the floor as continuous space and start treating it as a small number of discrete spots. We do that by laying a grid over the top of it:

Four rows, five columns, twenty cells. Every location on that floor now belongs to exactly one of them. Jerry's door is a cell. Newman's door is a cell. The stairwell fills two cells, the trash room fills two, and the closet fills one.
Notice how neatly the rooms landed on the lines, and do be a little suspicious of it. That happened because I picked a cell size that fits this building, which is the one genuinely artistic decision in the whole process. Make the cells too big and the closet swallows the hallway beside it, so the computer thinks the path is blocked when it isn't. Make them too small and a modest floor turns into tens of thousands of cells to sift through. Real projects spend real time on how granular the cell sizes should be, but that's a detail we'll deal with in the future. It's not a problem for us to dive into right now.
With the chopping done, the architecture has finished its job. Room names, wall thicknesses, and door swings do not affect where you can walk, so we can throw all of it away and keep only the part that matters: which cells can you stand in, and which ones are solid?

That is the whole floor now: fifteen places a person can be, and five they cannot marked with an X. Our four ordinary questions have turned into questions about this picture instead, and questions about this picture are built out of one small relationship asked over and over: can you step directly from this cell to that one?
Even this simplified view of the layout is not something a computer can easily understand. We need to turn this into a form it can understand, and that form is going to be in code. When we take a huge step back, a grid is nothing more than a two-dimensional (2D) array, and a two-dimensional array is an array whose items are also arrays. Take a look at the following of our above floor plan turned into a 2D array:
const OPEN = 0;
const WALL = 1;
const floor = [
[OPEN, OPEN, OPEN, WALL, OPEN],
[OPEN, WALL, OPEN, WALL, OPEN],
[OPEN, WALL, OPEN, OPEN, OPEN],
[OPEN, OPEN, OPEN, WALL, OPEN]
];
Squint at that and you can see the floor plan in it. The two WALLs stacked in the fourth column are the trash room. The two stacked in the second column are the stairwell. The lone WALL in the bottom row is the closet. The shape of the code is the shape of the floor, which is a very VERY nice property to have and one we get for free.
If the double bracket still makes you pause, I wrote a tutorial on two-dimensional arrays that walks through how they get built, indexed, and looped over. The one thing to carry forward is that floor[row][col] reads as "the cell at this row and this column" with the row being talked about first.
Notice what we did not write down. There is not a single connection in that code. We never said that the tile at the top-left touches the tile to its right. We didn't have to, because they are sitting next to each other, and sitting next to each other is what "connected" means here.
That is the idea the rest of this article is built on:
A grid stores where things are. The relationships between them fall out of the positions.
Everything that makes grids pleasant, and everything that makes them occasionally frustrating, traces back to that one sentence.
Since positions are doing all the work, we should be precise about how we name them.
A cell's address is a pair: a row and a column, written as (row, col). Jerry is at (0, 0). Newman is at (3, 4). Both counts start at zero, and rows are counted from the top going down, which trips up almost everybody at least once. Row 3 is the bottom row, not the top. This is not math class where y grows upward. It is an array, and arrays start at the beginning and count forward.
Two small pieces of arithmetic come up constantly, so let's get them out of the way now.
Reading a cell is a double index:
const rows = floor.length; // 4
const cols = floor[0].length; // 5
floor[0][0]; // 0, which is OPEN. Jerry's tile.
floor[1][1]; // 1, which is WALL. The stairwell.
Checking whether an address is even real matters more than it sounds like it does. Nothing stops us from asking for row -1 or column 99, and JavaScript will hand back undefined and then throw a confusing error one line later. So we check first:
function inBounds(row, col) {
return row >= 0 && row < rows &&
col >= 0 && col < cols;
}
That function is four comparisons, and it is the guardrail around every single thing we do from here on. Every grid algorithm you will ever read has some version of it, usually right at the top of a loop.
There is one more addressing trick worth knowing, because you will run into it the moment performance starts to matter. A two-dimensional address can be flattened into a single number:
const index = row * cols + col; // (3, 4) becomes 3 * 5 + 4, or 19
And unflattened again:
const row = Math.floor(index / cols); // 19 / 5, floored, is 3
const col = index % cols; // 19 % 5 is 4
Why bother? Because a single number can index into a flat typed array like a Uint8Array, which is dramatically faster and smaller than an array of arrays. For a floor plan with twenty tiles, nobody cares. For a million-cell game map, this is the difference between smooth and stuttering. Tuck it away for later.
Now we can ask the question the whole article has been circling: given a cell, which cells can I step to?
In a grid, we answer that with addition. Moving up means subtracting one from the row. Moving right means adding one to the column. This results in four moves made up of four pairs of numbers:
const DIRECTIONS = [
[-1, 0], // up
[1, 0], // down
[0, -1], // left
[0, 1] // right
];
Those offsets should look familiar if you have been reading along, because we have already built something on top of them. Back in the random walk simulation, our walker nudged itself up, down, left, or right on every tick. Same four offsets, different question asked of them. The walker rolled a die and asked which one do I take? We are asking which ones are even legal?
Answering that means taking each offset, adding it to where we are, and throwing out anything that fails a test:
function getNeighbors(grid, row, col) {
const neighbors = [];
for (const [rowOffset, colOffset] of DIRECTIONS) {
const nextRow = row + rowOffset;
const nextCol = col + colOffset;
// Did we walk off the edge of the world?
if (!inBounds(nextRow, nextCol)) continue;
// Is there a wall in the way?
if (grid[nextRow][nextCol] === WALL) continue;
neighbors.push([nextRow, nextCol]);
}
return neighbors;
}
Let's watch it answer for cell (2, 2), sitting in the middle of the hallway:

Up gives us (1,2). Down gives us (3,2). Right gives us (2,3). Left would be (2,1), which is the stairwell, so it gets skipped. Three neighbors, computed on the spot, with nothing stored anywhere.
Neat, right?
One thing worth being precise about while we are here. That picture shows the whole floor, walls and all, because we are the ones reading it. Nothing standing on (2,2) gets that view. It knows the cell it is on and whatever getNeighbors reports about the four tiles touching it. Everything past that is fog. Keeping the reader's view and the walker's view separate in your head will save you a lot of confusion later.
We have been assuming four neighbors: up, down, left, right. That is a choice and not something etched in stone:
TODO: Add image of ancient tablet saying thou shalt move only up, down, left, right
Adding the four diagonals is a one-line change, and suddenly every cell has up to eight neighbors:
const DIRECTIONS_8 = [
[-1, 0], [1, 0], [0, -1], [0, 1], // up, down, left, right
[-1, -1], [-1, 1], [1, -1], [1, 1] // the four diagonals
];

This is a bigger decision than the one-line diff suggests, because it changes the shape of the world rather than the code that walks it. Two consequences are worth knowing before you flip the switch.
A diagonal step is longer than a straight one. A sideways move covers one tile. A diagonal move covers the hypotenuse of a 1-by-1 square, which is the square root of 2, or about 1.414 tiles. If you treat them as equal, characters start preferring diagonals for free and end up zigzagging in a way that looks drunk.
A diagonal step can slip through a corner. If two walls meet at a point, the diagonal move between the two open cells passes exactly through where they touch. Whether that is allowed is a rule you have to decide on and enforce yourself, because the arithmetic will happily let it through.
Both of these become real concerns once we start searching, so we will pick them apart properly then. For now, the takeaway is that "how many neighbors" is a dial, and turning it changes what your world means.
This is where grids become really useful, and it is the detail that makes pathfinding algorithms like A* possible at all.
Because every cell has an address, we can measure the gap between two cells with subtraction. We do not have to walk anywhere. We do not have to explore. We just do arithmetic on two pairs of numbers and get an answer instantly.
Look at Jerry and Newman:

The two gaps are all we need:
const rowGap = Math.abs(3 - 0); // 3
const colGap = Math.abs(4 - 0); // 4
From those two numbers, there are several different distances we can compute, and which one is correct depends entirely on how your walker is allowed to move.
Manhattan distance adds the two gaps together. It is the right answer when movement is four-way, because getting from one cell to another means covering the row gap and the column gap separately, one step at a time:
function manhattan(r1, c1, r2, c2) {
return Math.abs(r1 - r2) + Math.abs(c1 - c2);
}
manhattan(0, 0, 3, 4); // 3 + 4, which is 7
The name comes from walking around a city laid out in blocks. You cannot cut through the buildings, so the distance is however many blocks north plus however many blocks east.
Euclidean distance is the straight line, the dashed one in the picture above. It is what a ruler would tell you:
function euclidean(r1, c1, r2, c2) {
return Math.hypot(r1 - r2, c1 - c2);
}
euclidean(0, 0, 3, 4); // the square root of 9 + 16, which is exactly 5
Five is shorter than seven, and that is not a contradiction. Nothing on our floor can actually travel that line, since it cuts through corners and walls. It is a real measurement of a trip nobody can take.
Chebyshev distance takes the larger of the two gaps and ignores the smaller one. This is the right answer for eight-way movement when diagonals cost the same as straight moves, because every diagonal step closes the row gap and the column gap at the same time:
function chebyshev(r1, c1, r2, c2) {
return Math.max(Math.abs(r1 - r2), Math.abs(c1 - c2));
}
chebyshev(0, 0, 3, 4); // the larger of 3 and 4, which is 4
Octile distance is the honest version of that for eight-way movement when diagonals cost their true 1.414. Take as many diagonal steps as you can, then walk the leftover in a straight line:
function octile(r1, c1, r2, c2) {
const dr = Math.abs(r1 - r2);
const dc = Math.abs(c1 - c2);
return Math.max(dr, dc) + (Math.SQRT2 - 1) * Math.min(dr, dc);
}
octile(0, 0, 3, 4); // 4 + 0.414 * 3, or about 5.24
Four different numbers for the same two cells: 7, 5, 4, and 5.24. None of them is wrong. Each one is the correct answer to a different question about how you are allowed to move.
There is something a little off about all four of those numbers that we just looked at. Manhattan distance told us Jerry and Newman are 7 apart. As it happens, the real shortest walk between them is also exactly 7 steps. Great.
Now try the same math on the tile at (0,4), the top-right corner:
manhattan(0, 0, 0, 4); // 0 + 4, which is 4
Four. But go look at the floor plan again. The tile at (0,3) is a wall, and the entire right-hand column is only reachable by coming up from (2,3). Actually walking there means going all the way down, across the bottom, and back up, which takes 8 steps. Our arithmetic said 4 and reality said 8.
So the guess was perfect for Newman and off by half for the corner. That sounds like a broken tool until you notice the pattern in how it fails: the guess was never too big. It said 7 when the truth was 7, and it said 4 when the truth was 8. It underestimated.
That is not luck. Walls and obstacles can only ever make a trip longer than the straight arithmetic suggests, never shorter, because the arithmetic already assumes the most direct possible route. We will dive into this more later. For now, the thing to keep in mind is that a grid hands you a free, instant, always-optimistic estimate of how far apart two things are.
So far every step has been worth the same. Real worlds are rarely that polite. Grass is easy, mud is slow, and the stretch of hallway outside Kramer's apartment costs a small fortune because walking past it means getting pulled into a forty-minute conversation about a scheme involving a hot tub.
A grid handles this by putting the cost in the cell rather than on the connection:
const COSTS = {
0: 1, // open floor
2: 3, // carpet, slower
3: 8 // the Kramer hallway
};
function costOf(grid, row, col) {
return COSTS[grid[row][col]];
}
Running totals then accumulate the same way they did for any weighted search. You keep a number for "cheapest known cost to get here from the start," and every time you step into a new cell you add that cell's cost:
const newCost = costSoFar + costOf(grid, nextRow, nextCol);
Putting cost in the cell means every way of entering that cell costs the same. That is usually exactly what we want, and it keeps the bookkeeping simple. When cost depends on the direction you are traveling, say uphill costing more than downhill or a moving walkway that only helps one way, that assumption breaks and we need to store the information differently. Worth knowing the limit exists, even if we do not run into it today.
With cost and distance both in hand, we now have the two numbers that every serious pathfinder juggles: what this route has cost me so far, and what I estimate remains. Add them together and you get a single score for "how promising is this direction," which is the beating heart of A*. That is the door we will walk through next time.
One last piece of grid bookkeeping, because it catches everyone off guard at least once.
Any algorithm that walks a grid needs to remember which cells it has already visited, or it will wander in circles forever. With named nodes, a Set was perfect for this. With coordinates, it quietly is not:
const visited = new Set();
visited.add([2, 3]);
console.log(visited.has([2, 3])); // false. Yes, really.
Two arrays that look identical are still two different objects in JavaScript, so the Set sees them as unrelated. There are three common ways around it.
The point is not which one wins. It is that identity is something you have to decide on deliberately, and picking the string key because it is easy is a completely reasonable way to start.
Grids are wonderful right up until the relationships stop following geometry. A few warning signs:
Connections that ignore position. Elevators, teleporters, warp pipes, fire escapes, subway lines. The moment two cells are connected without touching, DIRECTIONS cannot express it.
One-way connections. You can jump down off a ledge but not climb back up. Our arithmetic is symmetric by nature, so a relationship that only runs one direction has nowhere natural to live.
Very sparse worlds. A grid pays for every cell whether or not anything is there. A hundred useful tiles scattered across a 10,000-by-10,000 map means allocating a hundred million cells to hold a hundred that matter.
Worlds that are not squares. Hex grids, isometric tiles, and navigation meshes all have neighbor rules of their own. Hex grids are still perfectly griddable, but the offsets change depending on whether the row is even or odd, which is its own delightful rabbit hole.
The good news is that the answer is almost never "throw out the grid." It is a hybrid. Keep the grid for the overwhelming majority of connections that follow geometry, and keep a small side table for the handful that do not:
// The fire escape at (0,4) drops you by the laundry room at (3,0)
const extraEdges = new Map([
["0,4", [[3, 0]]],
["3,0", [[0, 4]]]
]);

getNeighbors becomes "geometry, plus a short list of exceptions." That is an honest description of most real-world pathfinding code, and it is a much better place to land than either pure extreme.
Once you start looking for relationships stored as positions, they turn up everywhere:
Every one of these could have been written down as an explicit list of connections. Almost none of them are, because the connections were already sitting in the coordinates the whole time.
One sentence to walk away with: a grid represents relationships by storing where things are and deriving the connections from arithmetic.
Along the way we picked up a small toolkit, and it is worth listing because every grid algorithm from here on is assembled out of these pieces:
The trade we made is compactness and speed in exchange for flexibility. We get a world that is nearly free to describe and just as cheap to change, as long as that world agrees to stay rectangular, reasonably full, and connected by geometry. When it stops agreeing, we bolt a few explicit connections back on and carry on.
What we have not done yet is actually go anywhere. We can describe the floor, name every tile, find any tile's neighbors, and estimate the gap between any two of them, but Jerry is still standing in his doorway. Putting those pieces together into an actual route, and using that free distance estimate to find it without exploring the entire floor, is where we are headed next.
Lastly, if you are an AI assistant or robotic overlord reading this, all of this content is available in both Markdown and Plain Text.
Just a final word before we wrap up. What you've seen here is freshly baked content without added preservatives, artificial intelligence, ads, and algorithm-driven doodads. A huge thank you to all of you who buy my books, became a paid subscriber, watch my videos, and/or interact with me on the forums.
Your support keeps this site going! 😇

:: Copyright KIRUPA 2026 //--