VISION · OBSERVER REQUEST LIFECYCLE

How to Use StructureObserver.observeRoom() Safely

Schedule Observer vision, store the accepted request, read the target Room on the next tick, preserve bounded intel, handle return codes, and avoid claiming that current visibility proves the new request completed.

Verification statusChinese source article: Reviewed in full · Official docs: Checked — observeRoom(), OBSERVER_RANGE, return codes, Game.rooms and getRoomLinearDistance() · Timing boundary: OK schedules observation; the requested Room is evaluated on the next tick

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official docs
Checked — observeRoom(), OBSERVER_RANGE, return codes, Game.rooms and getRoomLinearDistance()
Timing boundary
OK schedules observation; the requested Room is evaluated on the next tick
Attribution boundary
A visible target is not claimed to be visible exclusively because of the Observer
JavaScript syntax
Passed
Offline request-state review
Passed — none, waiting, visible, missing, expired, accepted and rejected requests
Screeps Console test
Pending
Live Observer and multi-tick intel test
Pending
Last verified
July 25, 2026

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_RANGE constant is 10 rooms.
  • OK means 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
  };
}
StatusMeaning
noneNo usable historical request
waitingThe request belongs to the current tick
visibleRequested last tick and the Room is available now
missingRequested last tick but no Room is available now
expiredThe 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 codeInterpretationNext action
OKRequest scheduledSave target and current tick
ERR_NOT_OWNERObserver is not yoursCheck ID and ownership
ERR_NOT_IN_RANGETarget is outside current capabilityCheck room names, distance, and effects
ERR_INVALID_ARGSTarget room name is invalidValidate configuration
ERR_RCL_NOT_ENOUGHStructure is not active at the current RCLCheck 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 - 1 for 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.

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.