GETTING STARTED · BEGINNER LESSON 9 OF 12

How to Make a Screeps Creep Upgrade the Room Controller

CURATED PATH

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

Controller

Recommended next steps

Debug a fixed Upgrader that is not upgradingDebugging branchController downgrade risk and recoveryFailure modeBuild your first Extension after RCL 2Next lessonDiagnosticsContinue diagnosis
Verification statusChinese source: Read in full · Official documentation: Checked · API range and codes: Checked

VERIFICATION

Evidence and test status

Chinese source
Read in full
Official documentation
Checked
API range and codes
Checked
JavaScript syntax
Checked
State transitions
Reviewed
State impact
Writes one Memory flag and submits Creep actions
Screeps Console
Pending — no live output is claimed
Live multi-tick test
Pending — one full round trip
Official engine source
Checked current screeps/engine master 80977824199a596d174d392fd0cf8c458c21fcbd: Source harvest submission has no Store-capacity ERR_FULL preflight; the processor can drop overflow beyond Creep Store capacity
Source harvest capacity policy
Project policy — for the unboosted beginner bodies in these lessons, switch out of Source acquisition before another full active-WORK harvest batch would exceed remaining Store capacity
Offline state logic
Passed — empty/full hysteresis is preserved, the unboosted next-Source-batch guard is applied before harvest(), oversized Source batches stop instead of overflowing, and upgradeController() remains a separate range-3 action
Screeps Console test
Pending — no live near-full Source harvest trace is claimed
Live Source-overflow test
Pending — no live multi-WORK near-full Upgrader overflow comparison is claimed
Last editorial review
August 17, 2026
Publication status
Published
RUNTIME EVIDENCE

2 accepted runtime evidence records

Open verified archive →
EV-FAE0F4BC885D92F8A363ConsoleCreep.upgradeControllerAug 11, 2026
returned -9Game.time 82172807shard3 / W39N53

Real Screeps Console observation using fresh Game lookups in the same expression: har_W39N53_2609 had 5 active WORK, 50 Energy, and range 15 to the room Controller. Creep.upgradeController returned ERR_NOT_IN_RANGE (-9); immediately observed Energy remained 50 and Controller progress remained 627315.

EV-4BFAF12BE09D383FACA6ConsoleCreep.upgradeControllerAug 11, 2026
returned 0Game.time 82172766shard3 / W39N53

Real Screeps Console observation using fresh Game lookups within one expression: upg_W39N53_1503 was range 3 from the Controller with 95 Energy and 5 active WORK parts; Creep.upgradeController returned OK (0). This record proves the successful return path under those observed preconditions only; it does not claim that later Controller progress growth was caused solely by this Console invocation.

Build one complete Upgrader round trip

This lesson has one observable goal: make Upgrader1 harvest Energy, travel to the owned Room Controller, spend that Energy with upgradeController(), and return to harvesting when empty.

Empty → harvest. Full, or too little room for the next unboosted Source batch → upgrade. Otherwise keep the current trip.

The previous lesson explained that Upgrader is a player-defined responsibility. This lesson supplies the repeated behavior that makes the label useful.

Lesson boundary: the example uses one fixed Creep name and the first currently active Source. It does not optimize Source assignment, Controller throughput, Containers, Links, or automatic replacement.

Confirm the Creep and room can run the lesson

  • Upgrader1 exists and has finished spawning.
  • The Creep has active WORK, CARRY, and MOVE parts.
  • The current room has a Controller that belongs to you.
  • At least one Source is active when the Creep needs Energy.

Create the Creep with the previous spawnCreep() lesson when it does not exist. Keep the name in that spawn request and this main loop identical.

The learning body [WORK, CARRY, MOVE] is enough for this first cycle: WORK harvests and upgrades, CARRY provides the Energy Store, and MOVE allows travel.

Understand what upgradeController() actually does

creep.room.controller returns the Controller in the Creep's current room. Calling creep.upgradeController(controller) spends carried Energy through active WORK parts and adds upgrade progress when the target is valid.

The action works within range 3. That is different from harvest(), which requires the Creep to be adjacent to the Source.

ActionRequired rangeResource effect
harvest(source)1Adds Energy to available CARRY capacity.
upgradeController(controller)3Spends carried Energy on Controller progress.

This is why the movement code below requests range 1 for the Source but range 3 for the Controller.

