MOVEMENT · CROSS-ROOM ROUTING

How to Plan and Execute a Cross-Room Route in Screeps

Use Game.map.findRoute() as a room-level plan, assign routeCallback costs, reject rooms deliberately, validate the first exit step, rebuild after room transitions, and keep route preference separate from live safety intel.

Verification statusChinese source article: Reviewed in full · Official docs: Checked — findRoute(), findExit(), describeExits() and routeCallback · Routing boundary: Room-level route steps are separated from tile-level pathfinding

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official docs
Checked — findRoute(), findExit(), describeExits() and routeCallback
Routing boundary
Room-level route steps are separated from tile-level pathfinding
Source correction
A route rebuilt from the current room uses and validates its first step instead of searching the entire stored route for a matching exit
JavaScript syntax
Passed
Offline route review
Passed — same room, invalid names, avoided rooms, highway costs, ERR_NO_PATH, exit validation and room transitions
Screeps Console test
Pending
Live cross-room movement test
Pending
Last verified
July 25, 2026

Quick answer

Game.map.findRoute(fromRoom, toRoom) creates a high-level sequence of rooms and exit directions. It does not move a Creep and does not return every tile. Handle ERR_NO_PATH before reading the array, assign room-entry costs with routeCallback, validate the first step against describeExits(), move toward that exit, and rebuild from the new current room after crossing the boundary.

Room routes and tile paths

ToolLevelResult
Game.map.findRoute()RoomsExit direction and next room
Game.map.findExit()Current roomExit direction toward another room
PathFinder.search()TilesRoomPosition path and search metadata
creep.moveTo()Movement commandScheduled order or error code

A valid room sequence does not prove that the tile-level path, traffic state, hostile Ramparts, or live room conditions permit safe travel.

Handle arrays and error codes

function calculateRoomRoute(fromRoom, targetRoom) {
  if (fromRoom === targetRoom) {
    return {
      ok: true,
      code: OK,
      steps: []
    };
  }

  const route = Game.map.findRoute(
    fromRoom,
    targetRoom
  );

  if (!Array.isArray(route)) {
    return {
      ok: false,
      code: route,
      steps: []
    };
  }

  return {
    ok: true,
    code: OK,
    steps: route
  };
}

Never call [0].exit before checking that the result is an array with a first step. The same-room case is successful completion, not a routing error.

routeCallback is an entry cost

const routeOptions = {
  routeCallback(roomName, fromRoomName) {
    if (Memory.routeAvoid?.includes(roomName)) {
      return Infinity;
    }

    if (isPreferredRoom(roomName)) {
      return 1;
    }

    return 2.5;
  }
};

Lower values are preferred. Infinity rejects the room completely. Values such as 1 and 2.5 are policy weights, not official recommendations and not travel time in ticks.

Classify highway rooms

function isHighwayRoom(roomName) {
  const match = /^[WE](\d+)[NS](\d+)$/.exec(
    roomName
  );

  if (!match) {
    return false;
  }

  const horizontal = Number(match[1]);
  const vertical = Number(match[2]);

  return horizontal % 10 === 0
    || vertical % 10 === 0;
}

This classifies a room name. It does not prove that the room is open, safe, visible, unreserved, or free of hostiles.

No vision does not mean safe

Game.rooms[roomName] exists only when the room is visible. Without vision, a route callback cannot directly read current hostile Creeps, Towers, Controller ownership, Invader Cores, Nukes, or temporary combat conditions. Use your own timestamped intel summary when those facts affect routing.

function getIntelRisk(roomName) {
  const intel = Memory.roomIntel?.[roomName];

  if (!intel || Game.time - intel.seenAt > 5000) {
    return 'unknown';
  }

  return intel.risk;
}

The 5,000-tick age is an example policy, not an official safe value.

Build and validate a route

function getRouteRoomCost(
  roomName,
  avoidedRooms
) {
  if (avoidedRooms.has(roomName)) {
    return Infinity;
  }

  const visibleRoom = Game.rooms[roomName];
  const owned = Boolean(
    visibleRoom?.controller?.my
  );

  if (owned || isHighwayRoom(roomName)) {
    return 1;
  }

  return 2.5;
}

function calculateRoute(fromRoom, targetRoom) {
  if (fromRoom === targetRoom) {
    return {
      ok: true,
      code: OK,
      steps: []
    };
  }

  const avoidedRooms = new Set(
    Array.isArray(Memory.routeAvoid)
      ? Memory.routeAvoid
      : []
  );

  const route = Game.map.findRoute(
    fromRoom,
    targetRoom,
    {
      routeCallback(roomName) {
        return getRouteRoomCost(
          roomName,
          avoidedRooms
        );
      }
    }
  );

  if (!Array.isArray(route)) {
    return {
      ok: false,
      code: route,
      steps: []
    };
  }

  return {
    ok: true,
    code: OK,
    steps: route.map(step => ({
      exit: step.exit,
      room: step.room
    }))
  };
}

Complete per-room route executor

State impact: this script writes creep.memory.routePlan and may submit one movement order. It rebuilds the plan whenever the current room or destination changes.

function isHighwayRoom(roomName) {
  const match = /^[WE](\d+)[NS](\d+)$/.exec(
    roomName
  );

  if (!match) {
    return false;
  }

  return Number(match[1]) % 10 === 0
    || Number(match[2]) % 10 === 0;
}

