BEGINNER · FIRST ROOM LOOP

Combine Your First Screeps Room Loop Without Hiding Failure States

CURATED PATH

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

Debugging / Observability

Recommended next steps

Add room-level exception isolationReliability upgradeDraw roles and targets in the RoomVisual debuggingVerify what actually happened last tickResult verificationDiagnosticsFull troubleshooting entry
Verification statusChinese source article: Reviewed in full — fixed names, one-room scope, simple role ownership, and documented collapse limits are preserved · Technical correction: Spawning, role execution, Energy phase, current-tick acceptance, later-tick outcomes, and emergency recovery are separated · JavaScript syntax: Passed by the editorial batch gate

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full — fixed names, one-room scope, simple role ownership, and documented collapse limits are preserved
Technical correction
Spawning, role execution, Energy phase, current-tick acceptance, later-tick outcomes, and emergency recovery are separated
JavaScript syntax
Passed by the editorial batch gate
Official documentation
Checked August 18, 2026 — game-loop timing, simultaneous action boundaries, Game.creeps, StructureSpawn.spawnCreep(), dryRun, and spawning state
Static code review
Passed — only one missing fixed-name Creep is requested, dryRun stays separate from the real result, spawning Creeps do not run role handlers, and current-tick snapshots are not presented as later outcomes
Intent boundary
This page integrates the beginner room loop; API-specific spawning, movement, harvesting, building, and recovery diagnosis stay in dedicated guides
Evidence level
Official-documentation review, Chinese-source review, and static code review only; no real-shard execution is claimed
Console test pending
Not run in this editorial pass
Screeps Console test
Pending — no real-shard Console transcript was collected for this revision
Live multi-tick verification pending
No live workforce replacement, accepted spawn request, next-tick Creep availability, competing intent, collapse, or recovery trace was collected for this revision
Last editorial review
August 18, 2026

Use this guide when

Use this page after the individual beginner lessons already work and you want one small module.exports.loop that combines them. The script keeps three fixed names, submits at most one Spawn request, lets existing Creeps continue when the Spawn lookup fails, and returns explicit statuses instead of pretending every accepted command already changed the room.

Use the focused spawnCreep() lesson, harvest lesson, or build and repair lesson when one of those actions is still unfamiliar.

Keep the lesson contract narrow

ContractThis script doesThis script does not do
Room scopeRuns three named Creeps in their current visible room.Coordinate multiple rooms or remote operations.
WorkforceRequests the first missing fixed name.Calculate population from room demand.
EnergyUses empty/full Store boundaries.Reserve Sources or optimize hauling.
TargetsUses nearest reachable eligible targets.Persist assignments or arbitrate traffic.
EvidenceReturns current-tick action and movement results.Claim later-tick movement or work completion.

Resolve the two inputs you must own

  • Replace Spawn1 with the exact owned Spawn name.
  • Keep the 200-Energy [WORK, CARRY, MOVE] body only while the room can recover it.
  • Back up the previous working code.
  • Do not reuse these fixed names for Creeps with different duties.
  • Deploy to one room first and observe several complete trips.

A small integrated room loop

State impact: this code may request Creeps, submit movement and work commands, and write one energyPhase string to each named Creep's Memory. It does not create construction sites, destroy Structures, or change room configuration.

const SPAWN_NAME = 'Spawn1';
const BODY = [WORK, CARRY, MOVE];

const ROLE_ORDER = [
  { name: 'Harvester1', run: runHarvester },
  { name: 'Upgrader1', run: runUpgrader },
  { name: 'Builder1', run: runBuilder }
];

function updateEnergyPhase(creep, memoryKey) {
  const used = creep.store.getUsedCapacity(RESOURCE_ENERGY);
  const free = creep.store.getFreeCapacity(RESOURCE_ENERGY);
  const capacity = creep.store.getCapacity(RESOURCE_ENERGY);

  if (
    !Number.isFinite(used)
    || !Number.isFinite(free)
    || !Number.isFinite(capacity)
    || capacity <= 0
  ) {
    return { status: 'invalid-energy-store', phase: null };
  }

  let phase = creep.memory[memoryKey];

  if (used === 0) {
    phase = 'acquire';
  } else if (free === 0) {
    phase = 'work';
  } else if (phase !== 'acquire' && phase !== 'work') {
    phase = 'acquire';
  }

  if (creep.memory[memoryKey] !== phase) {
    creep.memory[memoryKey] = phase;
  }

  return { status: 'ready', phase };
}

