SPAWNING · EXIT DIRECTION DIAGNOSIS

Screeps Spawn Exit Blocked: Diagnose Directions and Occupied Tiles

Verification statusOfficial API: Checked — spawnCreep directions and Spawning.setDirections boundaries · Pinned engine source: Checked — ordered completion search plus hostile-Creep spawn-stomp exception · JavaScript syntax: Checked by deterministic article simulation

VERIFICATION

Evidence and test status

Official API
Checked — spawnCreep directions and Spawning.setDirections boundaries
Pinned engine source
Checked — ordered completion search plus hostile-Creep spawn-stomp exception
JavaScript syntax
Checked by deterministic article simulation
Offline deterministic cases
Checked — explicit direction contract, planning, submission tracking, and later-tick observation
Screeps Console test
Pending
Official-shard multi-tick verification
Pending
Live hostile occupancy / spawn-stomp
Pending — engine-source behavior is not presented as a live reproduction
Evidence level
Official API and pinned engine source, static source review, syntax checks, and offline deterministic simulation

Debugging fast path: confirm the symptom first

Start here before changing a production planner. Read the active Spawn lifecycle, the named Creep, and all eight adjacent tiles in the same tick. This is read-only diagnostic evidence.

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 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,
    creepNames: creeps.map(creep => creep.name),
    powerCreepNames: powerCreeps.map(creep => creep.name)
  };
}

const spawn = Object.values(Game.spawns)[0];

if (!spawn) {
  console.log('No owned Spawn is visible.');
} else {
  const name = spawn.spawning?.name ?? null;

  console.log(JSON.stringify({
    tick: Game.time,
    spawn: spawn.name,
    name,
    remainingTime:
      spawn.spawning?.remainingTime ?? null,
    directions:
      spawn.spawning?.directions ?? null,
    creepVisible:
      name ? Boolean(Game.creeps[name]) : false,
    tiles: ALL_SPAWN_DIRECTIONS.map(direction =>
      inspectSpawnExitTile(spawn, direction)
    )
  }, null, 2));
}

If there is no active spawn.spawning, this is not an active Spawn-completion wait. If there is an active name, compare remainingTime, directions, Game.creeps[name], and the eight tile states before deciding what to change.

The tile scan is a script-visible diagnostic approximation, not the engine's private movement-reservation state. Same-tick movement can change occupancy before completion is processed.

Likely causes: symptom → evidence → cause → fix

SymptomEvidence to readLikely causeFix
Allowed tiles stay stable-obstacleTerrain, structures, construction sitesLayout blocks the configured exitsChange the layout or permit a structurally usable direction.
Allowed tiles are structurally valid but occupiedCreep/Power Creep names on the eight tilesTemporary trafficMove traffic away or keep additional valid directions available.
Only one allowed direction waits while another adjacent tile is openspawn.spawning.directions plus tile scanDeliberately strict direction policyAdd fallback directions if strict one-exit behavior is not intentional.
The final real spawnCreep() returned an errorExact final return codeThe request never entered this lifecycleFix the rejection with the return-code guide; do not start a birth observer.
The Creep is already born but later does not moveGame.creeps[name], creep.spawning, later positionOrdinary movement/pathing problemSwitch to the movement guide.

What Spawn completion actually does

spawnCreep() === OK means the request was scheduled. It does not prove that the Creep has completed spawning or appeared on an adjacent tile. Likewise, setDirections() === OK means the direction update intent was accepted, not that a listed tile is free at settlement time.

In the pinned public engine source, completion checks the configured directions in order against stable obstacles, blocking construction sites, terrain, and movement occupancy. When an allowed tile succeeds, the Creep is moved there and spawning becomes false. If no allowed tile succeeds, the ordinary path can remain incomplete for a later tick.

Hostile occupancy is a verified engine-source exception. While checking the allowed directions, the engine remembers the first hostile Creep it finds. If all allowed directions fail and there is no usable opening among the directions that were excluded by policy, the engine can spawn-stomp that hostile Creep and complete the new Creep on that tile. If an excluded direction is actually open, the engine does not use the stomp shortcut and leaves completion unfinished. This behavior is verified from the pinned engine source; a controlled real-shard hostile-occupancy/spawn-stomp reproduction has not been collected for this article.

Keep the directions contract explicit

The helper below has one simple contract: when you pass an array, it keeps only the valid unique directions from that array and preserves their order. When you omit the array, it defaults to all eight directions. It does not silently add fallbacks to an explicit policy.

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

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

  return valid;
}

Therefore [RIGHT] is a deliberate strict one-exit policy, while omitted input means “consider all eight.” An explicit empty or all-invalid array produces no allowed direction, which the planner rejects instead of silently widening the policy.

Build one deterministic exit plan

After the fast diagnostic confirms an exit problem, reuse the same tile inspection for a small deterministic plan. Stable blockers are excluded. Currently open tiles come before temporarily occupied but structurally valid tiles, while the explicit direction order remains stable 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 directions at the end is useful for non-strict policies because current traffic may move before a long spawn finishes. The planner still respects a deliberately narrow explicit array.

Keep dryRun and real submission separate

Use one plan for both calls. dryRun checks the request without scheduling it; only the final real call can create an active lifecycle record. The final return code can still differ because same-tick code may consume the Spawn, Energy, or name between calls.

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

  Memory.spawnExitChecks ??= {};
  delete Memory.spawnExitChecks[name];

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

  if (result !== OK) {
    return {
      status: 'spawn-submit-rejected',
      result,
      plan
    };
  }

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

  return {
    status: 'spawn-scheduled',
    result,
    plan
  };
}

A rejected final submission preserves the real return code in the returned result but does not enter the “waiting for birth” observer state.

Refresh directions near completion

If room traffic changes during a long spawn, one Spawn coordinator can refresh the same policy near completion. Do not let unrelated role modules compete to overwrite the list.

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

The same evidence rule applies here: an accepted setDirections() call records an accepted intent, not a completed exit.

Observe completion across ticks

Only observe names that have an active record created after a real spawnCreep() === OK. Repeated near-complete observations can support the local label completion-retry-observed; they do not identify the blocker without the corresponding tile state.

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,
      directions:
        spawn.spawning.directions ?? null,
      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 your later observation, record that the birth direction was missed instead of inventing one from a later position.

Retest after the fix

  1. Same tick: record the exact dryRun, real spawnCreep(), or setDirections() return code. That is call evidence only.
  2. Later tick: re-read spawn.spawning?.name, spawn.spawning?.remainingTime, spawn.spawning?.directions, Game.creeps[name], and any adjacent tile state needed to explain the result.
  3. Do not rename an accepted request or direction update as “Creep born.” The observed later world state is separate evidence.

Evidence boundary and official sources

The public API contract and the pinned Screeps engine source were checked for direction ordering, completion occupancy, and the hostile-Creep spawn-stomp branch. JavaScript blocks and deterministic planner/lifecycle cases are checked statically. Genuine Screeps Console output, controlled official-shard completion retries, same-tick traffic races, Power Creep occupancy, and a live hostile-occupancy/spawn-stomp reproduction remain Pending.

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

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.