Quick answer
Recover the Creep's stored Source ID first. If the current visible Source still matches the task and has Energy, keep it. Otherwise find FIND_SOURCES_ACTIVE, build only reachable candidates, rank shorter path before lower declared assignment count and stable Source ID, store the chosen ID plus room context, call harvest(), move to range 1 only on ERR_NOT_IN_RANGE, and apply a documented policy when the Source becomes empty.
Range-nearest is not path-nearest
const nearestByRange = creep.pos.findClosestByRange(
FIND_SOURCES
);
const nearestByPath = creep.pos.findClosestByPath(
FIND_SOURCES
);
Range compares RoomPosition distance. Path selection considers terrain and obstacles and can return null when no route is available. Path-aware selection is usually more relevant when a Creep must walk to the Source, but it also costs more CPU.
Choose active or all Sources intentionally
const allSources = creep.room.find(FIND_SOURCES);
const activeSources = creep.room.find(
FIND_SOURCES_ACTIVE
);
A fixed miner normally keeps its assigned Source through regeneration. A flexible early-room harvester may select only Sources with Energy. The query constant expresses task policy, not a universal best practice.
Count declared Source assignments
function countAssignmentsBySource() {
const counts = {};
for (const creep of Object.values(Game.creeps)) {
const sourceId = creep.memory?.sourceId;
if (typeof sourceId !== 'string') {
continue;
}
counts[sourceId] = (counts[sourceId] || 0) + 1;
}
return counts;
}
The count represents Memory declarations only. It does not prove the Creep arrived, has free capacity, has active WORK, or is still following that task.
Rank reachable candidates deterministically
function selectSourceCandidate(candidates) {
return [...candidates]
.filter(candidate =>
typeof candidate.id === 'string'
&& Number.isFinite(candidate.energy)
&& candidate.energy > 0
&& Number.isFinite(candidate.pathLength)
&& candidate.pathLength >= 0
&& candidate.reachable === true
&& Number.isInteger(candidate.assignmentCount)
&& candidate.assignmentCount >= 0
)
.sort((left, right) =>
left.pathLength - right.pathLength
|| left.assignmentCount - right.assignmentCount
|| left.id.localeCompare(right.id)
)[0] || null;
}
Path length has priority here; assignment count breaks equal-path ties. This is not a full mining-slot scheduler and does not count walkable tiles around the Source.
function buildSourceCandidates(creep, sources) {
const assignments = countAssignmentsBySource();
const candidates = [];
for (const source of sources) {
const path = creep.pos.findPathTo(source, {
range: 1,
ignoreCreeps: true,
maxOps: 4000
});
const alreadyNear = creep.pos.isNearTo(source);
if (path.length === 0 && !alreadyNear) {
continue;
}
candidates.push({
source,
id: source.id,
energy: source.energy,
reachable: true,
pathLength: path.length,
assignmentCount: assignments[source.id] || 0
});
}
return candidates;
}
ignoreCreeps: true reduces temporary traffic influence on long-lived assignment, but actual movement can still be blocked.
Recover a stored Source first
function getStoredActiveSource(creep) {
const sourceId = creep.memory?.sourceId;
if (typeof sourceId !== 'string') {
return null;
}
const source = Game.getObjectById(sourceId);
if (!source) {
return null;
}
if (
!Number.isFinite(source.energy)
|| source.energy <= 0
) {
return null;
}
return source;
}
Keeping a valid ID avoids repeated selection and target churn. Do not automatically delete a remote ID merely because the object is not visible.
Complete dynamic Source selector
function chooseActiveSource(creep) {
const stored = getStoredActiveSource(creep);
if (stored) {
return {
source: stored,
selection: 'stored-active-id'
};
}
const candidates = buildSourceCandidates(
creep,
creep.room.find(FIND_SOURCES_ACTIVE)
);
const selected = selectSourceCandidate(candidates);
if (!selected) {
return {
source: null,
selection: 'active-source-not-found'
};
}
creep.memory.sourceId = selected.source.id;
creep.memory.sourceRoom = selected.source.pos.roomName;
creep.memory.sourceSelectedAt = Game.time;
return {
source: selected.source,
selection: 'reachable-candidate',
pathLength: selected.pathLength,
assignmentCount: selected.assignmentCount
};
}
function runDynamicHarvester(creep) {
if (!creep || creep.spawning === true) {
return { status: 'creep-unavailable' };
}
if (creep.getActiveBodyparts(WORK) <= 0) {
return { status: 'no-active-work' };
}
if (
creep.store.getFreeCapacity(RESOURCE_ENERGY)
<= 0
) {
return { status: 'creep-full' };
}
const choice = chooseActiveSource(creep);
if (!choice.source) {
return { status: choice.selection };
}
const result = creep.harvest(choice.source);
if (result === ERR_NOT_IN_RANGE) {
const moveResult = creep.moveTo(choice.source, {
range: 1,
reusePath: 10
});
return {
status: 'moving-to-source',
sourceId: choice.source.id,
selection: choice.selection,
result,
moveResult
};
}
if (result === ERR_NOT_ENOUGH_RESOURCES) {
delete creep.memory.sourceId;
}
return {
status: result === OK
? 'harvest-scheduled'
: 'harvest-rejected',
sourceId: choice.source.id,
selection: choice.selection,
result
};
}
module.exports.loop = function () {
const creep = Game.creeps.Harvester1;
const outcome = runDynamicHarvester(creep);
if (
outcome.status === 'harvest-rejected'
|| outcome.status === 'active-source-not-found'
|| Game.time % 100 === 0
) {
console.log(JSON.stringify({
type: 'source-selection-status',
creepName: creep?.name || null,
...outcome
}));
}
};
Choose an empty-Source policy explicitly
This example is dynamic and deletes the stored ID after ERR_NOT_ENOUGH_RESOURCES. A fixed miner should normally preserve the ID and wait for regeneration. Do not mix those policies accidentally.
function handleEmptySource(creep, mode) {
if (mode === 'dynamic') {
delete creep.memory.sourceId;
return 'target-cleared';
}
return 'target-preserved';
}
Preserve remote-room visibility context
function inspectStoredSourceRecord(creep) {
const sourceId = creep.memory?.sourceId;
const roomName = creep.memory?.sourceRoom;
const roomVisible = typeof roomName === 'string'
? Boolean(Game.rooms[roomName])
: null;
const source = typeof sourceId === 'string'
? Game.getObjectById(sourceId)
: null;
return {
sourceId: sourceId || null,
roomName: roomName || null,
roomVisible,
sourceFound: Boolean(source)
};
}
For a remote assignment, null can mean no current visibility. Keep room context so missing evidence is not mistaken for confirmed object deletion.
Measure pathfinding CPU instead of guessing
function measureSourceSelection(creep) {
const before = Game.cpu.getUsed();
const choice = chooseActiveSource(creep);
const after = Game.cpu.getUsed();
return {
choice,
cpuUsed: after - before
};
}
Pathfinding cost depends on room terrain, options, caches, candidate count and runtime state. Measure representative ticks before optimizing.
Preserve harvest and movement results
Saving only the selected Source ID is not enough. Keep the harvest() result and, when movement is requested, the moveTo() result as separate evidence.
Handle return codes
| Code | Typical meaning | Response |
|---|---|---|
OK | Harvest scheduled | Observe Store and Source later |
ERR_NOT_OWNER | Creep not yours | Current Creep |
ERR_BUSY | Creep spawning | Wait |
ERR_NOT_ENOUGH_RESOURCES | Source currently empty | Apply fixed or dynamic policy |
ERR_INVALID_TARGET | Object is not harvestable | Refresh current target |
ERR_FULL | Creep Store full | Switch task state |
ERR_NOT_IN_RANGE | Not adjacent | Move to range 1 |
ERR_NO_BODYPART | No active WORK | Body damage or replacement |
Debugging checklist
- Recover a stored Source ID first.
- Keep room context for remote targets.
- Choose all or active Sources intentionally.
- Build only reachable path candidates.
- Count assignments as declarations, not proof.
- Sort by path, assignment count and ID.
- Require free capacity and active WORK.
- Document empty-Source behavior.
- Save harvest and movement results.
- Measure real CPU before optimizing.
Scope and next steps
This guide does not allocate walkable mining slots, reserve containers, coordinate remote rooms without vision, prevent all traffic jams, cache paths across resets, or balance lifetime Energy throughput. Return to the English article library for harvesting, movement and runtime foundations.
Frequently asked questions
Why rank path before assignment count?
It preserves reachability and shorter travel as the primary rule. A room scheduler may weight load more heavily when mining slots are scarce.
Can findClosestByPath replace the candidate builder?
It can select a reachable target simply, but a custom candidate list is needed when assignment count and deterministic extra tie-breakers matter.
Why ignore Creeps while building long-lived candidates?
Temporary traffic should not necessarily rewrite a persistent mining assignment, but actual movement must still handle occupied tiles.