Use this guide when
Use this page when a Creep must travel to another room and you need to turn Game.map.findRoute() into one validated next-room step. The method plans rooms. It does not select every tile, move the Creep, prove the exit is reachable, or complete the task inside the destination room.
Use the ERR_NO_PATH guide when the immediate problem is a failed route or path search.
Keep four stages separate
| Stage | Question | Evidence |
|---|---|---|
| Room plan | Which room should be entered next? | findRoute() result |
| Exit selection | Which exit tile is reachable now? | Current-room path search |
| Border transition | Did the Creep enter the next room? | Later-tick room name |
| Destination task | What position or object is the final target? | Role-specific logic |
A successful room route can still fail at the tile level. Reaching the target room can still leave the final task unfinished.
Use finite costs for preferences and Infinity for bans
function getRouteCost(roomName, policy) {
if (policy.blockedRooms.has(roomName)) {
return Infinity;
}
const intel = Memory.roomIntel?.[roomName];
const recentRisk = Boolean(
intel
&& Number.isInteger(intel.seenAt)
&& Game.time - intel.seenAt <= policy.maxIntelAge
&& intel.risk === 'high'
);
if (recentRisk) {
return 10;
}
return policy.preferredRooms.has(roomName)
? 1
: 2.5;
}
The values are relative policy weights, not travel ticks. A large finite cost keeps a room available as a last resort. Infinity removes it completely. Missing or stale intel is uncertainty, not proof of safety.
The current official engine normalizes every non-Infinity callback result with Number(value) || 1. That makes falsy or non-numeric values a subtle trap: 0, false, null, undefined, an empty string, and NaN all become room-entry cost 1. They are not zero-cost preferences and they do not block the room.
Validate routeCallback policy before the engine sees it
Do not overload callback falsiness with business meaning. Normalize your own configuration into either a positive finite cost or the explicit hard-block sentinel Infinity.
function normalizeRoutePolicyCost(value) {
if (value === Infinity) {
return Infinity;
}
const cost = Number(value);
if (!Number.isFinite(cost) || cost <= 0) {
return 1;
}
return cost;
}
function getConfiguredRouteCost(roomName) {
if (Memory.routeAvoid?.includes(roomName)) {
return Infinity;
}
return normalizeRoutePolicyCost(
Memory.routeCosts?.[roomName]
);
}
The helper's cost <= 0 → 1 rule is deliberate project validation. The engine expression itself does not repair negative finite values, so accepting only positive finite numbers or Infinity keeps configuration semantics explicit. Numeric strings such as "2.5" are coerced by the engine, but storing numeric policy as numbers is clearer.
| Callback return | Current engine entry cost | Recommended interpretation |
|---|---|---|
Infinity | Room skipped | Explicit hard block |
0, false, null, undefined, NaN | 1 | Do not use as block or zero-cost preference |
"2.5" | 2.5 | Works through numeric coercion; prefer numeric configuration |
| positive finite number | that number | Normal room-entry policy cost |
Handle same-room completion and error codes first
function calculateRoomRoute(
fromRoom,
targetRoom,
policy
) {
if (
typeof fromRoom !== 'string'
|| typeof targetRoom !== 'string'
) {
return {
status: 'room-name-invalid',
steps: []
};
}
if (fromRoom === targetRoom) {
return {
status: 'target-room-reached',
steps: []
};
}
const route = Game.map.findRoute(
fromRoom,
targetRoom,
{
routeCallback(roomName) {
return getRouteCost(roomName, policy);
}
}
);
if (!Array.isArray(route)) {
return {
status: 'route-failed',
code: route,
steps: []
};
}
return {
status: 'route-ready',
code: OK,
steps: route.map(step => ({
exit: step.exit,
room: step.room
}))
};
}
Do not read route[0] until the result is confirmed to be an array. An empty array is correct when the current room already equals the target room. That boundary is specific to findRoute(). Game.map.findExit(), which calls findRoute() internally, returns ERR_INVALID_ARGS when there is no first route step; invalid room names return ERR_NO_PATH from findRoute().
Validate only the first step from the current room
function validateFirstRouteStep(
currentRoom,
routeResult
) {
if (routeResult.status !== 'route-ready') {
return routeResult;
}
const step = routeResult.steps[0];
if (!step) {
return {
status: 'first-step-missing'
};
}
const exits = Game.map.describeExits(currentRoom);
if (!exits || exits[step.exit] !== step.room) {
return {
status: 'first-step-invalid',
step
};
}
return {
status: 'first-step-ready',
step
};
}
The route was built from the current room, so the first step is the only room-level instruction needed now. Searching an old full route for a matching exit can select a stale step after an unexpected room transition.
Choose a reachable exit tile in the current room
function chooseExitPosition(creep, step) {
if (!creep || !step) {
return {
status: 'input-missing',
position: null
};
}
const position = creep.pos.findClosestByPath(
step.exit,
{
maxOps: 4000,
ignoreCreeps: false
}
);
return position
? { status: 'exit-position-ready', position }
: { status: 'exit-position-unreachable', position: null };
}
findClosestByPath() answers a tile-level question inside the current room. A valid room step does not guarantee that walls, structures, traffic, or custom costs allow access to an exit tile.
Execute one room step and rebuild after transition
function moveTowardTargetRoom(
creep,
targetRoom,
policy
) {
if (!creep) {
return { status: 'creep-missing' };
}
if (creep.room.name === targetRoom) {
delete creep.memory.routeStep;
return { status: 'target-room-reached' };
}
const routeResult = calculateRoomRoute(
creep.room.name,
targetRoom,
policy
);
const firstStep = validateFirstRouteStep(
creep.room.name,
routeResult
);
if (firstStep.status !== 'first-step-ready') {
return firstStep;
}
const exitResult = chooseExitPosition(
creep,
firstStep.step
);
if (exitResult.status !== 'exit-position-ready') {
return exitResult;
}
creep.memory.routeStep = {
fromRoom: creep.room.name,
targetRoom,
nextRoom: firstStep.step.room,
exit: firstStep.step.exit,
builtAt: Game.time
};
const moveResult = creep.moveTo(
exitResult.position,
{
range: 0,
reusePath: 5,
maxRooms: 1
}
);
return {
status: moveResult === OK
? 'movement-submitted'
: 'movement-rejected',
moveResult,
nextRoom: firstStep.step.room,
exitPosition: {
roomName: exitResult.position.roomName,
x: exitResult.position.x,
y: exitResult.position.y
}
};
}
Recalculate after creep.room.name changes. This trades some route calculations for a small, recoverable state boundary. A shared route cache can be added later only with destination, policy-version, and invalidation rules.
Treat the room edge as a later-tick observation
moveTo() submits movement for the current tick. Record the current room, next room, exit direction, exit position, and movement result. On later ticks, compare the new roomName:x:y. An accepted order does not prove the Creep crossed the border during the same script execution.
Room-name patterns are not live safety evidence
Highway or sector classification can influence preference, but it does not reveal current hostiles, Towers, ownership, Invader Cores, Nukes, or temporary combat conditions. Timestamp any stored intel and expose an unknown state rather than silently treating missing vision as safe.
Entering the target room is not final completion
After the room transition, the role still needs a target position, Flag, Controller, Source, Structure, or object ID. Use the RoomPosition guide for local range choices and the CostMatrix guide for custom tile policy.
Verify route, exit, and transition separately
Test same-room completion, a valid adjacent step, a hard-blocked corridor, a finite-risk corridor, an unreachable exit tile, and a successful room transition as separate cases. This revision contains static analysis and syntax checks only; live cross-room movement remains pending.