MOVEMENT · FATIGUE AND BODY DESIGN

How to Calculate Screeps Creep Movement Speed

Calculate unboosted Creep movement from MOVE recovery, loaded body weight, road, plain, and swamp costs; account for empty CARRY parts; and separate a static body estimate from live multi-tick movement proof.

Verification statusChinese source article: Reviewed in full · Official docs: Checked — movement weight, terrain costs, empty CARRY and MOVE recovery · Formula boundary: Unboosted MOVE only; boosts and pull mechanics are excluded

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official docs
Checked — movement weight, terrain costs, empty CARRY and MOVE recovery
Formula boundary
Unboosted MOVE only; boosts and pull mechanics are excluded
Source correction
The estimator accepts the terrain being entered instead of treating the current tile as proof of the next step cost
JavaScript syntax
Passed
Offline calculation review
Passed — empty and loaded CARRY, roads, plains, swamps, invalid input and damaged MOVE capacity
Screeps Console test
Pending
Live multi-tick movement test
Pending
Last verified
July 25, 2026

Quick answer

An unboosted Creep's movement interval depends on two values: the fatigue generated by the body weight on the tile being entered, and the fatigue removed by active MOVE parts. Roads cost 1 fatigue per weight part, plains cost 2, and swamps cost 10. Each active unboosted MOVE part removes 2 fatigue per tick. Empty CARRY parts do not add weight.

The four movement rules

  • Every non-MOVE weight part generates fatigue when the Creep moves.
  • A road uses cost 1, a plain uses cost 2, and a swamp uses cost 10.
  • Each active unboosted MOVE part removes 2 fatigue per tick.
  • A Creep with positive fatigue cannot submit ordinary movement successfully.

The maximum normal speed is one tile per tick. Extra MOVE capacity beyond the amount required for zero carried fatigue does not create two-tile movement.

Count movement weight correctly

For an unboosted estimate, weight contains every non-MOVE body part except empty CARRY capacity. A loaded CARRY part counts when some resource occupies its 50-capacity segment.

function countLoadedCarryParts(
  activeCarryParts,
  usedCapacity
) {
  if (
    !Number.isInteger(activeCarryParts)
    || activeCarryParts < 0
    || !Number.isFinite(usedCapacity)
    || usedCapacity <= 0
  ) {
    return 0;
  }

  return Math.min(
    activeCarryParts,
    Math.ceil(usedCapacity / CARRY_CAPACITY)
  );
}

Examples for three unboosted CARRY parts:

Used capacityLoaded CARRY parts
00
1–501
51–1002
101–1503

Calculate ticks per step

fatigue generated = weight parts × terrain cost
fatigue removed per tick = active MOVE parts × 2
estimated ticks per step = ceil(
  fatigue generated / fatigue removed per tick
)

The result is never less than one tick. A Creep must have at least one active MOVE part to move under its own power.

const TERRAIN_COST = {
  road: 1,
  plain: 2,
  swamp: 10
};

function estimateTicksPerStep(input) {
  const {
    activeMoveParts,
    weightParts,
    terrain
  } = input;
  const terrainCost = TERRAIN_COST[terrain];

  if (
    !Number.isInteger(activeMoveParts)
    || activeMoveParts <= 0
    || !Number.isInteger(weightParts)
    || weightParts < 0
    || !Number.isInteger(terrainCost)
  ) {
    return {
      movable: false,
      reason: 'invalid-input'
    };
  }

  const fatigueGenerated = weightParts * terrainCost;
  const fatigueRemovedPerTick = activeMoveParts * 2;

  return {
    movable: true,
    reason: 'estimated',
    fatigueGenerated,
    fatigueRemovedPerTick,
    ticksPerStep: Math.max(
      1,
      Math.ceil(
        fatigueGenerated / fatigueRemovedPerTick
      )
    )
  };
}

Road, plain, and swamp examples

Consider a fully loaded [WORK, CARRY, MOVE] Creep. It has two weight parts and one MOVE part.

TerrainGeneratedRemoved per tickEstimated interval
Road2 × 1 = 221 tick per step
Plain2 × 2 = 422 ticks per step
Swamp2 × 10 = 20210 ticks per step

Adding a second MOVE part changes the plain result to one tile per tick, but the same body still needs several ticks per swamp step.

Why empty CARRY parts change the result

[CARRY, CARRY, MOVE] has zero movement weight while empty, one weight part with up to 50 resources, and two weight parts above 50 resources. A hauler can therefore travel outward quickly and return more slowly when loaded.

Do not count every CARRY part automatically. Also do not assume all resources are energy; creep.store.getUsedCapacity() counts the complete load.

