PATHFINDING · COSTMATRIX AND ROOM CALLBACKS

How to Build a Safe PathFinder CostMatrix in Screeps

Use CostMatrix values 0–255 correctly, classify roads and structures, separate invisible rooms from blocked rooms, layer dynamic Creep costs, validate coordinates, and reject incomplete PathFinder results.

Verification statusChinese source article: Reviewed in full · Official docs: Checked — CostMatrix values, roomCallback, search results, serialize(), deserialize() and moveByPath() · Visibility boundary: undefined keeps terrain-only search; false deliberately prevents searching the room

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official docs
Checked — CostMatrix values, roomCallback, search results, serialize(), deserialize() and moveByPath()
Visibility boundary
undefined keeps terrain-only search; false deliberately prevents searching the room
Override boundary
Custom medium costs never replace an existing 255 obstacle
JavaScript syntax
Passed
Offline matrix review
Passed — roads, Containers, owned Ramparts, obstacles, Creep overlays, custom costs and coordinate bounds
Screeps Console test
Pending
Live PathFinder, CPU and multi-tick traffic test
Pending
Last verified
July 25, 2026

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 valuePathFinder meaningTypical use
0Use the tile's terrain costNo custom override
1–254Use the custom costRoad preference, congestion, risk
255UnwalkableSolid 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.

GoalTypical range decision
Source or Mineral1 for harvest
Container or selected walkable tile0 when the tile itself is the goal
Construction Site3 for build
Damaged Structure3 for repair
Controller3 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

StageRepresentative resultWhat it proves
PathFinderincomplete: falseA complete path was found under the supplied model
MovementmoveByPath() === OKThe movement command was accepted this tick
Live outcomePosition changes over ticksThe 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 false only for explicitly blocked rooms.
  • Return undefined when 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.

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.