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
| Contract | This script does | This script does not do |
|---|---|---|
| Room scope | Runs three named Creeps in their current visible room. | Coordinate multiple rooms or remote operations. |
| Workforce | Requests the first missing fixed name. | Calculate population from room demand. |
| Energy | Uses empty/full Store boundaries. | Reserve Sources or optimize hauling. |
| Targets | Uses nearest reachable eligible targets. | Persist assignments or arbitrate traffic. |
| Evidence | Returns current-tick action and movement results. | Claim later-tick movement or work completion. |
Resolve the two inputs you must own
- Replace
Spawn1with 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
| Creep | Acquire branch | Work branch |
|---|---|---|
Harvester1 | Nearest reachable active Source | Nearest owned Spawn or Extension with free Energy capacity |
Upgrader1 | Nearest reachable active Source | Owned room Controller |
Builder1 | Nearest reachable active Source | Owned 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 family | Interpretation | Next check |
|---|---|---|
*-not-found | A required visible object or eligible target was absent. | Check the exact Spawn name, room ownership, target capacity, active Sources, and construction state. |
movement-deferred-fatigue | The Creep was tired, so no new movement was accepted. | Wait for fatigue to reach zero and compare later positions. |
*-rejected | The current API call returned a non-OK code. | Inspect the captured action or movement result. |
*-submitted | The command was accepted during this script execution. | Verify the resulting room state on later ticks. |
invalid-energy-store | The 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
- Record the current
Game.time, Spawn result, role status, action result, and movement result. - On later ticks, compare each Creep's
roomName:x:y, Store, and target state. - For spawning, confirm the named Creep appears after the Spawn finishes.
- For harvesting, transfer, build, repair, and upgrading, confirm the relevant resource or progress value changes.
- 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.