Quick answer
Game.rooms[roomName] is undefined when that room is not available to your script in the current tick. Check the Room before reading its Controller, structures, Sources, hostiles, or terrain-dependent live objects. A value in Memory.rooms[roomName] is historical application data, not proof of current vision.
What Game.rooms actually contains
Game.rooms is a hash of live Room objects available during the current tick. The global Game object is rebuilt each tick, so a Room reference is not a permanent handle that should be kept in Memory.
const roomName = 'W2N2';
const room = Game.rooms[roomName];
if (!room) {
console.log({
type: 'room-not-visible',
roomName,
tick: Game.time
});
return;
}
console.log({
type: 'room-visible',
roomName: room.name,
tick: Game.time
});
The guard is not optional defensive style. It is the boundary between “a live Room exists now” and “your code only knows a room name.”
What can make a room visible
The official API describes rooms as visible when you have a Creep or an owned structure in them. Other game mechanisms can also make a room available, including an Observer request completed for the current tick. The important application rule is simpler: use the presence of Game.rooms[roomName] as the current-tick fact.
| State | What your code may conclude |
|---|---|
Game.rooms[name] exists | The Room object is available now |
| The key is missing | Current room data is unavailable |
| A Creep was there previously | Historical fact only |
An Observer request returned OK this tick | The request was scheduled; read the target next tick |
| Memory contains room intel | Saved data exists, with unknown freshness unless timestamped |
Memory.rooms is not a live Room object
Memory.rooms may contain data after a room disappears from Game.rooms. That is useful for intel, route policy, source IDs, or your own configuration, but it must not be treated as a current Room snapshot.
function describeRoomAvailability(roomName) {
const room = Game.rooms[roomName];
const remembered = Memory.rooms?.[roomName];
return {
roomName,
visibleNow: Boolean(room),
hasRememberedData: Boolean(remembered),
rememberedAt: Number.isInteger(remembered?.observedAt)
? remembered.observedAt
: null
};
}
Without a timestamp and an explicit data contract, an old entry cannot answer whether the Controller is still owned, whether hostiles are present, or whether a Structure still exists.
Read a room with an explicit guard
State impact: this example reads live state and logs a bounded summary. It does not write Memory or submit game actions.
function inspectVisibleRoom(roomName) {
if (typeof roomName !== 'string' || roomName.length === 0) {
return {
status: 'room-name-invalid',
roomName: null
};
}
const room = Game.rooms[roomName];
if (!room) {
return {
status: 'room-not-visible',
roomName,
hasRememberedData: Boolean(
Memory.rooms?.[roomName]
)
};
}
const controller = room.controller;
return {
status: 'room-visible',
roomName: room.name,
controllerPresent: Boolean(controller),
controllerOwner: controller?.owner?.username ?? null,
controllerReservation:
controller?.reservation?.username ?? null,
controllerLevel: controller?.level ?? 0,
sourceCount: room.find(FIND_SOURCES).length,
hostileCount: room.find(FIND_HOSTILE_CREEPS).length
};
}
module.exports.loop = function () {
const result = inspectVisibleRoom('W2N2');
if (Game.time % 50 === 0) {
console.log(JSON.stringify({
type: 'room-visibility-check',
tick: Game.time,
...result
}));
}
};
The function returns a distinct status instead of replacing missing vision with an empty object. That prevents “unknown” from silently becoming “no hostiles,” “no Controller,” or “no structures.”
Complete visibility-first inspection helper
For reusable code, separate the live Room requirement from the business operation. Callers then choose whether to skip, request vision, use stale intel, or retry later.
function withVisibleRoom(roomName, reader) {
if (
typeof roomName !== 'string'
|| typeof reader !== 'function'
) {
return {
ok: false,
status: 'arguments-invalid',
value: null
};
}
const room = Game.rooms[roomName];
if (!room) {
return {
ok: false,
status: 'room-not-visible',
value: null
};
}
try {
return {
ok: true,
status: 'read-complete',
value: reader(room)
};
} catch (error) {
return {
ok: false,
status: 'reader-failed',
value: null,
message: error instanceof Error
? error.message
: String(error)
};
}
}
const result = withVisibleRoom(
'W2N2',
room => ({
roomName: room.name,
structures: room.find(FIND_STRUCTURES).length
})
);
The try/catch does not replace correct property checks. It only isolates an application reader so one inspection failure does not crash unrelated role logic.
Handle Controller states separately
A visible room can still have no Controller. Highways and some special rooms are examples where room.controller may be absent. A Controller can also be neutral, reserved, owned by you, or owned by another player.
| Condition | Meaning |
|---|---|
!room | No current Room object |
room && !room.controller | Visible room without a Controller |
controller.my | Your owned Controller |
controller.owner | An owned Controller |
controller.reservation | A reservation exists |
Do not combine these states into one truthy test when ownership, reservation, or room type changes behavior.
Object IDs also depend on visibility
An ID stored in Memory remains a string, but resolving it with Game.getObjectById() can return null when the object is unavailable. Keep the ID, distinguish missing vision from confirmed deletion when possible, and resolve it again after vision returns.
function resolveRememberedTarget(roomName, targetId) {
const visible = Boolean(Game.rooms[roomName]);
const target = targetId
? Game.getObjectById(targetId)
: null;
if (!visible) {
return {
status: 'room-not-visible',
target: null
};
}
return target
? { status: 'target-visible', target }
: { status: 'target-missing-in-visible-room', target: null };
}
When the room is visible and the expected object is still missing, your code has stronger evidence that the target was destroyed, expired, moved out of scope, or the stored ID is wrong.
Separate room names, positions, map data, and live Rooms
A valid room name is not the same thing as a live Room. Code can store a room-name string, construct a RoomPosition, or ask Game.map for map-level information without proving that Game.rooms[roomName] exists in this tick. Gate current Room methods and current Room objects on the live Game.rooms entry.
| Value | What it proves | What it does not prove |
|---|---|---|
| Room-name string | Your code has an identifier. | Current vision. |
RoomPosition | A room coordinate can be represented. | A live Room or current objects at that coordinate. |
Game.map result | Map-level information for that map operation. | That room.find() or room.controller is readable now. |
Game.rooms[roomName] | A live Room object is available this tick. | Why vision exists or how long it will remain. |
function describeRoomHandle(roomName) {
if (typeof roomName !== 'string') {
return { status: 'room-name-invalid' };
}
const position = new RoomPosition(
25,
25,
roomName
);
const room = Game.rooms[roomName] || null;
return {
status: room
? 'live-room-available'
: 'live-room-unavailable',
roomName,
representedPosition: {
x: position.x,
y: position.y,
roomName: position.roomName
},
liveRoom: Boolean(room)
};
}
Do not infer why vision exists
If Game.rooms[roomName] exists, you can safely say the room is available to the current script. Do not automatically say an Observer caused that vision: your Creeps, structures, or another supported visibility source may already expose the room. When attribution matters, keep the Observer ID, requested room, and request tick as separate evidence.
Apply the same evidence discipline to saved object IDs. If Game.getObjectById(id) returns null while the expected room is not visible, classify the object state as unknown rather than deleted. Once the expected room is visible, a missing ID is stronger evidence that the saved target is stale or absent; mobile-object assumptions still need their own identity check.
Use the Observer guide for the request/next-tick vision lifecycle and the saved-target guide for ID restoration.
Prove visibility across multiple ticks
A single log line proves only one tick. To debug intermittent visibility, record a compact transition history rather than writing entire Room objects.
function recordVisibility(roomName) {
Memory.visibilityHistory ??= {};
const history = Memory.visibilityHistory[roomName]
?? [];
history.push({
tick: Game.time,
visible: Boolean(Game.rooms[roomName])
});
Memory.visibilityHistory[roomName] =
history.slice(-20);
}
State impact: this writes a bounded Memory history. Twenty entries is an example limit, not an official recommendation.
Debugging checklist
- Verify the exact room-name string.
- Check
Game.rooms[roomName]before every Room property chain. - Keep missing vision distinct from an empty room.
- Do not use
Memory.roomsas current visibility. - Timestamp saved intel.
- Check
room.controllerbefore owner or reservation fields. - Resolve stored object IDs only after considering room visibility.
- Log state transitions across ticks for intermittent cases.
- Use an Observer or another vision source when current data is required.
Scope and next steps
This guide does not implement Observer scheduling, room-intel expiration policy, portals, shard travel, map status, or hostile-risk scoring. Continue with StructureObserver.observeRoom() timing and request state.
Frequently asked questions
Is an undefined Room a JavaScript error?
No. It is a normal state when the room is not available in the current tick. The error occurs only when code dereferences it without a guard.
Can old Memory fill in the live Room object?
No. Memory can provide your saved summary, not current game objects.
Should invisible mean safe?
No. Treat it as unknown.
What should request fresh visibility?
A Creep, owned vision-providing object, Observer workflow, or another applicable game mechanism must make the room available.