Keep one two-state rule in Creep Memory

creep.memory.upgrading is a small persistent flag. It prevents the Creep from reversing direction after every individual unit of Energy changes.

Store conditionSaved stateNext responsibility
No carried EnergyfalseFind an active Source and harvest.
No free Energy capacitytrueMove within range 3 and upgrade.
Some Energy carried and remaining capacity is smaller than the next unboosted Source batchtrue by this tutorial policyUpgrade instead of intentionally overflowing the next harvest.
Other partly full statesKeep the existing valueFinish the current harvest or upgrade trip.

An undefined flag behaves like false in the branch below, so a new empty Upgrader begins by harvesting. Review Screeps Memory basics when you want to inspect how the value persists.

Project policy, not an API contract: the code below assumes the unboosted beginner body from this lesson and estimates one Source batch as active WORK × HARVEST_POWER. The current Source harvest() call has no Store-capacity ERR_FULL preflight; its processor can drop overflow. Switching a little early may leave a few capacity units unused, but it keeps this mobile Upgrader from deliberately submitting a batch that does not fit.

Run the complete Upgrader1 loop

State impact: this main loop writes creep.memory.upgrading, moves the Creep, harvests Energy, and submits Controller upgrade actions.

const CREEP_NAME = 'Upgrader1';

function reportEveryTwentyTicks(message) {
  if (Game.time % 20 === 0) {
    console.log(message);
  }
}

function moveWithinRange(creep, target, range, label) {
  const moveResult = creep.moveTo(target, {
    range: range,
    reusePath: 5
  });

  if (moveResult !== OK && moveResult !== ERR_TIRED) {
    reportEveryTwentyTicks(
      creep.name + ' moveTo(' + label + ') returned ' + moveResult + '.'
    );
  }
}

module.exports.loop = function () {
  const creep = Game.creeps[CREEP_NAME];

  if (!creep) {
    reportEveryTwentyTicks(
      CREEP_NAME + ' was not found. Create it or check the exact name.'
    );
    return;
  }

  if (creep.spawning) {
    return;
  }

  if (creep.getActiveBodyparts(WORK) === 0 ||
      creep.getActiveBodyparts(CARRY) === 0 ||
      creep.getActiveBodyparts(MOVE) === 0) {
    reportEveryTwentyTicks(
      CREEP_NAME + ' needs active WORK, CARRY, and MOVE parts.'
    );
    return;
  }

  const controller = creep.room.controller;

  if (!controller || !controller.my) {
    reportEveryTwentyTicks(
      CREEP_NAME + ' cannot find an owned Controller in ' +
      creep.room.name + '.'
    );
    return;
  }

  const activeWork = creep.getActiveBodyparts(WORK);
  const usedEnergy =
    creep.store.getUsedCapacity(RESOURCE_ENERGY) ?? 0;
  const freeEnergy =
    creep.store.getFreeCapacity(RESOURCE_ENERGY) ?? 0;
  const energyCapacity =
    creep.store.getCapacity(RESOURCE_ENERGY) ?? 0;
  const harvestBatch = activeWork * HARVEST_POWER;

  if (usedEnergy === 0) {
    creep.memory.upgrading = false;
  } else if (
    freeEnergy === 0
    || (
      !creep.memory.upgrading
      && freeEnergy > 0
      && freeEnergy < harvestBatch
    )
  ) {
    creep.memory.upgrading = true;
  }

  if (
    !creep.memory.upgrading
    && usedEnergy === 0
    && harvestBatch > energyCapacity
  ) {
    reportEveryTwentyTicks(
      CREEP_NAME +
      ' has a Source harvest batch larger than its Energy capacity.'
    );
    return;
  }

  if (creep.memory.upgrading) {
    const upgradeResult =
      creep.upgradeController(controller);

    if (upgradeResult === ERR_NOT_IN_RANGE) {
      moveWithinRange(creep, controller, 3, 'Controller');
    } else if (upgradeResult === ERR_NOT_ENOUGH_RESOURCES) {
      creep.memory.upgrading = false;
    } else if (upgradeResult !== OK) {
      reportEveryTwentyTicks(
        CREEP_NAME + ' upgradeController() returned ' +
        upgradeResult + '.'
      );
    }

    return;
  }

  const activeSources =
    creep.room.find(FIND_SOURCES_ACTIVE);
  const source = activeSources[0];

  if (!source) {
    reportEveryTwentyTicks(
      CREEP_NAME + ' is waiting for an active Source.'
    );
    return;
  }

  const harvestResult = creep.harvest(source);

  if (harvestResult === ERR_NOT_IN_RANGE) {
    moveWithinRange(creep, source, 1, 'Source');
  } else if (harvestResult !== OK &&
             harvestResult !== ERR_NOT_ENOUGH_RESOURCES) {
    reportEveryTwentyTicks(
      CREEP_NAME + ' harvest() returned ' +
      harvestResult + '.'
    );
  }
};

