HARVESTING · SOURCE TARGET SELECTION

How to Select an Active Source by Reachable Path Without Target Churn

Recover a stored Source ID first, distinguish FIND_SOURCES from FIND_SOURCES_ACTIVE, build reachable path candidates, rank path length before assignment count and stable ID, store the selected identity, handle empty Sources according to a documented dynamic policy, and preserve harvest and movement results.

Verification statusChinese source article: Reviewed in full · Official docs: Checked — Source, FIND_SOURCES, FIND_SOURCES_ACTIVE, RoomPosition path methods, Game.getObjectById(), harvest() and return codes · Policy boundary: Dynamic empty-Source replacement, assignment counts, ignoreCreeps and path options are project choices

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official docs
Checked — Source, FIND_SOURCES, FIND_SOURCES_ACTIVE, RoomPosition path methods, Game.getObjectById(), harvest() and return codes
Policy boundary
Dynamic empty-Source replacement, assignment counts, ignoreCreeps and path options are project choices
Visibility boundary
A null ID recovery can mean no visibility for a remote room; local and remote target records must not use the same deletion rule blindly
JavaScript syntax
Passed
Offline selection review
Passed — active state, reachability, path length, assignment count, stable ID, stored target and no-candidate states
Screeps Console test
Pending
Live multi-Source pathing, traffic, regeneration, assignment contention, remote visibility and CPU test
Pending
Last verified
July 26, 2026

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

CodeTypical meaningResponse
OKHarvest scheduledObserve Store and Source later
ERR_NOT_OWNERCreep not yoursCurrent Creep
ERR_BUSYCreep spawningWait
ERR_NOT_ENOUGH_RESOURCESSource currently emptyApply fixed or dynamic policy
ERR_INVALID_TARGETObject is not harvestableRefresh current target
ERR_FULLCreep Store fullSwitch task state
ERR_NOT_IN_RANGENot adjacentMove to range 1
ERR_NO_BODYPARTNo active WORKBody 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.

Official documentation

SOURCE AND SCOPE

Review the source, evidence, or next system

This English guide is rewritten for a focused search intent while preserving the technical scope and verification boundaries of its Chinese source. Live-room evidence is claimed only when the verification record says it exists.