MOVEMENT · NO-PROGRESS DEBUGGING

Why moveTo() Returns OK but Your Screeps Creep Does Not Move

Distinguish an accepted movement order from later position change, validate the target and active MOVE parts, inspect fatigue and range, track roomName:x:y across ticks, and diagnose traffic, cached paths, and overwritten movement orders.

Verification statusChinese source article: Reviewed in full · Official docs: Checked — moveTo(), fatigue, active body parts and simultaneous actions · Timing boundary: OK schedules movement; position must be checked on a later tick

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official docs
Checked — moveTo(), fatigue, active body parts and simultaneous actions
Timing boundary
OK schedules movement; position must be checked on a later tick
Source correction
Current moveTo() return table does not list ERR_NO_BODYPART
JavaScript syntax
Passed
Offline progress review
Passed — target, spawning, MOVE, fatigue, range, result and unchanged-position states
Screeps Console test
Pending
Live traffic and path-cache test
Pending
Last verified
July 25, 2026

Quick answer

creep.moveTo(target) returning OK means the movement order was scheduled. It does not mean creep.pos changed during the current script or that the Creep will definitely enter the intended tile after resolution. Validate the target, spawning state, active MOVE parts, fatigue, and desired range; then record roomName:x:y over several ticks to distinguish a call error from accepted movement with no progress.

Separate call failure from later no progress

ObservationPrimary investigation
moveTo() returns an errorTarget, ownership, spawning, fatigue, cached path, or pathfinding result.
moveTo() returns OK, but later positions are unchangedTraffic, later movement calls, cached-route suitability, room edges, or movement timing.

Do not collapse both cases into “the Creep is stuck.” Save the current return value and compare positions from later ticks.

Validate the target position

function hasValidPosition(target) {
  return Boolean(
    target
    && target.pos
    && Number.isInteger(target.pos.x)
    && Number.isInteger(target.pos.y)
    && typeof target.pos.roomName === 'string'
    && target.pos.roomName.length > 0
  );
}

A deleted Flag, stale Memory ID, Game.getObjectById() returning null, or malformed coordinate object can make the target invalid. Review safe target restoration for saved IDs.

Check spawning, active MOVE parts and fatigue

if (creep.spawning) {
  return {
    status: 'creep-spawning'
  };
}

const activeMoveParts =
  creep.getActiveBodyparts(MOVE);

if (activeMoveParts <= 0) {
  return {
    status: 'no-active-move-part'
  };
}

if (creep.fatigue > 0) {
  return {
    status: 'creep-tired',
    fatigue: creep.fatigue
  };
}

A MOVE part in the original body does not prove that an active MOVE part remains after damage. Fatigue is reduced on later ticks by active MOVE parts, and ordinary movement cannot proceed while fatigue is positive.

Stop when the desired range is already reached

if (
  creep.pos.roomName === target.pos.roomName
  && creep.pos.inRangeTo(target, desiredRange)
) {
  return {
    status: 'already-in-range'
  };
}

A Creep that is already within range 3 of a Controller should stop moving and perform the work action. Using range 0 for an occupied Source, Controller, Construction Site, or most Structures asks for an impossible destination tile.

Track roomName:x:y across ticks

function getPositionKey(pos) {
  return [
    pos.roomName,
    pos.x,
    pos.y
  ].join(':');
}

Saving only x and y can misclassify movement across a room border. Store the room name as part of the position key. Compare the current key with the previous tick's key, not with a position read later in the same execution.

Complete multi-tick movement diagnostic

State impact: this script writes a bounded diagnostic record to creep.memory.moveDiagnostic, may submit one movement order, and temporarily disables path reuse after repeated unchanged positions.

function hasValidPosition(target) {
  return Boolean(
    target
    && target.pos
    && Number.isInteger(target.pos.x)
    && Number.isInteger(target.pos.y)
    && typeof target.pos.roomName === 'string'
  );
}

function getPositionKey(pos) {
  return [
    pos.roomName,
    pos.x,
    pos.y
  ].join(':');
}

