Use this guide when
A flexible harvester has several visible Sources and should keep a valid assignment, switch only under a documented rule, and reject candidate searches that stop before reaching range 1.
This page answers a target-selection question. It does not diagnose every CostMatrix or cross-room routing failure.
Choose fixed or dynamic assignment
A fixed miner and a dynamic early-room harvester should not share one invalidation rule.
- Fixed: preserve the assigned Source through regeneration. Empty Energy is temporary state, not a new identity.
- Dynamic: keep the stored Source while it remains visible, active, and valid; otherwise select another complete-path candidate.
function getSourceAssignmentMode(creep) {
return creep.memory?.sourceMode === 'fixed'
? 'fixed'
: 'dynamic';
}
The example below intentionally requires Energy capacity because it models a dynamic harvester that carries Energy. A stationary drop-mining design has a different body and delivery contract.
Interpret a stored ID before clearing it
function inspectStoredSource(creep) {
const sourceId = creep.memory?.sourceId;
const roomName = creep.memory?.sourceRoom;
if (typeof sourceId !== 'string') {
return { status: 'source-id-not-set', source: null };
}
const source = Game.getObjectById(sourceId);
if (source) {
if (
source.pos.roomName !== creep.room.name
|| (
typeof roomName === 'string'
&& source.pos.roomName !== roomName
)
) {
return { status: 'stored-source-wrong-room', source: null };
}
return { status: 'stored-source-visible', source };
}
if (
typeof roomName === 'string'
&& !Game.rooms[roomName]
) {
return {
status: 'stored-source-vision-unavailable',
source: null
};
}
return {
status: 'stored-source-missing-visible-room',
source: null
};
}
A null object lookup is not enough to prove deletion when the saved room is not visible. For this same-room selector, a remote record is not immediately reused, but the status remains explicit so another subsystem can decide whether to wait, scout, or cancel.
function keepStoredSource(creep, inspected) {
if (!inspected.source) {
return false;
}
const mode = getSourceAssignmentMode(creep);
if (mode === 'fixed') {
return true;
}
return inspected.source.energy > 0;
}
Reject incomplete path results
PathFinder.search() can return a partial path with incomplete: true. Path length alone is therefore not proof that the Creep can reach range 1.
function buildStaticSourceMatrix(room) {
const matrix = new PathFinder.CostMatrix();
for (const structure of room.find(FIND_STRUCTURES)) {
if (structure.structureType === STRUCTURE_ROAD) {
matrix.set(structure.pos.x, structure.pos.y, 1);
continue;
}
if (structure.structureType === STRUCTURE_CONTAINER) {
continue;
}
if (structure.structureType === STRUCTURE_RAMPART) {
if (structure.my === true || structure.isPublic === true) {
continue;
}
matrix.set(structure.pos.x, structure.pos.y, 255);
continue;
}
if (OBSTACLE_OBJECT_TYPES.includes(
structure.structureType
)) {
matrix.set(structure.pos.x, structure.pos.y, 255);
}
}
return matrix;
}
function searchCompleteSourcePath(creep, source) {
const matrix = buildStaticSourceMatrix(creep.room);
const result = PathFinder.search(
creep.pos,
{ pos: source.pos, range: 1 },
{
maxRooms: 1,
maxOps: 4000,
plainCost: 2,
swampCost: 10,
roomCallback(roomName) {
return roomName === creep.room.name
? matrix
: false;
}
}
);
return {
complete: result.incomplete !== true,
pathLength: result.path.length,
ops: result.ops,
cost: result.cost
};
}
The matrix ignores Creeps on purpose so temporary traffic does not redefine a long-lived Source assignment. Actual movement can still be delayed by traffic and must be observed separately.
Rank complete candidates
Count declared assignments as planning input, not proof that a Creep is currently harvesting. Exclude the Creep being reassigned so its stale target does not penalize one candidate.
function countOtherSourceAssignments(currentCreepName) {
const counts = {};
for (const creep of Object.values(Game.creeps)) {
if (creep.name === currentCreepName) {
continue;
}
const sourceId = creep.memory?.sourceId;
if (typeof sourceId !== 'string') {
continue;
}
counts[sourceId] = (counts[sourceId] || 0) + 1;
}
return counts;
}
function buildCompleteSourceCandidates(creep) {
const assignments = countOtherSourceAssignments(
creep.name
);
const candidates = [];
for (const source of creep.room.find(
FIND_SOURCES_ACTIVE
)) {
const path = searchCompleteSourcePath(creep, source);
if (!path.complete) {
continue;
}
candidates.push({
source,
sourceId: source.id,
pathLength: path.pathLength,
pathOps: path.ops,
pathCost: path.cost,
assignmentCount: assignments[source.id] || 0
});
}
return candidates;
}
function selectSourceCandidate(candidates) {
return [...candidates].sort((left, right) =>
left.pathLength - right.pathLength
|| left.assignmentCount - right.assignmentCount
|| left.pathCost - right.pathCost
|| left.sourceId.localeCompare(right.sourceId)
)[0] || null;
}
Path length is the primary rule here. Assignment count and stable ID make ties deterministic. This is not a mining-slot allocator; it does not prove that enough walkable harvesting positions exist around a Source.
Act without reselecting every tick
function chooseSource(creep) {
const inspected = inspectStoredSource(creep);
if (keepStoredSource(creep, inspected)) {
return {
status: 'stored-source-kept',
source: inspected.source
};
}
if (
inspected.status === 'stored-source-vision-unavailable'
) {
return { status: inspected.status, source: null };
}
const selected = selectSourceCandidate(
buildCompleteSourceCandidates(creep)
);
if (!selected) {
return {
status: 'complete-active-source-not-found',
source: null
};
}
creep.memory.sourceId = selected.sourceId;
creep.memory.sourceRoom = selected.source.pos.roomName;
creep.memory.sourceSelectedAt = Game.time;
return {
status: 'complete-source-selected',
source: selected.source,
pathLength: selected.pathLength,
assignmentCount: selected.assignmentCount
};
}
function submitSelectedHarvest(creep, state) {
if (!creep || creep.spawning === true) {
return { status: 'creep-unavailable' };
}
if (creep.getActiveBodyparts(WORK) <= 0) {
return { status: 'no-active-work' };
}
const capacity = creep.store.getCapacity(
RESOURCE_ENERGY
);
if (!Number.isFinite(capacity) || capacity <= 0) {
return { status: 'no-energy-capacity-for-this-role' };
}
if (
creep.store.getFreeCapacity(RESOURCE_ENERGY) <= 0
) {
return { status: 'creep-full' };
}
const choice = chooseSource(creep);
if (!choice.source) {
return { status: choice.status };
}
const result = creep.harvest(choice.source);
if (result === ERR_NOT_IN_RANGE) {
const beforePosition = [
creep.pos.roomName,
creep.pos.x,
creep.pos.y
].join(':');
const moveResult = creep.moveTo(choice.source, {
range: 1,
reusePath: 10
});
state.lastMovement = {
submittedAt: Game.time,
creepId: creep.id,
sourceId: choice.source.id,
beforePosition,
moveResult
};
return {
status: moveResult === OK
? 'movement-submitted'
: moveResult === ERR_TIRED
? 'movement-deferred-fatigue'
: 'movement-rejected',
sourceId: choice.source.id,
harvestResult: result,
moveResult
};
}
if (result !== OK) {
if (
result === ERR_NOT_ENOUGH_RESOURCES
&& getSourceAssignmentMode(creep) === 'dynamic'
) {
delete creep.memory.sourceId;
delete creep.memory.sourceRoom;
}
return {
status: 'harvest-rejected',
sourceId: choice.source.id,
result
};
}
state.pendingHarvest = {
requestId: [
creep.name,
Game.time,
choice.source.id
].join(':'),
submittedAt: Game.time,
creepId: creep.id,
sourceId: choice.source.id,
beforeEnergy: creep.store.getUsedCapacity(
RESOURCE_ENERGY
)
};
return {
status: 'harvest-accepted',
sourceId: choice.source.id,
result
};
}
The selected ID is reused until the chosen policy invalidates it. Pathfinding runs during assignment, not on every successful harvest tick.
Verify the selected harvest event
function verifySelectedHarvest(room, state) {
const pending = state.pendingHarvest;
if (!pending) {
return { status: 'nothing-pending' };
}
if (Game.time <= pending.submittedAt) {
return { status: 'waiting-for-next-tick' };
}
if (Game.time !== pending.submittedAt + 1) {
delete state.pendingHarvest;
return {
status: 'verification-window-missed',
requestId: pending.requestId
};
}
const matches = room.getEventLog().filter(event =>
event.event === EVENT_HARVEST
&& event.objectId === pending.creepId
&& event.data?.targetId === pending.sourceId
);
const creep = Game.getObjectById(pending.creepId);
const source = Game.getObjectById(pending.sourceId);
const netState = {
creepEnergyNow: creep
? creep.store.getUsedCapacity(RESOURCE_ENERGY)
: null,
sourceEnergyNow: source?.energy ?? null
};
delete state.pendingHarvest;
if (matches.length !== 1) {
return {
status: matches.length === 0
? 'accepted-event-not-found'
: 'ambiguous-harvest-events',
requestId: pending.requestId,
matchCount: matches.length,
netState
};
}
return {
status: 'harvest-event-verified',
requestId: pending.requestId,
eventData: matches[0].data || null,
beforeEnergy: pending.beforeEnergy,
netState
};
}
The event binds the acting Creep to the selected Source. Net Energy can also be changed by transfer, drop, withdrawal, or another harvest, so it remains supporting context.
Complete dynamic-harvester loop
function runDynamicHarvester(creepName) {
Memory.sourceSelection ??= {};
const state = Memory.sourceSelection[creepName] ??= {
pendingHarvest: null,
lastMovement: null
};
if (state.lastRunAt === Game.time) {
return { status: 'already-run-this-tick' };
}
state.lastRunAt = Game.time;
const creep = Game.creeps[creepName];
if (!creep) {
return { status: 'creep-not-found' };
}
const verification = verifySelectedHarvest(
creep.room,
state
);
if (state.pendingHarvest) {
return {
status: 'harvest-already-pending',
verification
};
}
const submission = submitSelectedHarvest(
creep,
state
);
state.lastStatus = submission.status;
state.lastRunAt = Game.time;
state.lastVerification = verification;
return {
status: submission.status,
verification,
submission
};
}
module.exports.loop = function () {
const outcome = runDynamicHarvester('Harvester1');
if (
outcome.status.includes('rejected')
|| outcome.status.includes('not-found')
|| outcome.status.includes('missed')
|| Game.time % 100 === 0
) {
console.log(JSON.stringify({
type: 'source-selection',
gameTick: Game.time,
outcome
}));
}
};
Failure states and boundaries
| Status or result | Meaning | Next action |
|---|---|---|
complete-active-source-not-found | No active candidate produced a complete same-room path | Inspect candidate count, matrix, limits, or wait for regeneration |
stored-source-vision-unavailable | The saved room is not visible | Wait or use a scouting policy; do not claim deletion |
no-energy-capacity-for-this-role | The Creep does not fit this carrying-harvester contract | Use a fixed-miner design or change the body |
ERR_NOT_ENOUGH_RESOURCES | The selected Source is empty now | Preserve in fixed mode or clear in dynamic mode |
ERR_FULL | The Creep cannot accept more Energy | Switch to delivery |
ERR_NOT_IN_RANGE | The Creep is not at range 1 | Record movement and observe later position |
ERR_NO_BODYPART | No active harvesting capability remains | Inspect body damage or replacement |
Production adaptation notes
- Build or cache the static matrix once per room and runtime when many Creeps reassign in the same tick. The cache must be disposable and invalidated after layout changes.
- Do not run a full path search every tick. Persist the Source ID and search only when the task policy requires reassignment.
- Assignment count does not reserve a harvesting tile. A production miner scheduler should count usable positions and its own occupancy contract.
- Temporary Creep traffic is excluded from the assignment matrix. Use later-tick movement diagnostics for congestion.
- For remote mining, separate route planning, room visibility, reservation, replacement timing, and hostile policy from this same-room selector.
Choose another guide when
Use the path-search diagnostic when one known target fails because of CostMatrix, callback, or search limits. Use the accepted-movement diagnostic when moveTo() returns OK but positions do not change across later ticks. Use the saved-object guide when the main problem is interpreting a null object lookup rather than choosing among Sources.