CODE ORGANIZATION · TICK-SAFE MODULE CONTRACTS

Split Screeps Code into Modules Without Hiding Tick Boundaries

Verification statusChinese source article: Existing bilingual mapping retained; this English-only pass did not use the Chinese page as live-runtime evidence · Official documentation: Checked August 18, 2026 — Screeps module organization, the main exported game loop, and Game/Memory lifetime boundaries · Contributed caching overview: Reviewed separately — require/global cache behavior is treated as contributed guidance, not as a core API guarantee

VERIFICATION

Evidence and test status

Chinese source article
Existing bilingual mapping retained; this English-only pass did not use the Chinese page as live-runtime evidence
Official documentation
Checked August 18, 2026 — Screeps module organization, the main exported game loop, and Game/Memory lifetime boundaries
Contributed caching overview
Reviewed separately — require/global cache behavior is treated as contributed guidance, not as a core API guarantee
Static code review
Passed — helper exports are not described as automatically scheduled; current-tick objects stay out of durable module state; role exports and returned result shapes are both validated before dispatch
Evidence level
Official-documentation review, contributed caching guidance, and static code review only; no live module-load or reset trace is claimed
Console test pending
Not run in this editorial pass
Screeps Console test
Pending — no real-shard module load or per-Creep exception transcript was collected for this revision
Live multi-tick verification pending
No live code reload, global reset, stale-object reproduction, malformed role result, or recovery trace was collected for this revision
Last editorial review
August 18, 2026

Use this guide when

Use this page when one growing main file needs clear module boundaries. The goal is not to create more files. The goal is to keep one game loop, give each module a small public contract, pass current-tick objects into that contract, and make failures attributable to one Creep or subsystem.

Separate four kinds of module state

StateWhere it belongsLifetime
Functions, constants, role registryModule scopeStable until code reload or runtime reset
Disposable acceleration cacheModule scope or globalMay disappear; must rebuild
Current Game objects and query resultsInside the active loop or passed argumentsCurrent tick only
Durable decisions and assignmentsMemory or another persistent storeAcross runtime resets

The require cache is useful for stable definitions. It does not turn current Game, Room, Creep, or Structure instances into durable objects.

Keep one exported main loop

Only the loop exported by the main module is the engine entry point. A helper module may export any property name — including a property named loop — but Screeps does not schedule that helper export automatically; main must call it explicitly.

Give a role one small public contract

function run(creep, context) {
  if (!creep || creep.spawning === true) {
    return {
      status: 'creep-unavailable'
    };
  }

  return {
    status: 'role-ready',
    creepName: creep.name,
    tick: context.tick
  };
}

module.exports = { run };

The role receives the current Creep and a small current-tick context. It does not rescan every Creep, own global scheduling, or hide persistent writes from the caller.

Load stable module definitions explicitly

const roles = Object.freeze({
  harvester: require('role.harvester'),
  upgrader: require('role.upgrader')
});

function validateRoles(roleMap) {
  const errors = [];

  for (const [name, role] of Object.entries(roleMap)) {
    if (!role || typeof role.run !== 'function') {
      errors.push({
        name,
        reason: 'run-export-missing'
      });
    }
  }

  return {
    valid: errors.length === 0,
    errors
  };
}

const roleValidation = validateRoles(roles);

Running this validation when the runtime loads is appropriate because it examines stable exports, not current world state. A missing or malformed export should remain visible instead of falling back to another role.

Never preserve a current-world snapshot at module scope

const staleNames = Object.keys(Game.creeps);

function getStaleNames() {
  return staleNames;
}

function getCurrentCreepNames() {
  return Object.keys(Game.creeps).sort();
}

The first array reflects only the tick when that runtime initialized. The function on the right reads the current Game.creeps object each time the loop calls it. The same boundary applies to Game.rooms, room.find(), and objects recovered by ID.

Make module-scope caches rebuildable

function getRoleCountCache() {
  global.roleCountCache ??= {
    tick: null,
    counts: null
  };

  if (global.roleCountCache.tick !== Game.time) {
    const counts = {};

    for (const creep of Object.values(Game.creeps)) {
      const role = creep.memory.role ?? 'unassigned';
      counts[role] = (counts[role] ?? 0) + 1;
    }

    global.roleCountCache = {
      tick: Game.time,
      counts
    };
  }

  return global.roleCountCache.counts;
}

This cache is valid only for the current tick and is rebuilt after a runtime reset. It is acceleration, not the source of truth. Use the global-cache guide for longer-lived derived data and invalidation.

Isolate one Creep failure without hiding it

function runOneCreep(creep, context) {
  const roleName = creep.memory.role;
  const role = roles[roleName];

  if (!role) {
    return {
      status: 'unknown-role',
      roleName: roleName ?? null
    };
  }

  try {
    const result = role.run(creep, context);

    if (!result || typeof result.status !== 'string') {
      return {
        status: 'invalid-role-result',
        roleName
      };
    }

    return result;
  } catch (error) {
    return {
      status: 'role-threw',
      roleName,
      message: error instanceof Error
        ? error.message
        : String(error)
    };
  }
}

module.exports.loop = function () {
  if (!roleValidation.valid) {
    console.log(JSON.stringify({
      type: 'role-module-invalid',
      errors: roleValidation.errors
    }));
    return;
  }

  const context = { tick: Game.time };

  for (const name of Object.keys(Game.creeps).sort()) {
    const result = runOneCreep(
      Game.creeps[name],
      context
    );

    if (
      result.status === 'unknown-role'
      || result.status === 'invalid-role-result'
      || result.status === 'role-threw'
    ) {
      console.log(JSON.stringify({
        type: 'creep-role-failure',
        creepName: name,
        ...result
      }));
    }
  }
};

The boundary lets other Creeps continue while preserving the failure. Production code should rate-limit repeated messages and retain enough stack or context to reproduce the first error.

Checking that role.run exists proves only that the export is callable. If the dispatcher later branches on result.status, then a result object with a string status is part of the project contract too. Validate that boundary before dereferencing the result so one malformed role does not crash the main loop outside the per-Creep failure guard.

Distinguish load failures from runtime role failures

A syntax error, missing module name, or exception during top-level module initialization can prevent main from loading. The per-Creep try/catch above cannot catch an error that occurs before the loop exists. Fix the first parser, resolution, or initialization error in the Console before debugging role behavior.

Circular dependencies can also expose partially initialized exports. Move shared constants and pure helpers into a lower-level module instead of making role modules require each other.

Verify normal ticks and a real runtime reset separately

Record the runtime start tick, current Game.time, loaded role names, and one current Creep name. Confirm that current names change without redeploying. Then observe a genuine runtime reset before claiming cache recovery. This revision contains syntax and static architecture checks, not a fabricated module-load transcript.

Choose another guide when

Use the Memory guide for durable role state. Use the working-state guide for one Creep's task transition. Use the cache guide when the main problem is rebuildable derived data rather than file organization.

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.