SPAWNING · EXIT DIRECTION DIAGNOSIS

Screeps Spawn Exit Blocked: Diagnose Directions and Occupied Tiles

Verification statusOfficial API: Checked — spawnCreep directions and Spawning.setDirections boundaries · Public engine source: Checked — ordered exit search, obstacle checks and later-tick completion retry · JavaScript syntax: Passed by the article simulation gate

VERIFICATION

Evidence and test status

Official API
Checked — spawnCreep directions and Spawning.setDirections boundaries
Public engine source
Checked — ordered exit search, obstacle checks and later-tick completion retry
JavaScript syntax
Passed by the article simulation gate
Offline cases
53 direction-planning and observation assertions passed
Screeps Console test
Pending
Official-shard multi-tick verification
Pending
Evidence level
Official source review, repository integration, syntax checks and offline simulation only

Use this guide after spawnCreep() succeeds

Use this page when the final spawnCreep() call returned OK, the named Creep exists in the spawning lifecycle, but completion appears delayed or the Creep does not become available on an adjacent tile. Use the return-code guide when the request itself was rejected. Use the movement guide only after the Creep has finished spawning and ordinary movement code is running.

The useful model is not “the Spawn is done, therefore the Creep must already be outside.” The completion step still needs one allowed adjacent tile that is usable during settlement.

What the completion step actually checks

The public API accepts an ordered directions array containing direction constants from TOP through TOP_LEFT. The pinned public engine source checks those directions in order, rejects stable obstacle objects and blocking construction sites, consults movement occupancy, and retries completion on a later tick when no permitted tile succeeds. A direction list changes the search order and allowed set; it does not remove an obstacle or reserve a tile.

ObservationWhat it supportsWhat it does not prove
spawnCreep() === OKThe spawn request was scheduledThe Creep already exited
setDirections() === OKThe update intent was acceptedAny listed tile is now free
One empty-looking tileA current script-visible candidateNo same-tick movement contention
Repeated near-complete observationsThe process did not finish immediatelyThe exact blocking object without a tile snapshot

Normalize preferred and fallback directions

Do not let an invalid or single-direction configuration silently remove every fallback. Preserve the player's preferred order, discard invalid values and duplicates, then append the remaining directions.

const ALL_SPAWN_DIRECTIONS = [
  TOP,
  TOP_RIGHT,
  RIGHT,
  BOTTOM_RIGHT,
  BOTTOM,
  BOTTOM_LEFT,
  LEFT,
  TOP_LEFT
];

const SPAWN_DIRECTION_OFFSETS = {
  [TOP]: [0, -1],
  [TOP_RIGHT]: [1, -1],
  [RIGHT]: [1, 0],
  [BOTTOM_RIGHT]: [1, 1],
  [BOTTOM]: [0, 1],
  [BOTTOM_LEFT]: [-1, 1],
  [LEFT]: [-1, 0],
  [TOP_LEFT]: [-1, -1]
};

function normalizeSpawnDirections(input) {
  const preferred = Array.isArray(input)
    ? input
    : [];
  const valid = [];
  const seen = new Set();

  for (const direction of preferred) {
    if (
      Number.isInteger(direction)
      && direction >= TOP
      && direction <= TOP_LEFT
      && !seen.has(direction)
    ) {
      seen.add(direction);
      valid.push(direction);
    }
  }

  for (const direction of ALL_SPAWN_DIRECTIONS) {
    if (!seen.has(direction)) {
      seen.add(direction);
      valid.push(direction);
    }
  }

  return valid;
}

A true one-exit layout can still pass one direction deliberately. That is a policy choice with an explicit waiting risk, not a safe default for every room.

Separate stable blockers from temporary occupancy

A terrain wall or obstacle structure is a stable layout problem. A Creep or Power Creep on an otherwise passable tile is usually temporary. Keep those states separate so a unit standing beside the Spawn now does not permanently remove a direction needed many spawning ticks later.

function structureBlocksSpawnExit(structure) {
  if (structure.structureType === STRUCTURE_RAMPART) {
    return !structure.my && !structure.isPublic;
  }

  return OBSTACLE_OBJECT_TYPES.includes(
    structure.structureType
  );
}