Follow the loop across repeated ticks

  1. The main loop finds Upgrader1 and validates its body and owned Controller.
  2. An empty Store sets upgrading to false.
  3. The Creep selects a currently active Source.
  4. harvest() returns ERR_NOT_IN_RANGE until the Creep reaches range 1, so moveTo() advances it across later ticks.
  5. A full Store sets upgrading to true. Under this tutorial's unboosted no-overflow policy, a partly filled Store also switches when the next full Source batch would not fit.
  6. upgradeController() returns ERR_NOT_IN_RANGE until the Creep is within range 3.
  7. Successful upgrade actions consume Energy across later ticks.
  8. When the Store reaches zero, the next tick switches back to harvesting.

The code does not attempt to harvest and upgrade in the same branch. Each tick reads current state and submits the action appropriate for that state.

Read the important action results

ResultMeaning in this lessonResponse
OKThe action was scheduled.Inspect the Creep and Controller on later ticks.
ERR_NOT_IN_RANGEThe target is outside the action range.Move to range 1 for Source or range 3 for Controller.
ERR_NOT_ENOUGH_RESOURCESThe Creep has no Energy for upgrading, or the Source has no available Energy for harvesting.Change state or wait for an active Source.
ERR_NO_BODYPARTNo active WORK part can perform the action.Inspect body damage or the original body design.
ERR_INVALID_TARGETThe target cannot currently accept that action.Inspect the target and Controller state.
ERR_NOT_OWNERThe Controller is not owned by you.Stop this beginner loop in that room.

Use the ERR_NOT_IN_RANGE guide when movement succeeds but the action never becomes valid.

Verify one full Source-to-Controller cycle

Do not treat a single OK return value as proof of the whole loop. Watch several later ticks and confirm this sequence:

  1. Upgrader1 approaches an active Source while empty.
  2. Its Energy Store increases.
  3. memory.upgrading becomes true when the Store is full or when this tutorial's conservative unboosted Source-batch threshold is reached.
  4. The Creep stops within range 3 of the owned Controller.
  5. Its carried Energy decreases while Controller progress increases.
  6. The flag becomes false after the Store reaches zero.
  7. The Creep returns to an active Source.

Completing this full round trip is the acceptance test for the lesson. The page does not claim that this sequence has been run in a live Screeps room.

Diagnose the first failures without adding more systems

SymptomFirst evidence to inspect
The Creep never leaves the Source.Check Store capacity and whether memory.upgrading becomes true when full.
The Creep reaches the Controller but does not upgrade.Check carried Energy, active WORK, ownership, range, and upgradeResult.
The Creep changes direction too early.Confirm the partially full state preserves the previous flag.
The Creep waits beside an empty Source.Confirm the code uses FIND_SOURCES_ACTIVE, not a permanently fixed depleted Source.
The Console repeats the same message every tick.Keep throttled diagnostics while fixing the underlying missing object or body part.

Do not add Builder, repair, population control, or Link logic until this one cycle is observable.

Finish the lesson, then build the first Extension

You have completed Lesson 9 when you can explain and observe all of the following:

  • upgradeController() spends carried Energy and works within range 3;
  • harvest() requires range 1;
  • creep.memory.upgrading preserves one responsibility across ticks;
  • empty, full, and the tutorial's conservative next-Source-batch boundary switch the state without treating ERR_FULL as a Source-harvest signal;
  • the code validates the Creep, active body parts, active Source, and owned Controller;
  • one full harvest-upgrade-harvest round trip is visible across later ticks.

Continue to build the first Extension after reaching RCL 2 →

Return to the English beginner roadmap to review all twelve lessons.

Official sources

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.