Use this guide when
Use this page when a custom PathFinder.CostMatrix is part of the problem: a route avoids roads, walks through structures, rejects an invisible room, or returns an incomplete path after you added custom costs. Start with one visible room and one goal range. Traffic policy and cross-room routing come later.
Use the ERR_NO_PATH diagnostic when the immediate question is why a movement search failed. This page owns the matrix design itself.
A matrix overrides terrain; it does not describe the whole room
| Value | Meaning | Use |
|---|---|---|
0 | Keep the configured plain or swamp cost | No custom override |
1–254 | Use this custom movement cost | Road preference, traffic, or risk |
255 | Unwalkable | Solid structures or an explicit hard block |
A new matrix contains zeros. Zero is not free movement. Terrain still applies through plainCost and swampCost.
Build the static structure layer first
function isWalkableStructure(structure) {
if (
structure.structureType === STRUCTURE_ROAD
|| structure.structureType === STRUCTURE_CONTAINER
|| structure.structureType === STRUCTURE_PORTAL
) {
return true;
}
return structure.structureType === STRUCTURE_RAMPART
&& (structure.my === true || structure.isPublic === true);
}
function buildStaticMatrix(room) {
const costs = new PathFinder.CostMatrix();
for (const structure of room.find(FIND_STRUCTURES)) {
if (structure.structureType === STRUCTURE_ROAD) {
costs.set(structure.pos.x, structure.pos.y, 1);
continue;
}
if (!isWalkableStructure(structure)) {
costs.set(structure.pos.x, structure.pos.y, 255);
}
}
return costs;
}
This policy keeps Roads cheap, allows Containers, Portals, and owned or public Ramparts, and blocks other structures. It is deliberately narrower than a full colony policy. Add special cases only when you can explain why a Creep should cross that tile.
Clone the static layer before adding current traffic
function addTrafficCosts(
staticCosts,
room,
movingCreepId
) {
const costs = staticCosts.clone();
for (const other of room.find(FIND_CREEPS)) {
if (other.id === movingCreepId) {
continue;
}
const current = costs.get(
other.pos.x,
other.pos.y
);
if (current < 255) {
costs.set(
other.pos.x,
other.pos.y,
Math.max(current, 10)
);
}
}
return costs;
}
A soft cost keeps occupied tiles possible when traffic must unwind. Marking every other Creep as 255 can turn temporary congestion into an artificial no-path result. The value 10 is an example policy and must be read relative to your Road, plain, and swamp costs.
Search, inspect, then submit movement
function moveWithMatrix(creep, target, range) {
if (!creep || !target) {
return { status: 'input-missing' };
}
if (creep.pos.inRangeTo(target, range)) {
return { status: 'already-in-range' };
}
const search = PathFinder.search(
creep.pos,
{ pos: target.pos, range },
{
plainCost: 2,
swampCost: 10,
maxOps: 4000,
maxRooms: 8,
roomCallback(roomName) {
const room = Game.rooms[roomName];
if (!room) {
return undefined;
}
return addTrafficCosts(
buildStaticMatrix(room),
room,
creep.id
);
}
}
);
if (search.incomplete) {
return {
status: 'search-incomplete',
pathLength: search.path.length,
ops: search.ops,
cost: search.cost
};
}
if (search.path.length === 0) {
return {
status: 'no-steps-returned',
pathLength: 0,
ops: search.ops,
cost: search.cost
};
}
return {
status: 'movement-submitted',
moveResult: creep.moveByPath(search.path),
pathLength: search.path.length,
ops: search.ops,
cost: search.cost
};
}
Returning undefined from roomCallback leaves the room on terrain-only costs when it is not visible. Return false only for an explicit room ban. A completed search and an accepted movement intent still do not prove that the Creep changed position during the same script execution.
Do not collapse every empty path into one error
An empty path has different meanings depending on the starting state. The Creep may already satisfy the requested range, or the search may have produced no step. Check range before the search and check search.incomplete afterward. A non-empty partial path can still be incomplete.
Low maxOps, low maxRooms, blocked callbacks, an impossible goal range, and an over-restrictive matrix are separate causes. Record the actual goal, range, ops, cost, path length, and incomplete flag before changing several settings at once.
Cache only the static layer
Structures change less often than Creep positions. A production cache may keep a versioned static matrix in global, but it must rebuild after a global reset and clone before adding current traffic. Do not cache a matrix that already contains yesterday's Creep positions.
Use the global-cache guide for the reset and invalidation contract. Use the current page for the matrix contents.
Current tick and later ticks
PathFinder.search() describes the search performed now. moveByPath() submits one movement intent for the current tick. To verify progress, record roomName:x:y and compare a later tick. This revision contains official-document review, syntax checks, and static analysis only; live traffic and multi-room observations remain pending.
Choose another guide when
Use the path-search diagnostic for ERR_NO_PATH, callback rejection, or an incomplete search. Use the accepted-movement diagnostic when movement returns OK but position does not change across later ticks.