Use this guide when
Use this pattern when one Creep alternates between acquiring Energy and spending Energy, and you want the phase to persist across ticks without reversing after every small resource change. The working field is your own Memory contract; Screeps does not provide a built-in “working state.”
If the role is a fixed upgrader that refills from a nearby Link or Container while continuing to work, a full-to-empty cycle may be the wrong policy. This page is about a two-phase worker, not every possible role.
Define the phase contract
working = false
acquire Energy
working = true
spend Energy on the assigned task
Keep that meaning stable. Do not let one module interpret working: true as “has any Energy” while another interprets it as “currently executing a work action.” The field is a decision state, not proof that an action succeeded.
Use empty and full as hysteresis
Two boundaries create a stable cycle:
- When carried Energy reaches 0, switch to acquisition.
- When free Energy capacity reaches 0, switch to working.
- At partial Energy, keep the previous initialized phase.
0 / 50 -> acquire
50 / 50 -> work
30 / 50 -> keep previous phase
This is hysteresis: the transition into each phase happens at a different boundary. It avoids the task-flapping produced by creep.memory.working = !creep.memory.working or by switching to work as soon as usedEnergy > 0.
Make the decision pure
function decideWorkingState(input) {
const {
usedEnergy,
freeEnergy,
totalCapacity,
previousWorking
} = input;
if (
!Number.isFinite(usedEnergy)
|| !Number.isFinite(freeEnergy)
|| !Number.isFinite(totalCapacity)
|| usedEnergy < 0
|| freeEnergy < 0
|| totalCapacity <= 0
) {
return {
valid: false,
changed: false,
working: previousWorking === true,
reason: 'invalid-store-values'
};
}
if (usedEnergy === 0) {
return {
valid: true,
changed: previousWorking !== false,
working: false,
reason: 'energy-empty'
};
}
if (freeEnergy === 0) {
return {
valid: true,
changed: previousWorking !== true,
working: true,
reason: 'energy-full'
};
}
if (typeof previousWorking === 'boolean') {
return {
valid: true,
changed: false,
working: previousWorking,
reason: 'partial-keep-previous'
};
}
return {
valid: true,
changed: true,
working: false,
reason: 'partial-initialized'
};
}
The function has no Screeps side effects. You can test empty, full, partial, invalid, and first-run inputs without a live room.
Handle uninitialized partial Energy explicitly
A Creep can enter this code with partial Energy while creep.memory.working is undefined—for example after a schema change or after another system initialized the Creep differently. The API does not define what your custom boolean should become.
This guide chooses acquisition mode for an uninitialized partial Creep:
partial Energy + no previous boolean
-> working = false
-> reason = partial-initialized
That is a project policy, not an official Screeps rule. An emergency repair role could choose the opposite. The quality requirement is that the first-run rule is explicit and deterministic.
Write Memory only when the phase changes
Do not teach a persistent-state machine by rewriting the same boolean and “last checked tick” on every tick. Calculate first, then write the durable phase only when needed.
function updateWorkingState(creep) {
const decision = decideWorkingState({
usedEnergy:
creep.store.getUsedCapacity(RESOURCE_ENERGY),
freeEnergy:
creep.store.getFreeCapacity(RESOURCE_ENERGY),
totalCapacity:
creep.store.getCapacity(RESOURCE_ENERGY),
previousWorking: creep.memory.working
});
if (!decision.valid) {
return decision;
}
if (decision.changed) {
creep.memory.working = decision.working;
}
return decision;
}
decision.changed is useful for debugging and for limiting persistent writes. If you need transition history, keep it bounded and only append on real transitions rather than on every loop call.
Keep state transition and action result separate
The state decision chooses which branch should run. It does not prove that the branch's game action succeeded.
const decision = updateWorkingState(creep);
if (!decision.valid) {
console.log(JSON.stringify({
type: 'working-state-invalid',
creepName: creep.name,
reason: decision.reason
}));
return;
}
if (decision.working) {
const result = creep.upgradeController(controller);
// Handle result separately.
} else {
const result = creep.harvest(source);
// Handle result separately.
}
Submitting harvest() does not mean Store is already fuller in the current JavaScript execution, and submitting upgradeController() does not mean Store is already empty. Read the next tick's Store state and let the resource boundary drive the later transition.
Likewise, if the action returns ERR_NOT_IN_RANGE, movement is a separate operation. Keep the action result and movement result in different variables rather than changing the working phase because a movement call returned OK.
Do not force this cycle onto every role
- Fixed upgrader: may withdraw locally and continue upgrading without waiting to fill completely.
- Dedicated hauler: may use source/destination assignment rather than a generic “working” boolean.
- Emergency repairer: may deliberately begin spending partial Energy before reaching full capacity.
- Miner: often has a fixed position and a different resource-flow contract.
The empty/full pattern is a reusable state machine, not a universal role architecture.
Minimal diagnostic loop
State impact: this example may update only creep.memory.working when the phase changes. It does not submit a movement or work action.
module.exports.loop = function () {
const creep = Game.creeps.Worker1;
if (!creep || creep.spawning === true) return;
const decision = updateWorkingState(creep);
if (!decision.valid || decision.changed) {
console.log(JSON.stringify({
tick: Game.time,
creepName: creep.name,
working: decision.working,
changed: decision.changed,
reason: decision.reason,
usedEnergy:
creep.store.getUsedCapacity(RESOURCE_ENERGY),
freeEnergy:
creep.store.getFreeCapacity(RESOURCE_ENERGY)
}));
}
};
This produces a smaller, more meaningful log: invalid state and actual phase transitions, not one line every tick.
What static review does not prove
The boundary cases and Memory-write policy are reviewable offline. This revision does not contain a live multi-tick transcript of a Creep filling, switching, draining, and switching back. Console and live verification remain pending.