function calculateRoute(fromRoom, targetRoom) {
  const avoidedRooms = new Set(
    Array.isArray(Memory.routeAvoid)
      ? Memory.routeAvoid
      : []
  );

  const route = Game.map.findRoute(
    fromRoom,
    targetRoom,
    {
      routeCallback(roomName) {
        if (avoidedRooms.has(roomName)) {
          return Infinity;
        }

        const room = Game.rooms[roomName];
        const owned = Boolean(
          room?.controller?.my
        );

        return owned || isHighwayRoom(roomName)
          ? 1
          : 2.5;
      }
    }
  );

  if (!Array.isArray(route)) {
    return {
      ok: false,
      code: route,
      steps: []
    };
  }

  return {
    ok: true,
    code: OK,
    steps: route
  };
}

function getValidatedFirstStep(creep, targetRoom) {
  creep.memory.routePlan ??= {};
  const plan = creep.memory.routePlan;
  const needsRebuild =
    plan.targetRoom !== targetRoom
    || plan.fromRoom !== creep.room.name
    || !Array.isArray(plan.steps);

  if (needsRebuild) {
    const result = calculateRoute(
      creep.room.name,
      targetRoom
    );

    if (!result.ok) {
      return result;
    }

    creep.memory.routePlan = {
      targetRoom,
      fromRoom: creep.room.name,
      steps: result.steps,
      builtAt: Game.time
    };
  }

  const currentPlan = creep.memory.routePlan;
  const step = currentPlan.steps[0];

  if (!step) {
    return {
      ok: false,
      code: ERR_NO_PATH,
      steps: []
    };
  }

  const exits = Game.map.describeExits(
    creep.room.name
  );

  if (!exits || exits[step.exit] !== step.room) {
    delete creep.memory.routePlan;
    return {
      ok: false,
      code: ERR_NO_PATH,
      steps: []
    };
  }

  return {
    ok: true,
    code: OK,
    step
  };
}

module.exports.loop = function () {
  const creep = Game.creeps.Scout1;
  const targetRoom = 'W8N3';

  if (!creep) {
    return;
  }

  if (creep.room.name === targetRoom) {
    delete creep.memory.routePlan;
    return;
  }

  const routeResult = getValidatedFirstStep(
    creep,
    targetRoom
  );

  if (!routeResult.ok || !routeResult.step) {
    if (Game.time % 100 === 0) {
      console.log(JSON.stringify({
        type: 'route-not-found',
        creepName: creep.name,
        roomName: creep.room.name,
        targetRoom,
        code: routeResult.code
      }));
    }
    return;
  }

  const exitPosition =
    creep.pos.findClosestByRange(
      routeResult.step.exit
    );

  if (!exitPosition) {
    delete creep.memory.routePlan;
    return;
  }

  const moveResult = creep.moveTo(
    exitPosition,
    {
      reusePath: 10,
      maxRooms: 1
    }
  );

  if (
    moveResult !== OK
    && moveResult !== ERR_TIRED
    && Game.time % 20 === 0
  ) {
    console.log(JSON.stringify({
      type: 'route-move-failed',
      creepName: creep.name,
      roomName: creep.room.name,
      nextRoom: routeResult.step.room,
      moveResult
    }));
  }
};

The first step is correct because the route was built from the current room. Validating it against describeExits() prevents a stale or malformed step from being executed.

Why the plan rebuilds after each room

When the Creep crosses an exit, plan.fromRoom !== creep.room.name becomes true and the route is rebuilt. This design is intentionally simple and recoverable. A whole-route cursor can save route calculations, but then it must handle pushes, portals, unexpected rooms, changing avoidance policy, stale intel, and shared cache versions.

Use Infinity deliberately

Infinity means “never enter this room.” It is appropriate for a hard block, not merely a preference. A civilian hauler, emergency defender, and scout may need different route policies. Returning a large finite cost keeps a risky corridor available as a last resort; returning Infinity removes it.

Entering the target room is not task completion

The example stops when creep.room.name === targetRoom. Real role code must then choose a destination position, Flag, Controller, Structure, or object ID inside the room. Crossing the room boundary does not mean the final task has completed.

Debugging checklist

  • Handle same-room completion before routing.
  • Check for an array before reading route steps.
  • Treat route weights as preferences, not travel ticks.
  • Use Infinity only for hard rejection.
  • Do not interpret missing vision as safety.
  • Validate the first step with describeExits().
  • Rebuild after the current room changes.
  • Handle a missing exit position.
  • Log the movement result separately from the route result.
  • Remember that room-level success does not prove a tile-level path.

Scope and next steps

This guide does not implement persistent shared route caches, portals, shard travel, hostile-room scoring, SK-room timing, traffic reservations, room-status policy, or destination behavior. Console and live cross-room verification remain Pending.

Frequently asked questions

Does findRoute() return tile coordinates?

No. It returns room-level exit steps.

What happens when routeCallback returns Infinity?

The room is rejected. Rejecting every corridor can produce ERR_NO_PATH.

Are highways always safe?

No. Highway classification is only a room-name property.

Why use only the first route step?

The route was built from the current room, so its first step is the next room transition. Rebuild after crossing.

Official documentation

SOURCE AND SCOPE

Review the source, evidence, or next system

This English guide is rewritten for a focused search intent while preserving the technical scope and verification boundaries of its Chinese source. Live-room evidence is claimed only when the verification record says it exists.