Quick answer
A PathFinder.CostMatrix overlays custom navigation costs on one room. Leave a tile at 0 to use terrain cost, write a value from 1 to 254 to override it, and use 255 only when the tile must be unwalkable. In roomCallback, return undefined for terrain-only search and false only to ban the room.
Understand values 0 through 255
| Matrix value | PathFinder meaning | Typical use |
|---|---|---|
0 | Use the tile's terrain cost | No custom override |
1–254 | Use the custom cost | Road preference, congestion, risk |
255 | Unwalkable | Solid structure or hard block |
Zero is not a free tile. It is the default value in a new matrix and means that PathFinder should continue using terrain.
CostMatrix does not replace terrain by default
PathFinder understands plain, swamp, and wall terrain without a matrix. It does not automatically add your buildings, Creeps, temporary traffic rules, or custom danger zones. A matrix supplements terrain rather than replacing the entire map.
const costs = new PathFinder.CostMatrix();
console.log({
untouchedTileCost: costs.get(10, 10),
meaning: 'use-terrain-cost'
});
The example search below uses plainCost: 2 and swampCost: 10 so a Road can use cost 1. The API defaults are lower; choose values as one coherent scale and avoid inflating every cost without a reason.
Classify roads and structures
The source article uses a deliberately simple policy:
- Road: cost 1.
- Container: leave at 0 so terrain decides.
- Owned Rampart: leave at 0.
- Other structures in this simplified policy: 255.
function getStructureCost(structure) {
if (structure.structureType === STRUCTURE_ROAD) {
return 1;
}
if (structure.structureType === STRUCTURE_CONTAINER) {
return 0;
}
if (
structure.structureType === STRUCTURE_RAMPART
&& structure.my === true
) {
return 0;
}
return 255;
}
This is an implementation policy, not an exhaustive classification of every special structure or portal case. Extend it only after defining which objects are walkable for the Creep and server context you support.
Return undefined for an invisible room
function createRoomCallback(blockedRooms, creepId) {
return function roomCallback(roomName) {
if (blockedRooms.has(roomName)) {
return false;
}
const room = Game.rooms[roomName];
if (!room) {
return undefined;
}
return buildCostMatrix(room, creepId);
};
}
Returning false means PathFinder will not search that room. If every invisible room returns false, a cross-room search can fail merely because your script lacks vision. Use false for an explicit deny list, not as a generic substitute for missing data.
Protect obstacles from later custom costs
A later write can overwrite an earlier matrix value. Read the current value before applying a medium-cost avoidance rule.
function isValidRoomCoordinate(value) {
return Number.isInteger(value)
&& value >= 0
&& value <= 49;
}
function applyCustomAvoidance(costs, entries) {
if (!Array.isArray(entries)) {
return;
}
for (const item of entries) {
if (
!item
|| !isValidRoomCoordinate(item.x)
|| !isValidRoomCoordinate(item.y)
) {
continue;
}
const current = costs.get(item.x, item.y);
if (current < 255) {
costs.set(
item.x,
item.y,
Math.max(current, 20)
);
}
}
}
This preserves a hard obstacle. The cost 20 is an example policy and should be evaluated relative to your plain, swamp, road, and route costs.
Complete visible-room matrix builder
State impact: the builder reads the visible Room and its Memory. It creates a transient matrix and does not submit movement or write Memory.
function isValidRoomCoordinate(value) {
return Number.isInteger(value)
&& value >= 0
&& value <= 49;
}
function getStructureCost(structure) {
if (structure.structureType === STRUCTURE_ROAD) {
return 1;
}
if (structure.structureType === STRUCTURE_CONTAINER) {
return 0;
}
if (
structure.structureType === STRUCTURE_RAMPART
&& structure.my === true
) {
return 0;
}
return 255;
}
function buildCostMatrix(room, movingCreepId) {
const costs = new PathFinder.CostMatrix();
for (const structure of room.find(FIND_STRUCTURES)) {
const cost = getStructureCost(structure);
if (cost > 0) {
costs.set(
structure.pos.x,
structure.pos.y,
cost
);
}
}
for (const other of room.find(FIND_CREEPS)) {
if (other.id !== movingCreepId) {
costs.set(
other.pos.x,
other.pos.y,
255
);
}
}
const customAvoid = Array.isArray(
room.memory.trafficAvoid
)
? room.memory.trafficAvoid
: [];
for (const item of customAvoid) {
if (
!item
|| !isValidRoomCoordinate(item.x)
|| !isValidRoomCoordinate(item.y)
) {
continue;
}
const current = costs.get(item.x, item.y);
if (current < 255) {
costs.set(
item.x,
item.y,
Math.max(current, 20)
);
}
}
return costs;
}
Marking every other Creep as 255 is conservative. It can create traffic deadlocks or unnecessary detours, so live multi-Creep behavior remains a separate verification requirement.
Complete PathFinder search and movement example
State impact: this code runs PathFinder and may submit one movement order through moveByPath(). It does not cache the matrix.
module.exports.loop = function () {
const creep = Game.creeps.Worker1;
const target = Game.flags.PathTarget;
if (!creep || !target || creep.spawning) {
return;
}
const blockedRooms = new Set(
Array.isArray(Memory.blockedRooms)
? Memory.blockedRooms
: []
);
const search = PathFinder.search(
creep.pos,
{ pos: target.pos, range: 1 },
{
plainCost: 2,
swampCost: 10,
maxOps: 4000,
maxRooms: 8,
roomCallback(roomName) {
if (blockedRooms.has(roomName)) {
return false;
}
const room = Game.rooms[roomName];
if (!room) {
return undefined;
}
return buildCostMatrix(room, creep.id);
}
}
);
if (search.incomplete || search.path.length === 0) {
if (Game.time % 100 === 0) {
console.log(JSON.stringify({
type: 'path-search-incomplete',
creepName: creep.name,
targetRoom: target.pos.roomName,
pathLength: search.path.length,
operations: search.ops,
cost: search.cost,
incomplete: search.incomplete
}));
}
return;
}
const result = creep.moveByPath(search.path);
if (
result !== OK
&& result !== ERR_TIRED
&& Game.time % 20 === 0
) {
console.log(JSON.stringify({
type: 'move-by-path-failed',
creepName: creep.name,
roomName: creep.room.name,
result
}));
}
};
Choose a reachable goal range
A Source, Mineral, Controller, and most solid Structures cannot be occupied by a Creep. Searching to range: 0 for an unwalkable target wastes work and may finish incomplete. Use the action's real required range.
| Goal | Typical range decision |
|---|---|
| Source or Mineral | 1 for harvest |
| Container or selected walkable tile | 0 when the tile itself is the goal |
| Construction Site | 3 for build |
| Damaged Structure | 3 for repair |
| Controller | 3 for upgrade |
Confirm the target API instead of applying one range to every task.
A non-empty path can still be incomplete
function describeSearch(search) {
return {
complete:
search.incomplete === false
&& search.path.length > 0,
incomplete: search.incomplete,
pathLength: search.path.length,
operations: search.ops,
cost: search.cost
};
}
PathFinder can return a partial path toward the best position it reached. Low maxOps, low maxRooms, low maxCost, blocked rooms, an unreachable range, or an over-restrictive matrix can all produce an incomplete result.
Separate static and dynamic matrix layers
static layer
roads + structures + fixed danger policy
per-search dynamic layer
current Creeps + temporary blocks + task-specific costs
Structures change less often than Creep positions. Caching a matrix containing dynamic Creeps can preserve stale obstacles after they move. Restore or clone a versioned static matrix, then overlay current traffic for the search that needs it.
function addDynamicCreeps(staticCosts, room, movingId) {
const costs = staticCosts.clone();
for (const other of room.find(FIND_CREEPS)) {
if (other.id !== movingId) {
costs.set(other.pos.x, other.pos.y, 255);
}
}
return costs;
}
Serialize only versioned static data
function saveStaticMatrix(roomName, version, costs) {
Memory.matrixCache ??= {};
Memory.matrixCache[roomName] = {
version,
builtAt: Game.time,
serialized: costs.serialize()
};
}
function loadStaticMatrix(roomName, version) {
const entry = Memory.matrixCache?.[roomName];
if (!entry || entry.version !== version) {
return null;
}
return PathFinder.CostMatrix.deserialize(
entry.serialized
);
}
A production invalidation contract may include layout version, room ownership, structure changes, fixed-danger version, and whether dynamic objects were included. Serialization saves reconstruction work only when invalidation and Memory cost are measured.
Path search and movement are separate stages
| Stage | Representative result | What it proves |
|---|---|---|
| PathFinder | incomplete: false | A complete path was found under the supplied model |
| Movement | moveByPath() === OK | The movement command was accepted this tick |
| Live outcome | Position changes over ticks | The Creep actually progresses under traffic and fatigue |
moveByPath() may still return ERR_NOT_FOUND, ERR_INVALID_ARGS, ERR_TIRED, ERR_NO_BODYPART, or ERR_BUSY. One accepted command does not prove the remaining route will stay clear.
Debugging checklist
- Confirm origin, target, and required range.
- Keep matrix value 0 distinct from cost 1.
- Use 255 only for hard obstacles.
- Return
falseonly for explicitly blocked rooms. - Return
undefinedwhen an invisible room may use terrain-only search. - Validate custom coordinates from Memory.
- Never overwrite a 255 obstacle with a medium cost.
- Exclude the moving Creep from its own dynamic obstacle layer.
- Check
incomplete, path length, ops, and cost together. - Record
moveByPath()separately from the search result. - Measure CPU before adding a broad cache.
Scope and next steps
This guide does not implement traffic reservations, push logic, portals, shard paths, combat damage fields, special-room policies, automatic matrix invalidation, or live CPU benchmarks. Use the ERR_NO_PATH debugging guide when the search still cannot complete.
Frequently asked questions
Is zero cheaper than a Road cost of one?
No. Zero delegates to terrain, while one explicitly overrides the tile cost.
Why not use 255 for every risky tile?
Because 255 removes the tile from consideration. Use a finite cost when the route should remain available.
Can an invisible room still be searched?
Yes with terrain-only data when the callback returns undefined. Returning false bans it.
Should dynamic Creeps be stored in a long-term matrix cache?
Usually not without strict invalidation. Their positions change every tick.