Complete unboosted movement calculator

State impact: this diagnostic reads the body and Store and returns an estimate. It does not move the Creep or write Memory.

const TERRAIN_COST = {
  road: 1,
  plain: 2,
  swamp: 10
};

function countLoadedCarryParts(creep) {
  const usedCapacity =
    creep.store.getUsedCapacity() ?? 0;
  const activeCarryParts =
    creep.getActiveBodyparts(CARRY);

  if (usedCapacity <= 0) {
    return 0;
  }

  return Math.min(
    activeCarryParts,
    Math.ceil(usedCapacity / CARRY_CAPACITY)
  );
}

function countWeightParts(creep) {
  const activeOtherParts = creep.body.filter(
    part =>
      part.hits > 0
      && part.type !== MOVE
      && part.type !== CARRY
  ).length;

  return activeOtherParts
    + countLoadedCarryParts(creep);
}

function estimateCreepMovement(creep, terrain) {
  if (!creep || creep.spawning) {
    return {
      status: 'creep-unavailable'
    };
  }

  const terrainCost = TERRAIN_COST[terrain];
  const activeMoveParts =
    creep.getActiveBodyparts(MOVE);
  const weightParts = countWeightParts(creep);

  if (!Number.isInteger(terrainCost)) {
    return {
      status: 'terrain-invalid'
    };
  }

  if (activeMoveParts <= 0) {
    return {
      status: 'no-active-move-part',
      activeMoveParts,
      weightParts,
      currentFatigue: creep.fatigue
    };
  }

  const fatigueGenerated =
    weightParts * terrainCost;
  const fatigueRemovedPerTick =
    activeMoveParts * 2;

  return {
    status: 'movement-estimated',
    terrain,
    activeMoveParts,
    weightParts,
    currentFatigue: creep.fatigue,
    fatigueGenerated,
    fatigueRemovedPerTick,
    estimatedTicksPerStep: Math.max(
      1,
      Math.ceil(
        fatigueGenerated / fatigueRemovedPerTick
      )
    )
  };
}

module.exports.loop = function () {
  const creep = Game.creeps.Hauler1;

  if (!creep || Game.time % 25 !== 0) {
    return;
  }

  console.log(JSON.stringify({
    type: 'movement-estimate',
    creepName: creep.name,
    ...estimateCreepMovement(creep, 'plain')
  }));
};

The terrain argument describes the route segment being evaluated. Do not label one current-tile snapshot as a measured route speed.

Practical body ratios

Expected terrainUnboosted maximum-speed target
Mostly roads1 MOVE per 2 loaded weight parts
Mostly plains1 MOVE per 1 loaded weight part
Mostly swamps5 MOVE per 1 loaded weight part

Swamp-speed bodies are often impractical. Roads, route changes, reduced payload, or MOVE boosts may be cheaper than adding enough MOVE parts for one-tile-per-tick swamp travel.

Why damage can slow a Creep

Use getActiveBodyparts(MOVE), not the original body array length. A MOVE part at zero hits no longer contributes movement recovery. The same body can therefore move normally before combat and accumulate fatigue afterward.

Live diagnosis should record active MOVE parts, Store usage, fatigue, terrain, and position over multiple ticks.

Terrain-sampling correction

The Chinese source's complete diagnostic read the Creep's current tile and used that value as the movement estimate. A route can enter a different terrain type on the next step. This English version requires the terrain being evaluated as an explicit input and labels the result as an estimate rather than a measured next-step fact.

Debugging checklist

  • Use active MOVE parts, not the original count.
  • Count only loaded CARRY capacity as weight.
  • Use costs 1, 2, and 10 for road, plain, and swamp.
  • Keep unboosted and boosted formulas separate.
  • Include current fatigue in live logs.
  • Evaluate the terrain of the route segment, not only the current tile.
  • Do not confuse a static estimate with multi-tick proof.
  • Check traffic and overwritten movement orders before adding MOVE parts.

Scope and next steps

This guide excludes MOVE boosts, pull chains, Power Creeps, traffic reservations, mixed-terrain route statistics, and live server timing. Continue with RoomPosition distance methods before using range as a path estimate.

Frequently asked questions

What is the plain-terrain ratio?

One active unboosted MOVE part per loaded weight part supports maximum normal speed on plains.

Why is an empty hauler faster?

Empty CARRY parts do not generate fatigue.

Does more MOVE exceed one tile per tick?

No. It can remove fatigue faster, but ordinary movement remains capped at one tile per tick.

Is the formula a live test?

No. Observe several ticks to include traffic, fatigue history, terrain changes, and command conflicts.

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.