function inspectSpawnExitTile(spawn, direction) {
  const offset = SPAWN_DIRECTION_OFFSETS[direction];

  if (!offset) {
    return {
      direction,
      status: 'direction-invalid',
      stablePassable: false,
      currentlyOpen: false
    };
  }

  const x = spawn.pos.x + offset[0];
  const y = spawn.pos.y + offset[1];

  if (x < 0 || x > 49 || y < 0 || y > 49) {
    return {
      direction,
      x,
      y,
      status: 'outside-room',
      stablePassable: false,
      currentlyOpen: false
    };
  }

  const structures = spawn.room.lookForAt(
    LOOK_STRUCTURES,
    x,
    y
  );
  const sites = spawn.room.lookForAt(
    LOOK_CONSTRUCTION_SITES,
    x,
    y
  );
  const creeps = spawn.room.lookForAt(
    LOOK_CREEPS,
    x,
    y
  );
  const powerCreeps = spawn.room.lookForAt(
    LOOK_POWER_CREEPS,
    x,
    y
  );
  const hasRoad = structures.some(
    structure =>
      structure.structureType === STRUCTURE_ROAD
  );
  const terrain = spawn.room.getTerrain().get(x, y);
  const terrainBlocked =
    (terrain & TERRAIN_MASK_WALL) !== 0
    && !hasRoad;
  const stableBlocked =
    terrainBlocked
    || structures.some(structureBlocksSpawnExit)
    || sites.some(site =>
      OBSTACLE_OBJECT_TYPES.includes(
        site.structureType
      )
    );
  const occupied =
    creeps.length > 0
    || powerCreeps.length > 0;

  return {
    direction,
    x,
    y,
    status: stableBlocked
      ? 'stable-obstacle'
      : occupied
        ? 'temporarily-occupied'
        : 'open-now',
    stablePassable: !stableBlocked,
    currentlyOpen: !stableBlocked && !occupied
  };
}

This is a script-visible snapshot, not the engine's internal movement reservation table. Same-tick movement can change the result before settlement, so keep uncertainty visible.

Build one deterministic exit plan

Exclude stable blockers, place currently open tiles before temporarily occupied tiles, and preserve normalized preference order inside each group.

function planSpawnExitDirections(
  spawn,
  preferredDirections
) {
  const snapshots = normalizeSpawnDirections(
    preferredDirections
  ).map(direction =>
    inspectSpawnExitTile(spawn, direction)
  );

  const stable = snapshots.filter(
    item => item.stablePassable
  );
  const open = stable.filter(
    item => item.currentlyOpen
  );
  const occupied = stable.filter(
    item => !item.currentlyOpen
  );

  return {
    status: stable.length === 0
      ? 'no-stable-exit'
      : open.length === 0
        ? 'temporary-occupancy-only'
        : 'exit-plan-ready',
    directions: [
      ...open,
      ...occupied
    ].map(item => item.direction),
    snapshots
  };
}

Keeping temporarily occupied but structurally valid tiles at the end matters because the request may complete much later. Removing every currently occupied direction can turn a short traffic conflict into a self-imposed permanent restriction.

Reuse the same plan for dryRun and submission

Use one immutable plan for the preflight and final request. Recomputing between calls makes the recorded preflight evidence describe a different request.

function submitSpawnWithExitPlan({
  spawn,
  body,
  name,
  memory,
  preferredDirections
}) {
  if (!spawn?.my) {
    return {
      status: 'owned-spawn-required',
      result: null
    };
  }

  const plan = planSpawnExitDirections(
    spawn,
    preferredDirections
  );

  if (plan.directions.length === 0) {
    return {
      status: plan.status,
      result: null,
      plan
    };
  }

  const options = {
    memory,
    directions: plan.directions
  };
  const dryRunResult = spawn.spawnCreep(
    body,
    name,
    {
      ...options,
      dryRun: true
    }
  );

  if (dryRunResult !== OK) {
    return {
      status: 'dry-run-rejected',
      result: dryRunResult,
      plan
    };
  }

  const result = spawn.spawnCreep(
    body,
    name,
    options
  );

  Memory.spawnExitChecks ??= {};
  Memory.spawnExitChecks[name] = {
    spawnId: spawn.id,
    name,
    submittedAt: Game.time,
    directions: [...plan.directions],
    result,
    lastObservedAt: Game.time,
    nearCompleteTicks: 0
  };

  return {
    status: result === OK
      ? 'spawn-scheduled'
      : 'spawn-submit-rejected',
    result,
    plan
  };
}

The final call can still differ from dryRun because another same-tick module may consume the Spawn, name, or Energy first. Preserve the final return code.

Refresh directions near completion