function moveAfterRangeFailure(creep, target, range) {
  const result = creep.moveTo(target, {
    range,
    reusePath: 5
  });

  return {
    status: result === OK
      ? 'movement-submitted'
      : result === ERR_TIRED
        ? 'movement-deferred-fatigue'
        : 'movement-rejected',
    result
  };
}

function acquireEnergy(creep) {
  const source = creep.pos.findClosestByPath(
    FIND_SOURCES_ACTIVE
  );

  if (!source) {
    return { status: 'active-source-not-found' };
  }

  const result = creep.harvest(source);

  if (result === ERR_NOT_IN_RANGE) {
    return {
      status: 'moving-to-source',
      actionResult: result,
      movement: moveAfterRangeFailure(creep, source, 1)
    };
  }

  return {
    status: result === OK
      ? 'harvest-submitted'
      : 'harvest-rejected',
    actionResult: result
  };
}

function runHarvester(creep) {
  const phase = updateEnergyPhase(creep, 'energyPhase');

  if (phase.status !== 'ready') {
    return phase;
  }

  if (phase.phase === 'acquire') {
    return acquireEnergy(creep);
  }

  const target = creep.pos.findClosestByPath(
    FIND_MY_STRUCTURES,
    {
      filter(structure) {
        return (
          structure.structureType === STRUCTURE_SPAWN
          || structure.structureType === STRUCTURE_EXTENSION
        ) && structure.store.getFreeCapacity(
          RESOURCE_ENERGY
        ) > 0;
      }
    }
  );

  if (!target) {
    return { status: 'delivery-target-not-found' };
  }

  const result = creep.transfer(target, RESOURCE_ENERGY);

  if (result === ERR_NOT_IN_RANGE) {
    return {
      status: 'moving-to-delivery',
      actionResult: result,
      movement: moveAfterRangeFailure(creep, target, 1)
    };
  }

  return {
    status: result === OK
      ? 'transfer-submitted'
      : 'transfer-rejected',
    actionResult: result
  };
}

function runUpgrader(creep) {
  const phase = updateEnergyPhase(creep, 'energyPhase');

  if (phase.status !== 'ready') {
    return phase;
  }

  if (phase.phase === 'acquire') {
    return acquireEnergy(creep);
  }

  const controller = creep.room.controller;

  if (!controller || controller.my !== true) {
    return { status: 'owned-controller-not-found' };
  }

  const result = creep.upgradeController(controller);

  if (result === ERR_NOT_IN_RANGE) {
    return {
      status: 'moving-to-controller',
      actionResult: result,
      movement: moveAfterRangeFailure(
        creep,
        controller,
        3
      )
    };
  }

  return {
    status: result === OK
      ? 'upgrade-submitted'
      : 'upgrade-rejected',
    actionResult: result
  };
}

function findBuilderTarget(creep) {
  const site = creep.pos.findClosestByPath(
    FIND_MY_CONSTRUCTION_SITES
  );

  if (site) {
    return { kind: 'build', target: site, range: 3 };
  }

  const structure = creep.pos.findClosestByPath(
    FIND_STRUCTURES,
    {
      filter(candidate) {
        const repairable = (
          candidate.my === true
          || candidate.structureType === STRUCTURE_ROAD
          || candidate.structureType === STRUCTURE_CONTAINER
        );

        return repairable
          && candidate.structureType !== STRUCTURE_WALL
          && candidate.structureType !== STRUCTURE_RAMPART
          && candidate.hits < candidate.hitsMax;
      }
    }
  );

  if (structure) {
    return {
      kind: 'repair',
      target: structure,
      range: 3
    };
  }

  const controller = creep.room.controller;
  return controller && controller.my === true
    ? { kind: 'upgrade', target: controller, range: 3 }
    : null;
}

function runBuilder(creep) {
  const phase = updateEnergyPhase(creep, 'energyPhase');

  if (phase.status !== 'ready') {
    return phase;
  }

  if (phase.phase === 'acquire') {
    return acquireEnergy(creep);
  }

  const task = findBuilderTarget(creep);

  if (!task) {
    return { status: 'builder-target-not-found' };
  }

  const result = task.kind === 'build'
    ? creep.build(task.target)
    : task.kind === 'repair'
      ? creep.repair(task.target)
      : creep.upgradeController(task.target);

  if (result === ERR_NOT_IN_RANGE) {
    return {
      status: 'moving-to-' + task.kind,
      actionResult: result,
      movement: moveAfterRangeFailure(
        creep,
        task.target,
        task.range
      )
    };
  }

  return {
    status: result === OK
      ? task.kind + '-submitted'
      : task.kind + '-rejected',
    actionResult: result
  };
}

