MOVEMENT · ACCEPTED INTENT DEBUGGING

How to Debug moveTo() Returning OK Without Position Progress

CURATED PATH

These links are maintained for a concrete prerequisite, failure mode, debugging step, or next lesson instead of generic topic similarity.

Movement

Recommended next steps

Separate action range from movement failureReturn-code boundaryCheck fatigue and MOVE ratioCommon causeContinue into path-search failuresPathfindingDraw the target and path while debuggingVisual debugging
Verification statusOfficial documentation: Checked August 17, 2026 — Creep.moveTo() return codes, reusePath/noPathFinding, game-loop timing, and same-Creep movement-call priority · Static code review: Passed — no-progress evidence now requires consecutive ticks, the same target/range, and a previous OK result; range satisfaction and final same-tick movement caller stay separate from traffic hypotheses · Evidence level: Official-documentation review and static code review only; no real-shard execution is claimed

VERIFICATION

Evidence and test status

Official documentation
Checked August 17, 2026 — Creep.moveTo() return codes, reusePath/noPathFinding, game-loop timing, and same-Creep movement-call priority
Static code review
Passed — no-progress evidence now requires consecutive ticks, the same target/range, and a previous OK result; range satisfaction and final same-tick movement caller stay separate from traffic hypotheses
Evidence level
Official-documentation review and static code review only; no real-shard execution is claimed
Console test pending
Not run in this editorial pass
Screeps Console test
Pending — no real-shard Console transcript was collected for this revision
Live multi-tick verification pending
No live traffic collision, same-tick movement override, or consecutive real-shard position-stall trace was collected for this revision
Last editorial review
August 17, 2026

Use this guide when

Use this guide only after the current movement call returns OK and later-tick observations show no position progress. A non-OK return belongs in the return-code branch first; a Creep already inside the requested range may correctly stay on the same tile.

function countUnchangedTicks(previous, currentKey) {
  const consecutive = previous
    && previous.tick === Game.time - 1
    && previous.position === currentKey;

  return consecutive
    ? (previous.unchangedTicks ?? 0) + 1
    : 0;
}

unchangedTicks is evidence of repeated position stability, not proof of a traffic cause. Combine it with the final movement intent, requested range, fatigue, and nearby occupancy before changing path policy.

Quick answer

moveTo() === OK means the movement request was accepted by that method call. It is not a promise that the Creep will occupy a different tile on the next tick. To debug an apparent stall, first prove that the call really returned OK, then compare the Creep position on later ticks and inspect the last movement call issued for that Creep in the current tick.

First confirm this is an OK-but-no-progress case

This page is deliberately narrower than a general movement guide. Do not use it when the current call already tells you why movement was rejected.

Current resultNext diagnostic
ERR_NO_PATHPath search failed. Use the ERR_NO_PATH guide.
ERR_TIREDInspect fatigue and body movement ratio.
ERR_BUSYCheck the Creep state, including whether it is still spawning.
ERR_NO_BODYPARTNo active MOVE part remains.
ERR_INVALID_TARGETFix the target before investigating traffic.
OKNow compare later-tick position progress and final movement intent.

Compare positions across ticks

Do not decide that a Creep is stuck from one line of code. Save a compact position key and compare it on a later tick:

function getPositionKey(pos) {
  if (!pos) return null;
  return pos.roomName + ':' + pos.x + ':' + pos.y;
}

function getTargetPosition(target) {
  if (target instanceof RoomPosition) {
    return target;
  }

  if (target?.pos instanceof RoomPosition) {
    return target.pos;
  }

  return null;
}

function getTargetKey(target) {
  const pos = getTargetPosition(target);
  return pos ? getPositionKey(pos) : null;
}

function recordMovementObservation(
  creep,
  target,
  desiredRange,
  moveResult,
  caller
) {
  Memory.moveDebug ??= {};

  const previous = Memory.moveDebug[creep.name] ?? null;
  const position = getPositionKey(creep.pos);
  const targetKey = getTargetKey(target);
  const sameSeries = Boolean(
    previous
    && previous.tick === Game.time - 1
    && previous.targetKey === targetKey
    && previous.desiredRange === desiredRange
    && previous.moveResult === OK
  );
  const previousAcceptedWithoutProgress = Boolean(
    sameSeries
    && previous.position === position
  );
  const consecutiveAcceptedStalls =
    previousAcceptedWithoutProgress
      ? (previous.consecutiveAcceptedStalls ?? 0) + 1
      : 0;

  Memory.moveDebug[creep.name] = {
    tick: Game.time,
    position,
    targetKey,
    desiredRange,
    moveResult,
    caller,
    consecutiveAcceptedStalls
  };

  return {
    previousAcceptedWithoutProgress,
    consecutiveAcceptedStalls,
    previousTick: previous?.tick ?? null,
    previousPosition: previous?.position ?? null,
    currentPosition: position
  };
}

The counter advances only when the previous observation is from the immediately preceding tick, targets the same position with the same requested range, and records moveTo() === OK. That makes it evidence for a repeated accepted-movement series instead of accidentally comparing unrelated observations several ticks apart. It still does not prove traffic, a stale cached path, or another cause; combine it with the final same-tick movement caller and local occupancy before changing policy.

Check whether movement is still needed

If you call moveTo(target, { range: 3 }) and the Creep is already within range 3, a lack of tile movement can be the correct outcome. Compare the current range with the range your role actually requires:

