CONTROLLER · FIXED UPGRADER DIAGNOSTICS

Why a Fixed Screeps Upgrader Is Not Upgrading

Verification statusDocumentation: Official API and game-loop references checked · Syntax: Complete JavaScript example checked offline · Offline cases: 24 passed

VERIFICATION

Evidence and test status

Documentation
Official API and game-loop references checked
Syntax
Complete JavaScript example checked offline
Offline cases
24 passed
Live shard
Pending

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.

LayerRequired conditionFailure signal
AnchorWithin range 3 of Controller and range 1 of LinkRange or walkability reason
BodyEnergy capacity and active WORK; MOVE until anchoredNo capacity, WORK, or MOVE
Controller LinkOwned, active, same room, with Energycontroller-link-empty
ControllerOwned and not upgrade-blockedcontroller-upgrade-blocked
EvidenceLater position, Store delta, or exact eventAccepted 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

MethodImportant resultsPrimary diagnosis
moveTo()OK, ERR_TIRED, ERR_NO_PATH, ERR_NO_BODYPARTTraffic, fatigue, path, active MOVE
withdraw()OK, ERR_NOT_ENOUGH_RESOURCES, ERR_FULL, ERR_NOT_IN_RANGELink stock, Creep capacity, range 1
upgradeController()OK, ERR_NOT_ENOUGH_RESOURCES, ERR_INVALID_TARGET, ERR_NOT_IN_RANGE, ERR_NO_BODYPART, ERR_ACCESS_DENIEDEnergy, 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_CONTROLLER whose objectId is 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.

SOURCE AND SCOPE

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.