function trySpawnFirstMissing(spawn) {
  if (!spawn) {
    return { status: 'spawn-not-found' };
  }

  if (spawn.spawning) {
    return { status: 'spawn-busy' };
  }

  const missing = ROLE_ORDER.find(
    role => !Game.creeps[role.name]
  );

  if (!missing) {
    return { status: 'workforce-present' };
  }

  const dryRunResult = spawn.spawnCreep(
    BODY,
    missing.name,
    { dryRun: true }
  );

  if (dryRunResult !== OK) {
    return {
      status: 'spawn-dry-run-rejected',
      creepName: missing.name,
      result: dryRunResult
    };
  }

  const result = spawn.spawnCreep(BODY, missing.name);

  return {
    status: result === OK
      ? 'spawn-submitted'
      : 'spawn-rejected',
    creepName: missing.name,
    result
  };
}

module.exports.loop = function () {
  const spawnResult = trySpawnFirstMissing(
    Game.spawns[SPAWN_NAME]
  );

  if (
    spawnResult.status.endsWith('rejected')
    || spawnResult.status === 'spawn-not-found'
  ) {
    console.log(JSON.stringify({
      type: 'beginner-spawn-status',
      tick: Game.time,
      ...spawnResult
    }));
  }

  for (const role of ROLE_ORDER) {
    const creep = Game.creeps[role.name];

    if (!creep || creep.spawning === true) {
      continue;
    }

    const result = role.run(creep);

    if (
      result.status.endsWith('rejected')
      || result.status.endsWith('not-found')
      || result.status === 'invalid-energy-store'
    ) {
      console.log(JSON.stringify({
        type: 'beginner-role-status',
        tick: Game.time,
        creepName: role.name,
        ...result
      }));
    }
  }
};

Choose one branch before submitting an action

Each role updates its Energy phase once, then either acquires Energy or performs work. The acquisition branch returns before the work branch. A partial Store preserves its previous phase, so one successful harvest does not immediately reverse the Creep toward a work target.

The shared phase helper is intentionally small. The dedicated working-state guide explains its initialization and hysteresis rules.

Submit only the first missing Spawn request

The Spawn helper checks fixed names in Harvester, Upgrader, Builder order, performs a dryRun, and submits one final request only when validation returns OK. A wrong Spawn name does not prevent existing Creeps from running because the role loop is separate from the Spawn result.

spawn-submitted means the request was accepted now. Confirm spawn.spawning and the later appearance of the named Creep before recording a completed spawn cycle.

Keep role priorities visible

CreepAcquire branchWork branch
Harvester1Nearest reachable active SourceNearest owned Spawn or Extension with free Energy capacity
Upgrader1Nearest reachable active SourceOwned room Controller
Builder1Nearest reachable active SourceOwned site, then selected repair target, then owned Controller

These priorities are teaching policy, not official Screeps rules. Walls and Ramparts are excluded from the repair fallback because an unbounded fortification target would hide the basic loop.

Read the returned status before changing more code

Status familyInterpretationNext check
*-not-foundA required visible object or eligible target was absent.Check the exact Spawn name, room ownership, target capacity, active Sources, and construction state.
movement-deferred-fatigueThe Creep was tired, so no new movement was accepted.Wait for fatigue to reach zero and compare later positions.
*-rejectedThe current API call returned a non-OK code.Inspect the captured action or movement result.
*-submittedThe command was accepted during this script execution.Verify the resulting room state on later ticks.
invalid-energy-storeThe Creep cannot use the two-phase Energy cycle.Inspect active CARRY parts and Store values.

Know the total-workforce-loss boundary

The body costs 200 Energy. If every Creep is gone and the Spawn cannot access 200 Energy, this teaching loop cannot manufacture its own supplier. That is a recovery design problem, not a reason to lower the body cost silently inside this article. Use the emergency harvester recovery guide when the room must bootstrap below its normal body budget.

