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 |
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.
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
}));
}
};
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.
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.