Quick answer
Call observer.observeRoom(targetRoom), save the target and current Game.time only when the result is OK, and inspect Game.rooms[targetRoom] on the next tick. Process the previous request before writing the new request. Save a small timestamped summary, not the live Room object.
Observer rules that affect the workflow
- An Observer becomes available at RCL 8.
- The base
OBSERVER_RANGEconstant is 10 rooms. OKmeans the operation was scheduled successfully.- The observed Room is available on the next tick.
- Invalid names, range, ownership, or inactive-structure conditions return error codes.
- Power effects can change Observer capability, so the method return value remains the final request result.
The scheduling result and the visibility result are separate states. Do not compress them into one boolean.
Request and result timeline
tick 200
read any request saved at tick 199
process Game.rooms[requestedRoom]
call observeRoom(nextRoom)
if result === OK, save requestedAt: 200
tick 201
read the request saved at tick 200
inspect Game.rooms[nextRoom]
then schedule another request
If the target is already visible on tick 200 for another reason, that same-tick Room object still does not prove the new observation completed immediately.
Store only an accepted request
function createAcceptedObservationState(
observer,
targetRoom,
result
) {
if (result !== OK) {
return null;
}
return {
observerId: observer.id,
observerRoom: observer.room.name,
requestedRoom: targetRoom,
requestedAt: Game.time
};
}
Writing request state after an error creates a false expectation on the next tick. The reader would report a missing result even though no observation was scheduled.
Read the previous request safely
function getObservationResult(state) {
if (
!state
|| typeof state.requestedRoom !== 'string'
|| !Number.isInteger(state.requestedAt)
) {
return {
status: 'none',
room: null
};
}
if (state.requestedAt === Game.time) {
return {
status: 'waiting',
room: null
};
}
if (state.requestedAt !== Game.time - 1) {
return {
status: 'expired',
room: null
};
}
const room = Game.rooms[state.requestedRoom];
return {
status: room ? 'visible' : 'missing',
room: room ?? null
};
}
| Status | Meaning |
|---|---|
none | No usable historical request |
waiting | The request belongs to the current tick |
visible | Requested last tick and the Room is available now |
missing | Requested last tick but no Room is available now |
expired | The state is older than the expected result window |
Save a bounded room-intel summary
Room objects, Creeps, Controllers, and Structures are live game objects. Save primitive fields and timestamps instead.
function summarizeObservedRoom(room) {
const controller = room.controller;
const mineral = room.find(FIND_MINERALS)[0] ?? null;
return {
roomName: room.name,
observedAt: Game.time,
controller: controller
? {
owner: controller.owner?.username ?? null,
reservation:
controller.reservation?.username ?? null,
level: controller.level
}
: null,
sourceCount: room.find(FIND_SOURCES).length,
mineralType: mineral?.mineralType ?? null,
hostileCount:
room.find(FIND_HOSTILE_CREEPS).length
};
}
function saveRoomIntel(summary) {
Memory.roomIntel ??= {};
Memory.roomIntel[summary.roomName] = summary;
}
This summary is intentionally small. Real combat or expansion planning may need Towers, Invader Cores, Nukes, deposits, room status, hostile combat parts, and confidence metadata.
Complete single-target Observer loop
State impact: this loop writes Observer request state and room intel. It may submit one observation request per tick. It does not move Creeps or modify the observed room.
function getObservationResult(state) {
if (
!state
|| typeof state.requestedRoom !== 'string'
|| !Number.isInteger(state.requestedAt)
) {
return { status: 'none', room: null };
}
if (state.requestedAt === Game.time) {
return { status: 'waiting', room: null };
}
if (state.requestedAt !== Game.time - 1) {
return { status: 'expired', room: null };
}
const room = Game.rooms[state.requestedRoom];
return {
status: room ? 'visible' : 'missing',
room: room ?? null
};
}
function summarizeObservedRoom(room) {
const controller = room.controller;
const mineral = room.find(FIND_MINERALS)[0] ?? null;
return {
roomName: room.name,
observedAt: Game.time,
controllerOwner:
controller?.owner?.username ?? null,
controllerReservation:
controller?.reservation?.username ?? null,
controllerLevel: controller?.level ?? 0,
sourceCount: room.find(FIND_SOURCES).length,
mineralType: mineral?.mineralType ?? null,
hostileCount:
room.find(FIND_HOSTILE_CREEPS).length
};
}
function processPreviousObservation() {
const result = getObservationResult(
Memory.observerState
);
if (result.status === 'visible') {
Memory.roomIntel ??= {};
const summary = summarizeObservedRoom(
result.room
);
Memory.roomIntel[summary.roomName] = summary;
}
if (
result.status === 'missing'
&& Game.time % 100 === 0
) {
console.log(JSON.stringify({
type: 'observer-result-missing',
requestedRoom:
Memory.observerState?.requestedRoom,
requestedAt:
Memory.observerState?.requestedAt
}));
}
return result.status;
}
function requestObservation(observer, targetRoom) {
const result = observer.observeRoom(targetRoom);
if (result === OK) {
Memory.observerState = {
observerId: observer.id,
observerRoom: observer.room.name,
requestedRoom: targetRoom,
requestedAt: Game.time
};
}
return result;
}
module.exports.loop = function () {
processPreviousObservation();
const config = Memory.observerConfig;
if (
!config
|| typeof config.observerId !== 'string'
|| typeof config.targetRoom !== 'string'
) {
return;
}
const observer = Game.getObjectById(
config.observerId
);
if (
!observer
|| observer.structureType !== STRUCTURE_OBSERVER
|| observer.my !== true
|| observer.isActive() !== true
) {
return;
}
const result = requestObservation(
observer,
config.targetRoom
);
if (result !== OK && Game.time % 100 === 0) {
console.log(JSON.stringify({
type: 'observer-request-failed',
observerId: observer.id,
observerRoom: observer.room.name,
targetRoom: config.targetRoom,
result
}));
}
};
The previous state is consumed before a new successful request can replace it. A production scheduler may also mark processed requests, preserve failure history, and avoid repeating an unchanged target every tick.
Visibility does not prove exclusive Observer attribution
The strongest statement this workflow supports is:
The room was requested on the previous tick and is available now.
A Creep, owned structure, another Observer, or another applicable vision source may also make the room visible. Do not write “Observer succeeded” merely because Game.rooms[targetRoom] exists without checking the stored request tick and the accepted return code.
Use range checks as diagnostics
function describeObserverDistance(
observer,
targetRoom
) {
const linearDistance =
Game.map.getRoomLinearDistance(
observer.room.name,
targetRoom
);
return {
observerRoom: observer.room.name,
targetRoom,
linearDistance,
baseRange: OBSERVER_RANGE,
insideBaseRange:
linearDistance <= OBSERVER_RANGE
};
}
This is a diagnostic, not permission to skip the method result. Power effects and invalid configuration still require the actual observeRoom() return code.
Schedule multiple rooms with one request per tick
function getNextQueueItem(queue, index) {
if (!Array.isArray(queue) || queue.length === 0) {
return null;
}
const safeIndex = Number.isInteger(index)
? Math.abs(index) % queue.length
: 0;
return {
roomName: queue[safeIndex],
index: safeIndex
};
}
function advanceQueueIndex(index, length) {
if (!Number.isInteger(length) || length <= 0) {
return 0;
}
return (index + 1) % length;
}
Advance the index only after an accepted request. A complete scheduler also needs priorities, retry delays, Observer-to-target assignment, stale-intel deadlines, and explicit behavior for rooms outside base range.
Handle Observer return codes
| Return code | Interpretation | Next action |
|---|---|---|
OK | Request scheduled | Save target and current tick |
ERR_NOT_OWNER | Observer is not yours | Check ID and ownership |
ERR_NOT_IN_RANGE | Target is outside current capability | Check room names, distance, and effects |
ERR_INVALID_ARGS | Target room name is invalid | Validate configuration |
ERR_RCL_NOT_ENOUGH | Structure is not active at the current RCL | Check Controller level and isActive() |
Do not record a successful request state for any error code.
Expire old intel deliberately
function readFreshIntel(roomName, maxAge) {
const intel = Memory.roomIntel?.[roomName];
if (
!intel
|| !Number.isInteger(intel.observedAt)
|| !Number.isInteger(maxAge)
|| maxAge < 0
) {
return null;
}
return Game.time - intel.observedAt <= maxAge
? intel
: null;
}
The expiration threshold is a business policy. Combat routing may require much fresher data than mineral cataloging.
Debugging checklist
- Validate the Observer ID and target room string.
- Recover the structure with
Game.getObjectById(). - Check structure type, ownership, and
isActive(). - Save the exact
observeRoom()return code. - Write request state only after
OK. - Process the previous request before scheduling the next one.
- Require
requestedAt === Game.time - 1for the normal result window. - Save summaries and timestamps, not live Room objects.
- Do not equate missing vision with a safe room.
- Do not claim exclusive Observer attribution without additional evidence.
Scope and next steps
This guide does not implement multi-Observer assignment, OPERATE_OBSERVER scheduling, portals, shard reconnaissance, hostile-player reputation, confidence scoring, or compressed long-term intel. Continue with PathFinder CostMatrix construction to use visible room structures in tile-level routing.
Frequently asked questions
Does OK mean the Room exists now?
No. It means the request was scheduled.
Why read before scheduling?
Otherwise a new request can overwrite the target and tick needed to process the previous result.
Can already-visible rooms be skipped?
Yes as a scheduler policy, but the API does not require that policy.
How long is intel valid?
The game does not choose that for your application. Store observedAt and define a use-specific age limit.