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
| Observation | Primary investigation |
|---|---|
moveTo() returns an error | Target, ownership, spawning, fatigue, cached path, or pathfinding result. |
moveTo() returns OK, but later positions are unchanged | Traffic, 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:yover 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
OKproves 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.