Separate mission choice from operation proof
reserveController() and claimController() solve different strategic goals, but both need the same operational discipline. The mission must identify one Creep, one Controller and one action before submission. An OK return value means the current intent was accepted; it does not by itself prove the next processed state.
| Action | Game-state result | Strongest next-tick evidence |
|---|---|---|
reserveController() | The Controller remains neutral and its reservation time increases | The exact Claimer's EVENT_RESERVE_CONTROLLER plus the exact room Controller's reservation state |
claimController() | The exact neutral Controller becomes owned at level 1 | The exact Controller ID reports your username and my === true |
The official engine records a reserve event with the acting Creep as objectId and the added reservation amount. That event does not include a Controller targetId. A claim produces no Room event, so do not invent one.
Resolve the exact Claimer and Controller
function resolveControllerMission(creep, mission) {
if (!creep || creep.my !== true || creep.spawning === true) {
return { ready: false, status: 'claimer-unavailable' };
}
if (creep.getActiveBodyparts(CLAIM) <= 0) {
return { ready: false, status: 'no-active-claim-part' };
}
const controller = creep.room.controller || null;
if (!controller) {
return { ready: false, status: 'controller-missing' };
}
if (
mission.roomName !== creep.room.name
|| mission.controllerId !== controller.id
) {
return {
ready: false,
status: 'mission-identity-mismatch',
observedControllerId: controller.id
};
}
return { ready: true, status: 'identity-ready', controller };
}
Store the Controller ID after the room is scouted and reviewed. Do not accept whichever Controller happens to be visible when a mission runs. The room name is useful for routing, while the ID prevents a stale or corrupted mission from silently targeting another object.
Evaluate reserve and claim rules
function countOwnedRooms() {
return Object.values(Game.rooms)
.filter(room => room.controller?.my === true)
.length;
}
function evaluateControllerAction(creep, controller, mission) {
if (!['reserve', 'claim'].includes(mission.action)) {
return { ready: false, status: 'invalid-action' };
}
if (controller.owner) {
return { ready: false, status: 'controller-already-owned' };
}
const username = creep.owner.username;
const reservationUsername =
controller.reservation?.username ?? null;
if (
reservationUsername
&& reservationUsername !== username
) {
return { ready: false, status: 'hostile-reservation' };
}
if (mission.action === 'claim') {
const expectedConfirmation =
'CLAIM_' + controller.pos.roomName + '_' + controller.id;
if (mission.confirmation !== expectedConfirmation) {
return { ready: false, status: 'claim-confirmation-mismatch' };
}
if (countOwnedRooms() >= Game.gcl.level) {
return { ready: false, status: 'gcl-capacity-unavailable' };
}
}
if (!creep.pos.inRangeTo(controller, 1)) {
return { ready: false, status: 'move-to-controller' };
}
return { ready: true, status: 'action-ready' };
}
The confirmation string is a project safeguard, not an API requirement. It binds a permanent claim to the reviewed room and Controller ID. Reservation remains renewable; claiming is a one-time ownership transition.
Record only an accepted Controller action
function submitControllerMission(creep) {
const mission = Memory.controllerMissions?.[creep?.name];
if (!mission || mission.enabled !== true) {
return { status: 'mission-disabled' };
}
const resolved = resolveControllerMission(creep, mission);
if (!resolved.ready) return resolved;
const controller = resolved.controller;
const decision = evaluateControllerAction(
creep,
controller,
mission
);
if (decision.status === 'move-to-controller') {
return {
status: 'moving',
moveResult: creep.moveTo(controller, {
range: 1,
reusePath: 10
})
};
}
if (!decision.ready) return decision;
Memory.pendingControllerOperations ??= {};
const existing =
Memory.pendingControllerOperations[creep.name];
if (existing && existing.submittedAt >= Game.time - 1) {
return { status: 'pending-verification' };
}
const before = {
ownerUsername: controller.owner?.username ?? null,
reservationUsername:
controller.reservation?.username ?? null,
reservationTicks:
controller.reservation?.ticksToEnd ?? 0,
ownedRoomCount: countOwnedRooms()
};
const result = mission.action === 'reserve'
? creep.reserveController(controller)
: creep.claimController(controller);
mission.lastResult = result;
mission.lastResultAt = Game.time;
if (result !== OK) {
return {
status: 'controller-action-rejected',
action: mission.action,
result
};
}
Memory.pendingControllerOperations[creep.name] = {
submittedAt: Game.time,
action: mission.action,
creepId: creep.id,
creepName: creep.name,
username: creep.owner.username,
roomName: creep.room.name,
controllerId: controller.id,
before
};
mission.lastStatus = 'accepted-awaiting-verification';
if (mission.action === 'claim') {
mission.enabled = false;
mission.completion = 'pending-owner-verification';
}
return {
status: 'controller-action-accepted',
action: mission.action,
result,
controllerId: controller.id
};
}
Disabling an accepted claim prevents another call, but it must not be labeled completed until ownership is observed. A reserve mission can remain enabled, although the pending record blocks another submission until the one-tick evidence window is consumed.
Verify reservation through the exact event
function verifyReserveOperation(pending) {
if (Game.time <= pending.submittedAt) {
return { status: 'wait-for-next-tick' };
}
if (Game.time > pending.submittedAt + 1) {
return { status: 'reserve-event-window-missed' };
}
const room = Game.rooms[pending.roomName];
if (!room) return { status: 'room-not-visible' };
const controller = room.controller;
if (!controller || controller.id !== pending.controllerId) {
return { status: 'controller-identity-mismatch' };
}
const matches = room.getEventLog().filter(event =>
event.event === EVENT_RESERVE_CONTROLLER
&& event.objectId === pending.creepId
);
if (matches.length === 0) {
return { status: 'accepted-reserve-event-missing' };
}
if (matches.length > 1) {
return {
status: 'reserve-event-ambiguous',
count: matches.length
};
}
const reservation = controller.reservation;
const usernameMatches =
reservation?.username === pending.username;
return {
status: usernameMatches
? 'reserve-event-and-state-observed'
: 'reserve-event-state-mismatch',
amount: matches[0].data?.amount ?? null,
controllerId: controller.id,
reservationUsername:
reservation?.username ?? null,
reservationTicks:
reservation?.ticksToEnd ?? 0,
beforeTicks: pending.before.reservationTicks
};
}
The event identifies the Claimer, not the Controller target. The verifier therefore combines three facts: it reads the event from the pending room, confirms that room's Controller ID, and checks the reservation username. The event amount is operation output; a current ticksToEnd value is processed state and should not be forced to equal a simple before-plus-amount calculation.
Verify claiming through exact ownership
function verifyClaimOperation(pending) {
if (Game.time <= pending.submittedAt) {
return { status: 'wait-for-next-tick' };
}
const room = Game.rooms[pending.roomName];
if (!room) return { status: 'claimed-room-not-visible' };
const controller = room.controller;
if (!controller || controller.id !== pending.controllerId) {
return { status: 'controller-identity-mismatch' };
}
const ownerMatches =
controller.owner?.username === pending.username;
return {
status: ownerMatches && controller.my === true
? 'claim-owner-observed'
: 'accepted-claim-owner-not-observed',
controllerId: controller.id,
ownerUsername: controller.owner?.username ?? null,
controllerMy: controller.my === true,
controllerLevel: controller.level,
ownedRoomCountNow: countOwnedRooms(),
ownedRoomCountBefore:
pending.before.ownedRoomCount
};
}
function verifyPendingControllerOperation(pending) {
if (!pending) return { status: 'no-pending-operation' };
return pending.action === 'reserve'
? verifyReserveOperation(pending)
: verifyClaimOperation(pending);
}
claimController() has no Room event in the official engine. Exact ownership on the saved Controller is therefore the primary evidence. The account's owned-room count is corroboration only; another room could be claimed or lost during the same period.
Keep incomplete evidence visible
| Status | Meaning | Response |
|---|---|---|
mission-identity-mismatch | The visible Controller is not the reviewed object | Stop and repair mission data |
hostile-reservation | Another username owns the reservation | Do not auto-attack or auto-claim |
accepted-reserve-event-missing | An accepted reserve lacks the exact Claimer event | Preserve the discrepancy |
reserve-event-window-missed | The one-tick Room event window was not read | Record the missed sample |
accepted-claim-owner-not-observed | The accepted claim did not produce exact ownership evidence | Keep the mission disabled for review |
claimed-room-not-visible | The expected owned room is not currently visible | Do not infer completion from GCL alone |
Production integration boundary
Run one Controller mission dispatcher before role code can submit another Controller action. Consume pending verification before allowing a new reserve call, and keep accepted claims disabled until exact ownership is observed. Console execution, live hostile-reservation behavior, shard-specific timing and genuine screenshots remain pending.
Official documentation
Review the official Screeps API for Creep.reserveController(), Creep.claimController(), Room.getEventLog(), EVENT_RESERVE_CONTROLLER, Controller ownership and reservation fields, GCL, range and return codes before adapting the examples.