function runMoveDiagnostic(
  creep,
  target,
  desiredRange
) {
  if (!creep) {
    return {
      status: 'creep-missing'
    };
  }

  if (!hasValidPosition(target)) {
    return {
      status: 'target-invalid'
    };
  }

  if (creep.spawning) {
    return {
      status: 'creep-spawning'
    };
  }

  const activeMoveParts =
    creep.getActiveBodyparts(MOVE);

  if (activeMoveParts <= 0) {
    return {
      status: 'no-active-move-part'
    };
  }

  if (
    !Number.isInteger(desiredRange)
    || desiredRange < 0
  ) {
    return {
      status: 'range-invalid'
    };
  }

  const sameRoom =
    creep.pos.roomName === target.pos.roomName;
  const currentRange = sameRoom
    ? creep.pos.getRangeTo(target)
    : null;

  if (
    currentRange !== null
    && currentRange <= desiredRange
  ) {
    return {
      status: 'already-in-range',
      currentRange
    };
  }

  const positionBefore = getPositionKey(creep.pos);
  const previous = creep.memory.moveDiagnostic;
  const unchangedTicks =
    previous?.position === positionBefore
      ? (previous.unchangedTicks ?? 0) + 1
      : 0;
  const reusePath = unchangedTicks >= 2 ? 0 : 5;

  const moveResult = creep.moveTo(target, {
    range: desiredRange,
    reusePath,
    visualizePathStyle: {
      stroke: '#ffcc00',
      opacity: 0.55
    }
  });

  creep.memory.moveDiagnostic = {
    targetRoom: target.pos.roomName,
    targetX: target.pos.x,
    targetY: target.pos.y,
    desiredRange,
    position: positionBefore,
    unchangedTicks,
    fatigue: creep.fatigue,
    activeMoveParts,
    moveResult,
    reusePath,
    checkedAt: Game.time
  };

  return {
    status: moveResult === OK
      ? 'move-submitted'
      : 'move-failed',
    moveResult,
    positionBefore,
    unchangedTicks,
    fatigue: creep.fatigue,
    activeMoveParts,
    reusePath
  };
}

module.exports.loop = function () {
  const creep = Game.creeps.Worker1;
  const target = Game.flags.WorkTarget;
  const outcome = runMoveDiagnostic(
    creep,
    target,
    1
  );

  if (
    outcome.status === 'move-failed'
    || outcome.unchangedTicks >= 3
  ) {
    console.log(JSON.stringify({
      type: 'move-diagnostic',
      creepName: creep?.name ?? null,
      ...outcome
    }));
  }
};

reusePath: 0 is used only after repeated no-progress observations. Setting it for every Creep on every tick can increase CPU use.

Why an OK order can still show no progress

A later movement call overwrote the first one

When the same Creep receives multiple movement calls in one tick, the later movement action takes precedence. Use one movement decision point rather than letting role, traffic, combat, and border modules all submit movement independently.

Another Creep occupied the intended tile

Pathfinding can produce a route, but temporary traffic can prevent entry during action resolution. This is a coordination problem, not proof that no route exists.

The cached path no longer fits

The default path reuse can lag behind changing traffic, construction, or target positions. Temporarily forcing a new search helps test that hypothesis.

The Creep is crossing a room edge

Track the room name with the coordinates. Edge transitions can look stationary when diagnostics compare only local x and y.

The body and terrain create slow movement

Load, swamp terrain, roads, and MOVE ratio affect fatigue generation and recovery. A Creep may move only on some ticks even though the logic submits movement repeatedly.

ERR_NO_PATH is a different problem

ERR_NO_PATH means the movement call could not find a route to the requested range. An OK order followed by temporary blockage is different. ERR_NOT_FOUND with noPathFinding: true means there was no reusable cached path. Continue with the dedicated pathfinding guide.

Current moveTo() return-code correction

The Chinese source listed ERR_NO_BODYPART as a moveTo() return. The current official return table does not list it. This English version checks active MOVE parts as a movement capability prerequisite and logs the server's actual moveTo() result without attributing an undocumented code to the method.

Debugging checklist

  • Validate the Creep and target position.
  • Wait until spawning finishes.
  • Check getActiveBodyparts(MOVE), not only the original body array.
  • Record fatigue.
  • Stop when the desired range is already reached.
  • Record roomName:x:y over several ticks.
  • Save the real moveTo() result.
  • Temporarily disable path reuse only after repeated unchanged positions.
  • Ensure one module makes the final movement decision.
  • Separate traffic blockage from ERR_NO_PATH.
  • Do not claim that OK proves position change.

Scope and next steps

This diagnostic does not implement traffic reservations, swapping, pull trains, cross-room route caches, combat kiting, or a colony-wide movement scheduler. Continue with ERR_NO_PATH diagnosis.

Frequently asked questions

Does OK mean the Creep already moved?

No. It means the order was scheduled. Verify a later tick.

Why can position remain unchanged?

Traffic, later movement calls, fatigue timing, stale paths, borders, or slow terrain movement can prevent visible progress.

Should reusePath always be zero?

No. Use that setting temporarily for diagnosis because repeated path searches can cost more CPU.

Is traffic blockage ERR_NO_PATH?

Not necessarily. A route can exist and the order can return OK while another unit temporarily blocks the next tile.

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.