CODE ORGANIZATION · COMMONJS MODULES

How to Split Screeps Code into Modules Without Caching Stale Game Objects

Keep one module.exports.loop, export small role functions, validate module shapes, read current Game objects inside each tick, avoid module-scope world snapshots, and preserve diagnostics for missing roles and load failures.

Verification statusChinese source article: Reviewed in full · Official docs: Checked — Screeps modules, require(), module.exports, the main loop and global reset behavior · Architecture boundary: Role names, file names, router behavior and logging intervals are project conventions

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official docs
Checked — Screeps modules, require(), module.exports, the main loop and global reset behavior
Architecture boundary
Role names, file names, router behavior and logging intervals are project conventions
Tick boundary
Stable module definitions may be cached, but current Game, Room, Creep and Structure objects must be read during the active tick
JavaScript syntax
Passed
Offline module review
Passed — valid exports, missing run(), unknown roles, missing Creeps and stale snapshot states
Screeps Console test
Pending
Live module loading, syntax failure, global reset, role routing and stale-object test
Pending
Last verified
July 26, 2026

Quick answer

Keep exactly one module.exports.loop in the main module. Let each role file export a small function such as run(creep), require modules through explicit names, validate their public shape, and read Game, Room, Creep and Structure objects inside the current tick. Module-scope constants and functions are appropriate; module-scope snapshots of current world objects are not.

Keep one exported game loop

const roleHarvester = require('role.harvester');
const roleUpgrader = require('role.upgrader');

module.exports.loop = function () {
  for (const creep of Object.values(Game.creeps)) {
    if (creep.memory.role === 'harvester') {
      roleHarvester.run(creep);
    } else if (creep.memory.role === 'upgrader') {
      roleUpgrader.run(creep);
    }
  }
};

Do not export a second loop from a role file and expect both loops to run. Main must call role functions explicitly.

Give each role one contract

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

  const source = creep.pos.findClosestByPath(
    FIND_SOURCES_ACTIVE
  );
  if (!source) {
    return { status: 'source-not-found' };
  }

  const result = creep.harvest(source);
  if (result === ERR_NOT_IN_RANGE) {
    return {
      status: 'moving-to-source',
      result,
      moveResult: creep.moveTo(source, {
        range: 1,
        reusePath: 10
      })
    };
  }

  return {
    status: result === OK
      ? 'harvest-scheduled'
      : 'harvest-rejected',
    result,
    sourceId: source.id
  };
}

module.exports = { run };

A role receives the current Creep, performs one bounded decision and returns diagnostics. It does not search all Creeps again or own the global loop.

Route Creeps from main

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

function runCreepRole(creep) {
  const roleName = creep?.memory?.role;
  const role = roles[roleName];

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

  return role.run(creep);
}

The role map makes supported names explicit. A typo should produce a diagnosis instead of silently selecting another behavior.

Validate module shape

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

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

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

This catches export mismatches before the router tries to call an absent function.

Read current state inside the tick

This captures one old collection:

const cachedHarvesters = Object.values(Game.creeps)
  .filter(creep => creep.memory.role === 'harvester');

function getHarvesters() {
  return cachedHarvesters;
}

Read current objects when the function runs:

function getCurrentHarvesters() {
  return Object.values(Game.creeps)
    .filter(creep =>
      creep.memory.role === 'harvester'
    )
    .sort((left, right) =>
      left.name.localeCompare(right.name)
    );
}

The same rule applies to Game.rooms, Game.structures, room.find() results and objects recovered by ID.

Understand module cache and global resets

const ROLE_NAMES = Object.freeze([
  'harvester',
  'upgrader'
]);
const roleNameSet = new Set(ROLE_NAMES);

function isKnownRole(roleName) {
  return roleNameSet.has(roleName);
}

Stable definitions can live at module scope. A global reset reruns initialization, so counters, queues and caches there are disposable unless rebuilt safely.

global.runtimeCache ??= {
  initializedAt: Game.time,
  roleNames: new Set()
};

This cache is acceleration, not durable state.

Complete modular example

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

module.exports.loop = function () {
  if (!validation.valid) {
    if (Game.time % 100 === 0) {
      console.log(JSON.stringify({
        type: 'role-module-validation',
        errors: validation.errors
      }));
    }
    return;
  }

  for (const creepName of Object.keys(Game.creeps).sort()) {
    const creep = Game.creeps[creepName];
    const outcome = runCreepRole(creep);

    if (
      outcome.status === 'unknown-role'
      || outcome.status.endsWith('-rejected')
    ) {
      console.log(JSON.stringify({
        type: 'creep-role-result',
        creepName,
        ...outcome
      }));
    }
  }
};

Handle unknown roles explicitly

function describeUnknownRole(creep) {
  return {
    gameTick: Game.time,
    creepName: creep?.name || null,
    roleName: creep?.memory?.role || null,
    status: 'unknown-role'
  };
}

Do not rewrite an unknown role unless a separate migration defines the intended mapping.

Diagnose module load failures

  • Fix the first syntax error in the required file.
  • Check the exact module and file names.
  • Check whether the export is an object or function.
  • Check for circular dependencies.
  • Keep the exported game loop in main.
  • Avoid reading unavailable tick objects during module initialization.

Debugging checklist

  • Keep one module.exports.loop.
  • Give each role one run(creep) contract.
  • Validate loaded module shapes.
  • Use an explicit role map.
  • Report unknown roles.
  • Read current game objects inside the tick.
  • Keep only stable definitions at module scope.
  • Treat global caches as disposable.
  • Store persistent state in serializable Memory.

Scope and next steps

This guide does not provide a bundler, process manager, dependency injection framework or complete role AI. Continue with the next reviewed operations guide.

Frequently asked questions

Can one module export several functions?

Yes. Keep the public contract small and explicit, such as { run, plan, validate }.

Should main catch every role exception?

A guarded boundary can keep other Creeps running, but it must preserve the error instead of turning every failure into silent success.

Can module-scope constants reference Screeps constants?

Stable constants are appropriate. Avoid capturing current world objects and query results.

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.