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
| State | Where it belongs | Lifetime |
|---|---|---|
| Functions, constants, role registry | Module scope | Stable until code reload or runtime reset |
| Disposable acceleration cache | Module scope or global | May disappear; must rebuild |
| Current Game objects and query results | Inside the active loop or passed arguments | Current tick only |
| Durable decisions and assignments | Memory or another persistent store | Across 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.