Verify acceptance and outcomes on different ticks

  1. Record the current Game.time, Spawn result, role status, action result, and movement result.
  2. On later ticks, compare each Creep's roomName:x:y, Store, and target state.
  3. For spawning, confirm the named Creep appears after the Spawn finishes.
  4. For harvesting, transfer, build, repair, and upgrading, confirm the relevant resource or progress value changes.
  5. Keep live evidence Pending until those observations come from a real room.

Choose another guide when

Use the module-contract guide before splitting this lesson into files. Use the dynamic-body guide when available Energy should change a body. Use the dead-Creep cleanup guide after moving from fixed names to durable role data.

Treat this page as the beginner-room orchestrator

This guide is the integration layer for the beginner series. It should decide which existing role handler runs, whether one fixed-name Creep is missing, and what evidence to inspect afterward. It should not duplicate the full harvest(), transfer(), upgradeController(), build(), repair(), or spawnCreep() references.

For this small teaching loop, give each Creep one role owner per tick. That is a project policy for predictable debugging, not a Screeps rule that forbids every compatible action combination. The benefit is practical: one module owns the movement/work decision and one captured return code explains the branch that actually ran.

const BEGINNER_ROLES = [
  'Harvester1',
  'Upgrader1',
  'Builder1'
];

function chooseMissingBeginnerCreep() {
  return BEGINNER_ROLES.find(name =>
    !Game.creeps[name]
  ) || null;
}

function requestOneMissingCreep(spawn) {
  if (!spawn) {
    return { status: 'spawn-missing' };
  }

  if (spawn.spawning) {
    return {
      status: 'spawn-busy',
      spawningName: spawn.spawning.name
    };
  }

  const name = chooseMissingBeginnerCreep();
  if (!name) {
    return { status: 'workforce-present' };
  }

  const body = [WORK, CARRY, MOVE];
  const dryRun = spawn.spawnCreep(
    body,
    name,
    { dryRun: true }
  );

  if (dryRun !== OK) {
    return {
      status: 'spawn-preflight-rejected',
      name,
      result: dryRun
    };
  }

  const result = spawn.spawnCreep(body, name);

  return {
    status: result === OK
      ? 'spawn-request-accepted'
      : 'spawn-request-rejected',
    name,
    result
  };
}

dryRun validates a request without starting production. An OK dry run is not evidence that a new Creep exists or has finished spawning. Preserve the real spawnCreep() result, then inspect spawn.spawning and Game.creeps[name] on later ticks.

Separate accepted commands from later room state

The Game snapshot read by the script is the current tick. Submitted game actions are resolved after the script phase, so an OK return is not permission to rewrite the current snapshot into a claimed outcome. Record the request now and compare the relevant object state on a later tick.

function describeBeginnerRoomTick(spawn) {
  const creeps = BEGINNER_ROLES.map(name => {
    const creep = Game.creeps[name];

    if (!creep) {
      return { name, present: false };
    }

    return {
      name,
      present: true,
      spawning: creep.spawning === true,
      energy: creep.store.getUsedCapacity(
        RESOURCE_ENERGY
      ),
      position: [
        creep.pos.roomName,
        creep.pos.x,
        creep.pos.y
      ].join(':')
    };
  });

  return {
    tick: Game.time,
    spawningName: spawn?.spawning?.name || null,
    creeps
  };
}

module.exports.loop = function () {
  const spawn = Game.spawns.Spawn1;
  const spawnOutcome = requestOneMissingCreep(spawn);

  const harvester = Game.creeps.Harvester1;
  const upgrader = Game.creeps.Upgrader1;
  const builder = Game.creeps.Builder1;

  if (harvester && !harvester.spawning) {
    runHarvester(harvester);
  }
  if (upgrader && !upgrader.spawning) {
    runUpgrader(upgrader);
  }
  if (builder && !builder.spawning) {
    runBuilder(builder);
  }

  if (Game.time % 20 === 0) {
    console.log(JSON.stringify({
      type: 'beginner-room-tick',
      spawnOutcome,
      snapshot: describeBeginnerRoomTick(spawn)
    }));
  }
};

This is still intentionally a one-room teaching loop with fixed names and one basic body. It is not a prespawn scheduler, task system, multi-room framework, or guaranteed collapse-recovery system. If the room cannot recover its harvesting workforce, use the emergency recovery guide. For a rejected spawn request, use the spawnCreep() return-code guide. For module boundaries, continue to the require() guide.

Official documentation

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.