Use this guide when
Use this guide when creep.moveTo() returns ERR_NO_PATH, or when a direct PathFinder.search() returns incomplete: true. Do not use it for a movement call that returned OK and then failed to make position progress; that belongs in the accepted-movement diagnostic.
The first question is not “How do I increase maxOps?” It is “Which path boundary actually failed?” A wrong target range, a room rejected by a callback, a CostMatrix blocker, a deliberately disabled path search, and a genuinely exhausted search need different fixes.
Separate three failure surfaces
| Signal | What it means | First place to look |
|---|---|---|
moveTo() === ERR_NO_PATH | The movement helper could not find a path for the current request. | Target, range, path options, obstacles, callbacks. |
moveTo({ noPathFinding: true }) === ERR_NOT_FOUND | No reusable cached path was available while new pathfinding was disabled. | Cache/reuse policy, not CostMatrix tuning first. |
PathFinder.search(...).incomplete === true | The search did not complete the requested goal; the returned path can still be partial. | Goal range, limits, callbacks, matrix, room route. |
const moveResult = creep.moveTo(target, {
range: desiredRange,
reusePath: 0
});
const search = PathFinder.search(
creep.pos,
{ pos: target.pos, range: desiredRange }
);
console.log(JSON.stringify({
moveResult,
pathLength: search.path.length,
ops: search.ops,
cost: search.cost,
incomplete: search.incomplete
}));
A non-empty search.path is not proof of success. Check incomplete before treating the path as a completed route.
Validate the goal range first
Many Screeps targets occupy tiles a Creep cannot stand on. The official PathFinder documentation specifically warns that an unwalkable goal such as a Source should use a range of at least 1. Requesting range: 0 can make an otherwise reachable task impossible.
function validGoal(target, range) {
return Boolean(
target?.pos
&& Number.isInteger(range)
&& range >= 0
);
}
const goal = {
pos: source.pos,
range: 1
};
Use the action's actual range: adjacent actions commonly need range 1, while build, repair, and Controller upgrading use range 3. If the Creep already satisfies the requested range, an empty path can be correct because no movement is needed.
Do not confuse search cost with movement fatigue
PathFinder cost is a route-selection weight. Movement fatigue is a Creep movement mechanic. They can use similar numbers, but they answer different questions.
| System | Plain | Swamp | Road | Purpose |
|---|---|---|---|---|
Standalone PathFinder.search() defaults | 1 | 5 | Only preferred if your matrix gives the road a lower cost | Choose a route |
| Actual Creep movement fatigue rate | 2 | 10 | 1 | Determine fatigue generated by a move |
If you deliberately run PathFinder with plainCost: 2, swampCost: 10, and road tiles set to 1, you are choosing search weights that make roads cheaper than plains. Those values are not the standalone PathFinder defaults, and the resulting search.cost is not a live measurement of Creep fatigue.
const search = PathFinder.search(
creep.pos,
{ pos: target.pos, range: desiredRange },
{
// Chosen search weights so road cost 1 is preferred.
// PathFinder defaults are plain 1 / swamp 5.
plainCost: 2,
swampCost: 10,
roomCallback(roomName) {
const room = Game.rooms[roomName];
if (!room) return undefined;
return buildDiagnosticMatrix(room);
}
}
);
For body weight and fatigue calculations, use the movement-speed guide instead of reading PathFinder cost as a speed metric.
Read the complete PathFinder result
Record the result before changing search limits:
const result = PathFinder.search(
creep.pos,
{ pos: target.pos, range: desiredRange },
{
maxOps: 2000,
maxRooms: 16
}
);
console.log(JSON.stringify({
pathLength: result.path.length,
ops: result.ops,
cost: result.cost,
incomplete: result.incomplete
}));
2000 operations and 16 rooms are the documented PathFinder defaults. They are not recommended production values for every bot. If a constrained diagnostic uses lower values, an incomplete path may be caused by your constraint rather than by an impossible world route.
Build a diagnostic CostMatrix
function buildDiagnosticMatrix(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;
}
const walkableRampart =
structure.structureType === STRUCTURE_RAMPART
&& (
structure.my === true
|| structure.isPublic === true
);
const walkable =
structure.structureType === STRUCTURE_CONTAINER
|| walkableRampart;
if (!walkable) {
costs.set(structure.pos.x, structure.pos.y, 255);
}
}
return costs;
}
A CostMatrix value of 255 is unwalkable. Do not mark your own private Ramparts as blocked: your Creeps can pass them. Public Ramparts are also walkable. For a diagnostic matrix, keep this walkability rule visible so a Boolean mistake cannot silently remove the only route.
Check callbacks and search limits
A roomCallback is a routing decision, not just a performance hook. Returning false rejects that room from the PathFinder search. Returning undefined leaves that room to default terrain handling when you have no custom matrix.
- Log which room names your callback rejects.
- Check whether
maxRoomsexcludes the destination or an unavoidable corridor. - Record
opsbefore increasingmaxOps. - Do not replace a broken matrix with an unlimited search budget.
Check the room-level route
const route = Game.map.findRoute(
creep.pos.roomName,
target.pos.roomName
);
if (route === ERR_NO_PATH) {
console.log('No room-level route was found.');
}
If your route callback returns Infinity for a required room, a room-level route can fail before local tile costs matter. Keep strategic room routing separate from local CostMatrix debugging.
Handle noPathFinding separately
const cachedResult = creep.moveTo(target, {
range: desiredRange,
noPathFinding: true
});
if (cachedResult === ERR_NOT_FOUND) {
const freshResult = creep.moveTo(target, {
range: desiredRange,
noPathFinding: false
});
console.log(JSON.stringify({
cachedResult,
freshResult
}));
}
ERR_NOT_FOUND in this branch means the cache-only request had no usable path. It is not interchangeable with ERR_NO_PATH.
Separate traffic from search failure
A valid path can exist and moveTo() can return OK while the Creep later remains on the same tile because movement resolution, another movement call, fatigue, or traffic changed the outcome. That is not evidence that PathFinder failed. Track room-aware positions across later ticks and inspect the final movement intent instead.
Minimal diagnostic workflow
- Confirm the target exists and the requested range is reachable.
- Separate
ERR_NO_PATH, cache-onlyERR_NOT_FOUND, andPathFinder.incomplete. - Inspect
path.length,ops,cost, andincomplete. - Review CostMatrix blockers and owned/public Ramparts.
- Log
roomCallbackrejections and search limits. - For cross-room work, check
Game.map.findRoute(). - Only after the search succeeds, move on to traffic/fatigue diagnostics.
What this guide does not prove
This revision has current official-documentation review and offline code review. It does not include a real-shard path trace showing a particular room, CostMatrix, or cross-room route failing and then succeeding. Console and live multi-tick verification remain pending.