For a long body, the tile that was open at submission may be occupied at completion. One Spawn coordinator may refresh the ordered list shortly before completion:

function refreshSpawnExitDirections(
  spawn,
  preferredDirections,
  refreshAtRemainingTime = 3
) {
  if (!spawn?.spawning) {
    return {
      status: 'no-active-spawn',
      result: null
    };
  }

  if (
    !Number.isInteger(refreshAtRemainingTime)
    || refreshAtRemainingTime < 0
  ) {
    return {
      status: 'refresh-threshold-invalid',
      result: null
    };
  }

  if (
    spawn.spawning.remainingTime
    > refreshAtRemainingTime
  ) {
    return {
      status: 'refresh-not-due',
      result: null
    };
  }

  const plan = planSpawnExitDirections(
    spawn,
    preferredDirections
  );

  if (plan.directions.length === 0) {
    return {
      status: plan.status,
      result: null,
      plan
    };
  }

  const result = spawn.spawning.setDirections(
    plan.directions
  );

  return {
    status: result === OK
      ? 'directions-refresh-accepted'
      : 'directions-refresh-rejected',
    result,
    plan
  };
}

Do not let role modules compete to call setDirections(). The final direction state should come from one coordinator with one observable result.

Verify retries and the first visible birth state

Observe the same Spawn and Creep name across ticks. A repeated near-complete process supports a local “completion retry observed” diagnosis. It does not identify the blocker without the corresponding tile snapshots.

function observeSpawnExit(name) {
  Memory.spawnExitChecks ??= {};
  const record = Memory.spawnExitChecks[name];

  if (!record) {
    return {
      status: 'spawn-exit-record-missing'
    };
  }

  const spawn = Game.getObjectById(record.spawnId);
  const creep = Game.creeps[name];

  if (
    spawn?.spawning
    && spawn.spawning.name === name
  ) {
    const nearComplete =
      spawn.spawning.remainingTime <= 1;

    record.nearCompleteTicks = nearComplete
      ? (record.nearCompleteTicks ?? 0) + 1
      : 0;
    record.lastObservedAt = Game.time;
    record.lastRemainingTime =
      spawn.spawning.remainingTime;

    return {
      status: record.nearCompleteTicks >= 2
        ? 'completion-retry-observed'
        : 'still-spawning',
      remainingTime:
        spawn.spawning.remainingTime,
      nearCompleteTicks:
        record.nearCompleteTicks
    };
  }

  if (creep?.spawning) {
    return {
      status: 'creep-still-inside-spawn'
    };
  }

  if (creep && spawn) {
    const dx = creep.pos.x - spawn.pos.x;
    const dy = creep.pos.y - spawn.pos.y;
    const direction = Object.entries(
      SPAWN_DIRECTION_OFFSETS
    ).find(([, offset]) =>
      offset[0] === dx
      && offset[1] === dy
    )?.[0];

    return {
      status: direction
        ? 'born-on-observable-adjacent-tile'
        : 'born-but-exit-direction-missed',
      direction: direction
        ? Number(direction)
        : null,
      wasPlanned: direction
        ? record.directions.includes(
            Number(direction)
          )
        : null
    };
  }

  return {
    status: 'completion-unverified',
    lastObservedAt: record.lastObservedAt
  };
}

The Creep may receive a movement intent immediately after birth. If it has already left range 1 before observation, record that the birth direction was missed instead of inferring it from a later position.

Common failure modes

  • Only one direction is allowed: a temporary occupant can force the process to wait even while another adjacent tile is free.
  • Current occupancy is treated as permanent: a Hauler present at submission may leave long before completion.
  • Every module refreshes directions: the final order depends on module execution order rather than one policy.
  • OK is renamed “born”: accepted requests and observed birth are different evidence states.
  • Spawn diagnostics continue after birth: once creep.spawning === false, ordinary movement and pathfinding guides own the next problem.

Evidence and production boundary

This revision checks the public API and pinned engine source, syntax-checks every JavaScript block, and runs 53 offline assertions covering direction normalization, fallback order, stable blockers, temporary occupancy, deterministic planning, no-exit states, completion retries, observable adjacent birth, missed direction windows, and invalid observations.

Genuine Screeps Console output, official-shard completion retries, same-tick traffic, Power Creeps, hostile occupancy and spawnstomp, screenshots, and long-running production evidence remain pending.

Official references: StructureSpawn.spawnCreep(), StructureSpawn.Spawning, setDirections(), and the game-loop model.

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.