Separate the Upgrader from its supply chain
A fixed Upgrader is the final consumer in a longer Energy chain: Source or Storage, hauler or source Link, Controller Link, Upgrader Store, then upgradeController(). A stationary Creep is not automatically broken. It may be waiting for Link Energy, missing an active WORK part, standing on the wrong tile, or facing a blocked Controller.
This guide is different from the beginner Source-to-Controller loop and the generic Link transfer guide. It diagnoses the combined fixed-position system.
| Layer | Required condition | Failure signal |
|---|---|---|
| Anchor | Within range 3 of Controller and range 1 of Link | Range or walkability reason |
| Body | Energy capacity and active WORK; MOVE until anchored | No capacity, WORK, or MOVE |
| Controller Link | Owned, active, same room, with Energy | controller-link-empty |
| Controller | Owned and not upgrade-blocked | controller-upgrade-blocked |
| Evidence | Later position, Store delta, or exact event | Accepted command without observed result |
Run a 30-second read-only triage
Before changing the anchor or replacing your loop, inspect the objects that already exist. Replace the two placeholder values, then run this in the Console. It does not issue movement, withdraw, transfer, or upgrade commands.
const UPGRADER_NAME = 'Upgrader1';
const CONTROLLER_LINK_ID = 'CONTROLLER_LINK_ID';
const creep = Game.creeps[UPGRADER_NAME];
const controller = creep?.room.controller ?? null;
const link = Game.getObjectById(CONTROLLER_LINK_ID);
console.log(JSON.stringify({
creepFound: Boolean(creep),
spawning: creep?.spawning ?? null,
activeWork: creep?.getActiveBodyparts(WORK) ?? null,
energy: creep?.store.getUsedCapacity(RESOURCE_ENERGY) ?? null,
controllerOwned: controller?.my ?? null,
controllerRange: creep && controller
? creep.pos.getRangeTo(controller)
: null,
upgradeBlocked: controller?.upgradeBlocked ?? null,
linkFound: Boolean(link),
linkOwned: link?.my ?? null,
linkType: link?.structureType ?? null,
linkActive: typeof link?.isActive === 'function'
? link.isActive()
: null,
linkEnergy: link?.store?.getUsedCapacity(RESOURCE_ENERGY) ?? null,
linkRange: creep && link
? creep.pos.getRangeTo(link)
: null
}));
| Probe result | Most likely branch | Next step |
|---|---|---|
creepFound: false or spawning: true | The worker is missing or not ready. | Fix the Creep name or wait for spawning to finish. |
activeWork: 0 | The Creep cannot perform Controller work. | Inspect its body and damage before changing logistics. |
controllerRange > 3 | The current position cannot upgrade the Controller. | Validate the anchor geometry below. |
linkRange > 1 | The current position cannot withdraw from the Controller Link. | Validate the same anchor against the Link. |
energy: 0 and linkEnergy: 0 | The Upgrader is starved upstream. | Repair Link supply instead of changing upgradeController(). |
upgradeBlocked > 0 | The Controller is temporarily blocked from upgrading. | Wait for the block to expire; code changes will not bypass it. |
| Ranges, body, ownership, Link Energy, and Controller state all look valid | The next useful signal is the exact action return code. | Use the checklist below before replacing the whole implementation. |
If the Creep is not currently on its intended fixed tile, treat the two range values as current-position diagnostics only. The next section checks the configured anchor itself.
Validate one exact anchor tile
The chosen tile must satisfy both documented action ranges:
anchor.getRangeTo(controller) <= 3;
anchor.getRangeTo(controllerLink) <= 1;
The tile must also be walkable. Moving only to range: 3 from the Controller does not guarantee adjacency to the Controller Link. Store the Link ID and anchor coordinates explicitly instead of depending on a structure array position.
Memory.fixedUpgraders ??= {};
Memory.fixedUpgraders.W1N1 = {
enabled: true,
creepName: 'Upgrader1',
controllerLinkId: 'CONTROLLER_LINK_ID',
anchor: { x: 24, y: 25 },
pending: null,
history: []
};
Use one diagnosable action per tick
not anchored -> move only
anchored and empty -> withdraw only
anchored with Energy -> upgrade only
Screeps commands are queued against the tick's starting state and settle later. Separating the states is not the only valid architecture, but it makes return codes and next-tick observations easier to interpret.
If the Controller Link is empty, keep the Upgrader fixed and repair upstream logistics. Use the hauling throughput planner and the Link guide instead of silently turning the Upgrader into a Storage hauler.
Return-code checklist
| Method | Important results | Primary diagnosis |
|---|---|---|
moveTo() | OK, ERR_TIRED, ERR_NO_PATH, ERR_NO_BODYPART | Traffic, fatigue, path, active MOVE |
withdraw() | OK, ERR_NOT_ENOUGH_RESOURCES, ERR_FULL, ERR_NOT_IN_RANGE | Link stock, Creep capacity, range 1 |
upgradeController() | OK, ERR_NOT_ENOUGH_RESOURCES, ERR_INVALID_TARGET, ERR_NOT_IN_RANGE, ERR_NO_BODYPART, ERR_ACCESS_DENIED | Energy, Controller state, range 3, active WORK |
Preserve the actual result. OK means the command was accepted, not that the same code block can already read the settled state. If the short probe already isolates the problem, stop there; the complete implementation below is a hardened reference, not the first diagnostic step.
Complete fixed-Upgrader example
The implementation below validates object identity, ownership, anchor geometry, walkability, active body parts, Link state, upgradeBlocked, and one later observation. History is bounded to 20 records.
const HISTORY_LIMIT = 20;
function evaluateFixedUpgraderState(input) {
const {
enabled, creepExists, creepOwned, spawning,
activeWork, energyCapacity, energy, activeMove,
atAnchor, anchorWalkable, anchorControllerRange,
anchorLinkRange, controllerExists, controllerOwned,
upgradeBlocked, linkExists, linkOwned, linkActive,
linkEnergy
} = input;
if (enabled !== true) return { action: 'none', reason: 'config-disabled' };
if (!creepExists || !creepOwned) return { action: 'none', reason: 'owned-creep-missing' };
if (spawning) return { action: 'none', reason: 'creep-spawning' };
if (!controllerExists || !controllerOwned) return { action: 'none', reason: 'owned-controller-missing' };
if (!linkExists || !linkOwned) return { action: 'none', reason: 'owned-controller-link-missing' };
if (!linkActive) return { action: 'none', reason: 'controller-link-inactive' };
if (!anchorWalkable) return { action: 'none', reason: 'anchor-not-walkable' };
if (anchorControllerRange > 3) return { action: 'none', reason: 'anchor-outside-controller-range' };
if (anchorLinkRange > 1) return { action: 'none', reason: 'anchor-outside-link-range' };
if (energyCapacity <= 0) return { action: 'none', reason: 'no-energy-capacity' };
if (activeWork <= 0) return { action: 'none', reason: 'no-active-work' };
if (!atAnchor) {
return activeMove > 0
? { action: 'move', reason: 'move-to-anchor' }
: { action: 'none', reason: 'no-active-move' };
}
if (energy <= 0) {
return linkEnergy > 0
? { action: 'withdraw', reason: 'take-controller-link-energy' }
: { action: 'none', reason: 'controller-link-empty' };
}
if (upgradeBlocked > 0) return { action: 'none', reason: 'controller-upgrade-blocked' };
return { action: 'upgrade', reason: 'upgrade-ready' };
}
function getState(roomName) {
Memory.fixedUpgraders ??= {};
Memory.fixedUpgraders[roomName] ??= {
enabled: false,
creepName: null,
controllerLinkId: null,
anchor: null,
pending: null,
history: []
};
return Memory.fixedUpgraders[roomName];
}
function getOwnedLink(id, roomName) {
if (typeof id !== 'string') return null;
const link = Game.getObjectById(id);
return link
&& link.structureType === STRUCTURE_LINK
&& link.my === true
&& link.room.name === roomName
? link
: null;
}
function getAnchor(room, value) {
if (
!value
|| !Number.isInteger(value.x)
|| !Number.isInteger(value.y)
|| value.x < 0 || value.x > 49
|| value.y < 0 || value.y > 49
) return null;
return new RoomPosition(value.x, value.y, room.name);
}
function isWalkable(room, position) {
if (!position) return false;
if (room.getTerrain().get(position.x, position.y) === TERRAIN_MASK_WALL) return false;
const blockedStructure = room.lookForAt(
LOOK_STRUCTURES,
position.x,
position.y
).some(item => OBSTACLE_OBJECT_TYPES.includes(item.structureType));
const blockedSite = room.lookForAt(
LOOK_CONSTRUCTION_SITES,
position.x,
position.y
).some(item => OBSTACLE_OBJECT_TYPES.includes(item.structureType));
return !blockedStructure && !blockedSite;
}
function snapshot(creep, controller, link, anchor) {
return {
creepEnergy: creep?.store.getUsedCapacity(RESOURCE_ENERGY) ?? null,
linkEnergy: link?.store.getUsedCapacity(RESOURCE_ENERGY) ?? null,
controllerProgress: Number.isFinite(controller?.progress)
? controller.progress
: null,
ticksToDowngrade: controller?.ticksToDowngrade ?? null,
atAnchor: Boolean(creep && anchor && creep.pos.isEqualTo(anchor))
};
}
function allFinite(values) {
return values.every(value => Number.isFinite(value));
}
function verifyPrevious(room, state, creep, controller, link, anchor) {
const pending = state.pending;
if (!pending || pending.tick >= Game.time) return null;
const after = snapshot(creep, controller, link, anchor);
const exactUpgradeEvent = pending.action === 'upgrade'
&& room.getEventLog().some(event =>
event.event === EVENT_UPGRADE_CONTROLLER
&& event.objectId === pending.creepId
);
let status = 'not-observed';
if (pending.action === 'move' && after.atAnchor) {
status = 'anchor-arrival-observed';
}
if (pending.action === 'withdraw') {
const canMeasureWithdraw = allFinite([
after.creepEnergy,
pending.before.creepEnergy,
pending.before.linkEnergy,
after.linkEnergy
]);
if (!canMeasureWithdraw) {
status = 'withdraw-evidence-unavailable';
} else {
const gain = after.creepEnergy - pending.before.creepEnergy;
const loss = pending.before.linkEnergy - after.linkEnergy;
status = gain > 0 && loss > 0
? 'withdraw-deltas-observed'
: gain > 0 || loss > 0
? 'withdraw-partial-observation'
: 'withdraw-not-observed';
}
}
if (pending.action === 'upgrade') {
const canMeasureSpent = allFinite([
after.creepEnergy,
pending.before.creepEnergy
]);
const canMeasureProgress = allFinite([
after.controllerProgress,
pending.before.controllerProgress
]);
const canMeasureDowngrade = allFinite([
after.ticksToDowngrade,
pending.before.ticksToDowngrade
]);
if (exactUpgradeEvent) {
status = 'upgrade-event-observed';
} else if (!canMeasureSpent || (!canMeasureProgress && !canMeasureDowngrade)) {
status = 'upgrade-evidence-unavailable';
} else {
const spent = pending.before.creepEnergy - after.creepEnergy;
const progress = canMeasureProgress
&& after.controllerProgress > pending.before.controllerProgress;
const downgrade = canMeasureDowngrade
&& after.ticksToDowngrade > pending.before.ticksToDowngrade;
status = spent > 0 && (progress || downgrade)
? 'upgrade-deltas-observed'
: spent > 0 || progress || downgrade
? 'upgrade-partial-observation'
: 'upgrade-not-observed';
}
}
const record = {
...pending,
verifiedAt: Game.time,
after: { ...after, exactUpgradeEvent },
status
};
state.history ??= [];
state.history.push(record);
state.history = state.history.slice(-HISTORY_LIMIT);
state.pending = null;
state.lastVerification = record;
return record;
}
function runFixedUpgrader(roomName) {
const state = getState(roomName);
const room = Game.rooms[roomName];
if (!room) return { status: 'room-not-visible' };
const controller = room.controller || null;
const creep = typeof state.creepName === 'string'
? Game.creeps[state.creepName] || null
: null;
const link = getOwnedLink(state.controllerLinkId, roomName);
const anchor = getAnchor(room, state.anchor);
const verification = verifyPrevious(
room, state, creep, controller, link, anchor
);
const decision = evaluateFixedUpgraderState({
enabled: state.enabled,
creepExists: Boolean(creep),
creepOwned: creep?.my === true,
spawning: creep?.spawning === true,
activeWork: creep?.getActiveBodyparts(WORK) ?? 0,
energyCapacity: creep?.store.getCapacity(RESOURCE_ENERGY) ?? 0,
energy: creep?.store.getUsedCapacity(RESOURCE_ENERGY) ?? 0,
activeMove: creep?.getActiveBodyparts(MOVE) ?? 0,
atAnchor: Boolean(creep && anchor && creep.pos.isEqualTo(anchor)),
anchorWalkable: isWalkable(room, anchor),
anchorControllerRange: controller && anchor
? anchor.getRangeTo(controller)
: Infinity,
anchorLinkRange: link && anchor
? anchor.getRangeTo(link)
: Infinity,
controllerExists: Boolean(controller),
controllerOwned: controller?.my === true,
upgradeBlocked: controller?.upgradeBlocked ?? 0,
linkExists: Boolean(link),
linkOwned: link?.my === true,
linkActive: link?.isActive() === true,
linkEnergy: link?.store.getUsedCapacity(RESOURCE_ENERGY) ?? 0
});
state.lastReason = decision.reason;
state.lastDecisionAt = Game.time;
if (decision.action === 'none') {
return { status: decision.reason, verification };
}
const before = snapshot(creep, controller, link, anchor);
let result = ERR_INVALID_ARGS;
if (decision.action === 'move') {
result = creep.moveTo(anchor, { range: 0, reusePath: 10 });
} else if (decision.action === 'withdraw') {
result = creep.withdraw(link, RESOURCE_ENERGY);
} else if (decision.action === 'upgrade') {
result = creep.upgradeController(controller);
}
state.lastAction = decision.action;
state.lastResult = result;
state.lastResultAt = Game.time;
if (result === OK) {
state.pending = {
tick: Game.time,
action: decision.action,
creepId: creep.id,
creepName: creep.name,
controllerId: controller.id,
linkId: link.id,
before
};
}
return {
status: result === OK ? 'command-accepted' : 'command-rejected',
action: decision.action,
reason: decision.reason,
result,
before,
verification
};
}
module.exports.loop = function () {
const outcome = runFixedUpgrader('W1N1');
if (outcome.status !== 'config-disabled' || outcome.verification) {
console.log(JSON.stringify({
type: 'fixed-upgrader',
tick: Game.time,
roomName: 'W1N1',
...outcome
}));
}
};
Verify the later result
- Move: the Creep is on the exact anchor.
- Withdraw: Creep Energy increased and Controller Link Energy decreased.
- Upgrade: prefer an exact
EVENT_UPGRADE_CONTROLLERwhoseobjectIdis the pending Creep ID.
If a Creep, Link, or Controller snapshot is missing, the code records withdraw-evidence-unavailable or upgrade-evidence-unavailable; it never lets null participate in arithmetic. When exact events are unavailable, Energy, Controller progress, and downgrade-timer changes are only bounded supporting observations. Concurrent Upgraders can confound pure deltas.
Evidence boundaries
Twenty-four offline cases passed, including missing objects, ownership, spawning, walkability, both action ranges, Energy capacity, active WORK and MOVE, empty Link, blocked Controller, move, withdraw, upgrade, missing Creep, Link and Controller snapshots, and later-observation classifications. The complete example passed a JavaScript syntax check.
These checks do not prove live shard traffic, anchor contention, competing Link senders, every Boost and Power Creep combination, RCL8 throughput, or that production IDs and coordinates are correct. Console and official-shard evidence remain pending.
Continue with Controller downgrade recovery, Storage Energy policy, or Room event logs.