HARVESTING · SOURCE ASSIGNMENT

Select a Reachable Source Without Treating a Partial Path as Success

CURATED PATH

These links are maintained for a concrete prerequisite, failure mode, debugging step, or next lesson instead of generic topic similarity.

Movement

Recommended next steps

Separate linear range from reachable pathPrerequisite conceptInspect the path search directlyPathfinding APIReturn to the Energy harvesting loopTask context
Verification statusExisting English route: Preserved — no URL, slug, Canonical, or Chinese mapping change · Official Screeps API: Reviewed — Source queries, PathFinder result boundaries, harvest(), movement, event log, and object IDs · Project-policy boundary: Dynamic versus fixed mode, assignment counts, matrix costs, maxOps, and tie-break order are examples

VERIFICATION

Evidence and test status

Existing English route
Preserved — no URL, slug, Canonical, or Chinese mapping change
Official Screeps API
Reviewed — Source queries, PathFinder result boundaries, harvest(), movement, event log, and object IDs
Project-policy boundary
Dynamic versus fixed mode, assignment counts, matrix costs, maxOps, and tie-break order are examples
Static validation
Complete-path filter, current-Creep count exclusion, visible-room invalidation, event identity, and JavaScript syntax reviewed
Validation level
Official API review, JavaScript syntax review, static control-flow review, and repository checks
Screeps Console test
Pending
Live multi-tick verification
Pending
Genuine room or Console screenshots
Pending
Last editorial review
August 3, 2026

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;
}

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 resultMeaningNext action
complete-active-source-not-foundNo active candidate produced a complete same-room pathInspect candidate count, matrix, limits, or wait for regeneration
stored-source-vision-unavailableThe saved room is not visibleWait or use a scouting policy; do not claim deletion
no-energy-capacity-for-this-roleThe Creep does not fit this carrying-harvester contractUse a fixed-miner design or change the body
ERR_NOT_ENOUGH_RESOURCESThe selected Source is empty nowPreserve in fixed mode or clear in dynamic mode
ERR_FULLThe Creep cannot accept more EnergySwitch to delivery
ERR_NOT_IN_RANGEThe Creep is not at range 1Record movement and observe later position
ERR_NO_BODYPARTNo active harvesting capability remainsInspect 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.

Official documentation

SOURCE AND SCOPE

Review the source, evidence, or next system

This English guide keeps the technical scope and verification boundaries of its Chinese source. The verification record shows which checks were completed and which runtime observations are still pending.