const desiredRange = 3;
const currentRange = creep.pos.getRangeTo(target);

if (currentRange <= desiredRange) {
  // Do the range-3 action instead of demanding another tile step.
}

This matters for builders, repairers, and upgraders. “Position unchanged” and “movement failure” are not the same observation.

Find the last movement call in the tick

Screeps resolves simultaneous movement actions by giving the later movement method call priority for the same Creep. An early moveTo() can therefore return OK even though a later module submits a different movement intent.

function submitMove(creep, target, reason) {
  const result = creep.moveTo(target, {
    reusePath: 5
  });

  creep.memory.lastMoveDecision = {
    tick: Game.time,
    reason,
    targetId: target.id ?? null,
    result
  };

  return result;
}

In production, centralize movement or at least overwrite one diagnostic record every time a movement method is submitted. The final record should identify the last caller, not the first one that happened to log.

Check traffic and occupied tiles

Accepted movement still competes with other movement intents and occupied positions. A path can be valid while the immediate step is not obtained on that tick. The caller does not receive a separate post-resolution collision return code.

Use a later-tick position comparison together with a small local snapshot:

const nearbyCreeps = creep.pos.findInRange(
  FIND_CREEPS,
  1
).map(other => ({
  name: other.name,
  x: other.pos.x,
  y: other.pos.y,
  my: other.my
}));

This is diagnostic input, not proof that any particular nearby Creep caused the stall. Repeated observations are needed before changing traffic policy.

Review path options without blaming the cache

reusePath can reduce repeated path work, but an accepted call plus no movement is not automatically a “bad cache” diagnosis. First rule out range, final same-tick intent, traffic, and call failures. If the route itself becomes invalid, inspect the path search branch and current obstacles instead of setting reusePath: 0 everywhere.

For persistent path-search failure, continue with ERR_NO_PATH. For movement speed caused by fatigue and body design, use the fatigue/body-ratio guide.

Validate the target position

function hasValidRoomPosition(target) {
  return Boolean(getTargetPosition(target));
}

The wrapper accepts either a real RoomPosition or a game object whose .pos is a RoomPosition. A plain deserialized {x, y, roomName} object is data, not a live RoomPosition; rebuild it with new RoomPosition(x, y, roomName) before using this helper. If your production caller uses the separate numeric x, y overload, normalize those coordinates to a RoomPosition for this diagnostic path.

Use one bounded diagnostic record

function debugMoveTo(creep, target, range = 1) {
  if (!creep || !hasValidRoomPosition(target)) {
    return { status: 'invalid-input' };
  }

  const before = getPositionKey(creep.pos);
  const result = creep.moveTo(target, {
    range,
    reusePath: 5
  });

  const observation = recordMovementObservation(
    creep,
    target,
    range,
    result,
    'debugMoveTo'
  );

  return {
    status: result === OK ? 'accepted' : 'call-failed',
    tick: Game.time,
    before,
    target: getTargetKey(target),
    currentRange: creep.pos.getRangeTo(target),
    requestedRange: range,
    fatigue: creep.fatigue,
    activeMove: creep.getActiveBodyparts(MOVE),
    result,
    previousPosition: observation.previousPosition,
    previousTick: observation.previousTick,
    previousAcceptedWithoutProgress:
      observation.previousAcceptedWithoutProgress,
    consecutiveAcceptedStalls:
      observation.consecutiveAcceptedStalls
  };
}

This record distinguishes current call failure from later-tick progress. It still does not prove why traffic resolution chose a particular outcome.

Production adaptation

A production movement manager should have one final owner for movement intent per Creep. Keep task selection separate from movement submission, record only bounded diagnostics, and define when a stationary Creep is expected because it already satisfies the role's range. Escalate to traffic/path replanning only after repeated later-tick evidence.

Checks in order before changing path policy

  1. Keep the current moveTo() return code. Leave this guide immediately for ERR_NO_PATH, ERR_TIRED, ERR_BUSY, ERR_INVALID_TARGET, or ERR_NO_BODYPART.
  2. Confirm the Creep is still outside the requested range. No tile change is required when the goal range is already satisfied.
  3. Compare a consecutive later-tick position for the same target and range.
  4. Identify the final movement call submitted for that Creep in the tick. A later movement call can replace the earlier intent.
  5. Only then inspect traffic, changing obstacles, and path reuse. One unchanged tick is diagnostic evidence, not a root-cause label.

This order prevents a common debugging loop where reusePath: 0 is applied globally before the code has proved that path reuse is the failing boundary.

When this guide does not apply

If the current result is not OK, follow that return code. If the original problem is an action returning ERR_NOT_IN_RANGE, use the action-range guide. This page does not claim a live traffic reproduction, a path-cache benchmark, or a measured number of stalled ticks.

Frequently asked questions

What does moveTo() returning OK prove?

It proves that this call accepted a movement request. Verify later-tick position progress separately.

Can a later movement call override it?

Yes. The later movement method submitted by the same Creep in the tick has priority.

Is ERR_TIRED part of an OK-but-not-moving case?

No. If the method returns ERR_TIRED, diagnose fatigue as a call failure instead.

What if the Creep has no MOVE part?

The current API includes ERR_NO_BODYPART (-12) for that state. It is not an accepted-OK stall.

Official documentation

SOURCE AND SCOPE

Review the source, evidence, or next system

This English guide keeps the technical scope and verification boundaries of its Chinese source. The verification record shows which checks were completed and which runtime observations are still pending.