Quick answer
Store one reviewed request containing a room, X, Y and STRUCTURE_ROAD. Require a visible room, integer coordinates from 0 through 49, no existing Road, no existing Construction Site, and a current site count below MAX_CONSTRUCTION_SITES. Record wall terrain instead of rejecting it, set request.enabled = false, call room.createConstructionSite() once, save the return code, and inspect the coordinate again on a later tick.
A Construction Site is not a completed Road
Room.createConstructionSite() creates a planning object. It does not produce a completed Structure and does not spend Builder Energy by itself.
| Stage | Object | Action |
|---|---|---|
| Planned | ConstructionSite | Create or remove the site |
| Under construction | ConstructionSite | Creeps call build() |
| Completed | StructureRoad | Use, repair or destroy the Structure |
Do not reject natural wall terrain for Roads
function inspectRoadTerrain(room, x, y) {
const terrain = room.getTerrain().get(x, y);
return {
terrain,
onNaturalWall:
terrain === TERRAIN_MASK_WALL
};
}
Roads are allowed on natural wall terrain, with different construction cost behavior. That does not mean a Spawn, Extension or arbitrary Structure can use the same placement rule.
Create a one-time Road request
Memory.constructionSiteRequest = {
enabled: true,
roomName: 'W1N1',
x: 20,
y: 20,
structureType: STRUCTURE_ROAD
};
The narrow structure type prevents a single-coordinate example from becoming an unreviewed generic blueprint engine.
Build a testable placement plan
function evaluateRoadSiteRequest(input) {
const request = input.request;
if (!request || request.enabled !== true) {
return { ready: false, reason: 'disabled' };
}
if (
typeof request.roomName !== 'string'
|| !Number.isInteger(request.x)
|| !Number.isInteger(request.y)
|| request.x < 0
|| request.x > 49
|| request.y < 0
|| request.y > 49
|| request.structureType !== STRUCTURE_ROAD
) {
return { ready: false, reason: 'invalid-request' };
}
if (!input.roomVisible) {
return { ready: false, reason: 'room-not-visible' };
}
if (input.hasRoad) {
return { ready: false, reason: 'road-exists' };
}
if (input.hasSite) {
return { ready: false, reason: 'site-exists' };
}
if (
!Number.isInteger(input.siteCount)
|| input.siteCount >= MAX_CONSTRUCTION_SITES
) {
return { ready: false, reason: 'site-limit' };
}
return { ready: true, reason: 'ready' };
}
This preflight catches obvious stale inputs. It cannot reproduce every Controller, edge, terrain-overlay and structure-count rule, so the API result remains authoritative.
Complete Road site handler
function inspectRoadSiteTile(room, x, y) {
const structures = room.lookForAt(
LOOK_STRUCTURES,
x,
y
);
const sites = room.lookForAt(
LOOK_CONSTRUCTION_SITES,
x,
y
);
const terrain = inspectRoadTerrain(room, x, y);
return {
hasRoad: structures.some(structure =>
structure.structureType === STRUCTURE_ROAD
),
hasSite: sites.length > 0,
onNaturalWall: terrain.onNaturalWall
};
}
function handleRoadSiteRequest() {
const request = Memory.constructionSiteRequest;
if (!request || request.enabled !== true) {
return { status: 'disabled' };
}
const coordinatesValid =
Number.isInteger(request.x)
&& Number.isInteger(request.y)
&& request.x >= 0
&& request.x <= 49
&& request.y >= 0
&& request.y <= 49;
const room = typeof request.roomName === 'string'
? Game.rooms[request.roomName]
: null;
const tile = room && coordinatesValid
? inspectRoadSiteTile(room, request.x, request.y)
: {
hasRoad: false,
hasSite: false,
onNaturalWall: null
};
const siteCount = Object.keys(
Game.constructionSites
).length;
const plan = evaluateRoadSiteRequest({
request,
roomVisible: Boolean(room),
hasRoad: tile.hasRoad,
hasSite: tile.hasSite,
siteCount
});
request.checkedAt = Game.time;
request.status = plan.reason;
if (!plan.ready) {
request.enabled = false;
return { status: plan.reason };
}
request.enabled = false;
request.status = 'submitted';
request.submittedAt = Game.time;
request.before = {
roomName: room.name,
x: request.x,
y: request.y,
structureType: STRUCTURE_ROAD,
onNaturalWall: tile.onNaturalWall,
siteCount
};
const result = room.createConstructionSite(
request.x,
request.y,
STRUCTURE_ROAD
);
request.result = result;
request.resultAt = Game.time;
request.status = result === OK
? 'accepted'
: 'failed-review-required';
return {
status: request.status,
result,
before: request.before
};
}
module.exports.loop = function () {
const outcome = handleRoadSiteRequest();
if (
outcome.status === 'accepted'
|| outcome.status === 'failed-review-required'
) {
console.log(JSON.stringify({
type: 'road-site-request-result',
...outcome
}));
}
};
Disable stale and submitted requests
The handler disables preflight failures because a request that is invalid now should not silently execute when visibility, site count or tile occupancy changes later. It also disables immediately before the API call to prevent repetition after an exception.
request.enabled = false;
const result = room.createConstructionSite(
request.x,
request.y,
STRUCTURE_ROAD
);
Respect the current site limit
const mySiteCount = Object.keys(
Game.constructionSites
).length;
const capacityRemaining = Math.max(
0,
MAX_CONSTRUCTION_SITES - mySiteCount
);
The local count is diagnostic. Handle ERR_FULL even after preflight because current rules and simultaneous actions can still reject placement.
Verify the next tick
function verifyRoadSite(request) {
const room = Game.rooms[request.roomName];
if (!room) {
return { verified: false, reason: 'room-not-visible' };
}
const site = room.lookForAt(
LOOK_CONSTRUCTION_SITES,
request.x,
request.y
).find(item =>
item.structureType === STRUCTURE_ROAD
) || null;
return site
? { verified: true, siteId: site.id }
: { verified: false, reason: 'site-not-observed' };
}
Only a later matching object proves the site became observable. A Builder and Energy supply are still required for completion.
Handle return codes
| Code | Typical meaning | Review |
|---|---|---|
OK | Site creation scheduled | Inspect the tile later |
ERR_NOT_OWNER | Controller state disallows placement | Owner and reservation |
ERR_INVALID_TARGET | Tile cannot accept the site | Terrain and existing objects |
ERR_FULL | Construction Site limit reached | Game.constructionSites |
ERR_INVALID_ARGS | Coordinates or type invalid | 0–49 integers and Road constant |
ERR_RCL_NOT_ENOUGH | Controller or structure limit | RCL and room structures |
Debugging checklist
- Require a one-time enabled request.
- Allow only
STRUCTURE_ROAD. - Validate 0–49 integer coordinates.
- Require current room visibility.
- Check existing structures and sites.
- Record natural wall terrain without rejecting it.
- Check
MAX_CONSTRUCTION_SITES. - Disable stale requests.
- Save the official return code.
- Verify a matching site later.
Scope and next steps
This guide does not place a path, validate a full room blueprint, create arbitrary Structures, manage site batches, assign Builders or remove old layouts. Continue with Construction Site progress reporting.
Frequently asked questions
Why use MAX_CONSTRUCTION_SITES instead of only 100?
The named game constant makes the code's dependency explicit. The API return remains the final authority.
Why reject any existing site instead of only an existing Road site?
A different site on the same coordinate is a layout conflict that needs review rather than automatic replacement.
Can the request stay enabled until visibility returns?
This guide deliberately does not do that because the old coordinate may no longer reflect current player intent.