src/room-intel.ts
/* global PathFinder Room RoomPosition
STRUCTURE_KEEPER_LAIR STRUCTURE_CONTROLLER CONTROLLER_DOWNGRADE FIND_SOURCES
TERRAIN_MASK_WALL TERRAIN_MASK_SWAMP POWER_BANK_DECAY STRUCTURE_PORTAL
STRUCTURE_POWER_BANK FIND_MY_CONSTRUCTION_SITES STRUCTURE_STORAGE
STRUCTURE_TERMINAL FIND_RUINS STRUCTURE_INVADER_CORE EFFECT_COLLAPSE_TIMER */
import cache from 'utils/cache';
import hivemind from 'hivemind';
import interShard from 'intershard';
import NavMesh from 'utils/nav-mesh';
import {deserializeCoords, serializeCoords, serializePosition} from 'utils/serialization';
import {getUsername} from 'utils/account';
import {handleMapArea} from 'utils/map';
import {markBuildings} from 'utils/cost-matrix';
import {packCoord, packCoordList, unpackCoordList, unpackCoordListAsPosList} from 'utils/packrat';
declare global {
interface RoomMemory {
abandonedResources?: Record<string, Record<string, number>>;
}
interface DepositInfo {
x: number;
y: number;
id: Id<Deposit>;
type: DepositConstant;
decays: number;
cooldown: number;
freeTiles: number;
}
namespace NodeJS {
interface Global {
getRoomIntel: typeof getRoomIntel;
}
}
}
export interface RoomIntelMemory {
lastScan: number;
exits: Partial<Record<ExitKey, string>>;
rcl: number;
ticksToDowngrade: number;
hasController: boolean;
owner: string;
reservation: {
username: string;
ticksToEnd: number;
};
sources: Array<{
x: number;
y: number;
id: Id<Source>;
free: number;
}>;
// @todo Deprecated, remove later! Use `minerals` instead.
mineralInfo: Record<string, unknown>;
minerals: Array<{
x: number;
y: number;
id: Id<Mineral>;
type: MineralConstant;
amount: number;
}>;
power: {
amount: number;
hits: number;
decays: number;
freeTiles: number;
pos: string;
};
deposits?: DepositInfo[];
structures: {
[T in StructureConstant]?: Record<string, {
x: number;
y: number;
hits: number;
hitsMax: number;
}>;
};
portals?: string[];
terrain: {
exit: number;
wall: number;
swamp: number;
plain: number;
};
invaderInfo: {
level: number;
active: boolean;
activates: number;
collapses: number;
};
costPositions: [string, string];
lastScout: number;
}
export default class RoomIntel {
roomName: string;
memory: RoomIntelMemory;
newStatus: Record<string, boolean>;
otherSafeRooms: string[];
otherUnsafeRooms: string[];
joinedDirs: Record<string, Record<string, boolean>>;
constructor(roomName) {
this.roomName = roomName;
const key = 'intel:' + roomName;
if (!hivemind.segmentMemory.has(key)) {
hivemind.segmentMemory.set(key, {});
}
this.memory = hivemind.segmentMemory.get(key);
}
/**
* Updates intel for a room.
*/
gatherIntel() {
const room = Game.rooms[this.roomName];
if (!room) return;
const intel = this.memory;
const isNew = !intel.lastScan;
this.registerScoutAttempt();
let lastScanThreshold = hivemind.settings.get('roomIntelCacheDuration');
if (Game.cpu.bucket < 5000) {
lastScanThreshold *= 5;
}
if (intel.lastScan && !hivemind.hasIntervalPassed(lastScanThreshold, intel.lastScan)) return;
hivemind.log('intel', room.name).debug('Gathering intel after', intel.lastScan ? Game.time - intel.lastScan : 'infinite', 'ticks.');
intel.lastScan = Game.time;
this.gatherControllerIntel(room);
this.gatherResourceIntel(room);
const structures = room.structuresByType;
this.gatherPowerIntel(structures[STRUCTURE_POWER_BANK] as StructurePowerBank[]);
this.gatherDepositIntel();
this.gatherPortalIntel(structures[STRUCTURE_PORTAL] as StructurePortal[]);
this.gatherInvaderIntel(structures);
this.gatherExitIntel(room.name);
if (isNew) {
this.gatherTerrainIntel();
this.gatherStructureIntel(structures, STRUCTURE_KEEPER_LAIR);
this.gatherStructureIntel(structures, STRUCTURE_CONTROLLER);
}
const ruins = room.find(FIND_RUINS);
this.gatherAbandonedResourcesIntel(structures, ruins);
// At the same time, create a PathFinder CostMatrix to use when pathfinding through this room.
let constructionSites = _.groupBy(room.find(FIND_MY_CONSTRUCTION_SITES), 'structureType');
if (room.controller && !room.controller.my && room.controller.owner && hivemind.relations.isAlly(room.controller.owner.username)) {
constructionSites = _.groupBy(room.find(FIND_CONSTRUCTION_SITES, {
filter: site => site.my || hivemind.relations.isAlly(site.owner.username),
}), 'structureType');
}
this.gatherPathfindingInfo(structures, constructionSites);
// Update nav mesh for this room.
const mesh = new NavMesh();
mesh.generateForRoom(this.roomName);
}
/**
* Commits controller status to memory.
*
* @param {Room} room
* The room to gather controller intel on.
*/
gatherControllerIntel(room: Room) {
this.memory.owner = null;
this.memory.rcl = 0;
this.memory.ticksToDowngrade = 0;
this.memory.hasController = typeof room.controller !== 'undefined';
if (room.controller && room.controller.owner) {
this.memory.owner = room.controller.owner.username;
this.memory.rcl = room.controller.level;
this.memory.ticksToDowngrade = room.controller.ticksToDowngrade;
}
if (!room.controller) {
const invaderCores = room.structuresByType[STRUCTURE_INVADER_CORE] as StructureInvaderCore[];
if (invaderCores && invaderCores.length > 0 && invaderCores[0].level) {
this.memory.owner = invaderCores[0].owner.username;
this.memory.rcl = invaderCores[0].level;
this.memory.ticksToDowngrade = 0;
}
}
this.memory.reservation = room.controller ? room.controller.reservation : {
username: null,
ticksToEnd: 0,
};
}
/**
* Commits room resources to memory.
*
* @param {Room} room
* The room to gather resource intel on.
*/
gatherResourceIntel(room: Room) {
// Check sources.
this.memory.sources = _.map(
room.find(FIND_SOURCES),
source => ({
x: source.pos.x,
y: source.pos.y,
id: source.id,
free: source.getNumHarvestSpots(),
}),
);
// Check minerals.
this.memory.minerals = [];
for (const mineral of room.minerals) {
this.memory.minerals.push({
x: mineral.pos.x,
y: mineral.pos.y,
id: mineral.id,
type: mineral.mineralType,
amount: mineral.mineralAmount,
});
}
}
/**
* Commits basic terrain metrics to memory.
*/
gatherTerrainIntel() {
// Check terrain.
this.memory.terrain = {
exit: 0,
wall: 0,
swamp: 0,
plain: 0,
};
const terrain = new Room.Terrain(this.roomName);
for (let x = 0; x < 50; x++) {
for (let y = 0; y < 50; y++) {
const tileType = terrain.get(x, y);
// Check border tiles.
if (x === 0 || y === 0 || x === 49 || y === 49) {
if (tileType !== TERRAIN_MASK_WALL) {
this.memory.terrain.exit++;
}
continue;
}
// Check non-border tiles.
switch (tileType) {
case TERRAIN_MASK_WALL:
this.memory.terrain.wall++;
break;
case TERRAIN_MASK_SWAMP:
this.memory.terrain.swamp++;
break;
default:
this.memory.terrain.plain++;
}
}
}
}
/**
* Commits power bank status to memory.
*
* @param {Structure[]} powerBanks
* An array containing all power banks for the room.
*/
gatherPowerIntel(powerBanks: StructurePowerBank[]) {
delete this.memory.power;
const powerBank: StructurePowerBank = _.first(powerBanks);
if (!powerBank || powerBank.hits === 0 || powerBank.power === 0) return;
// For now, send a notification!
hivemind.log('intel', this.roomName).info('Power bank containing', powerBank.power, 'power found!');
// Find out how many access points there are around this power bank.
const terrain = new Room.Terrain(this.roomName);
let numberFreeTiles = 0;
handleMapArea(powerBank.pos.x, powerBank.pos.y, (x, y) => {
if (terrain.get(x, y) !== TERRAIN_MASK_WALL) {
numberFreeTiles++;
}
});
this.memory.power = {
amount: powerBank.power,
hits: powerBank.hits,
decays: Game.time + (powerBank.ticksToDecay || POWER_BANK_DECAY),
freeTiles: numberFreeTiles,
pos: packCoord({x: powerBank.pos.x, y: powerBank.pos.y}),
};
// Also store room in strategy memory for easy access.
if (Memory.strategy) {
if (!Memory.strategy.power) {
Memory.strategy.power = {rooms: {}};
}
if (!Memory.strategy.power.rooms) {
Memory.strategy.power.rooms = {};
}
if (!Memory.strategy.power.rooms[this.roomName] || !Memory.strategy.power.rooms[this.roomName].isActive) {
Memory.strategy.power.rooms[this.roomName] = this.memory.power;
// @todo Update info when gathering is active.
}
}
}
gatherDepositIntel() {
delete this.memory.deposits;
const room = Game.rooms[this.roomName];
const deposits = room.find(FIND_DEPOSITS);
const maxCooldown = hivemind.settings.get('maxDepositCooldown');
if (deposits.length === 0) return;
const terrain = new Room.Terrain(this.roomName);
this.memory.deposits = [];
for (const deposit of deposits) {
if (!deposit || deposit.lastCooldown > maxCooldown) return;
// For now, send a notification!
hivemind.log('intel', this.roomName).info('Deposit containing', deposit.depositType, 'found!');
// Find out how many access points there are around this power bank.
let numberFreeTiles = 0;
handleMapArea(deposit.pos.x, deposit.pos.y, (x, y) => {
if (terrain.get(x, y) !== TERRAIN_MASK_WALL) {
numberFreeTiles++;
}
});
this.memory.deposits.push({
x: deposit.pos.x,
y: deposit.pos.y,
id: deposit.id,
type: deposit.depositType,
decays: Game.time + deposit.ticksToDecay,
cooldown: deposit.lastCooldown || 0,
freeTiles: numberFreeTiles,
});
// Also store room in strategy memory for easy access.
if (Memory.strategy) {
if (!Memory.strategy.deposits) {
Memory.strategy.deposits = {rooms: {}};
}
if (!Memory.strategy.deposits.rooms) {
Memory.strategy.deposits.rooms = {};
}
if (!Memory.strategy.deposits.rooms[this.roomName] || !Memory.strategy.deposits.rooms[this.roomName].isActive) {
Memory.strategy.deposits.rooms[this.roomName] = {scouted: true};
// @todo Update info when gathering is active.
}
}
}
}
/**
* Commits portal status to memory.
*
* @param {Structure[]} portals
* An array containing all power banks for the room.
*/
gatherPortalIntel(portals: StructurePortal[]) {
delete this.memory.portals;
const targetRooms = [];
for (const portal of portals || []) {
// Ignore same-shard portals for now.
if ('shard' in portal.destination) {
interShard.registerPortal(portal);
continue;
}
if (!targetRooms.includes(portal.destination.roomName)) targetRooms.push(portal.destination.roomName);
}
if (targetRooms.length > 0) {
this.memory.portals = targetRooms;
}
}
getRoomPortals(): string[] {
return this.memory.portals ?? [];
}
/**
* Commits structure status to memory.
*
* @param {object} structures
* An object containing Arrays of structures, keyed by structure type.
* @param {string} structureType
* The type of structure to gather intel on.
*/
gatherStructureIntel(structures: Record<string, Structure[]>, structureType: StructureConstant) {
if (!this.memory.structures) this.memory.structures = {};
this.memory.structures[structureType] = {};
for (const structure of structures[structureType] || []) {
this.memory.structures[structureType][structure.id] = {
x: structure.pos.x,
y: structure.pos.y,
hits: structure.hits,
hitsMax: structure.hitsMax,
};
}
}
/**
* Commits abandoned resources to memory.
*
* @param {object} structures
* An object containing Arrays of structures, keyed by structure type.
* @param {object[]} ruins
* An array of Ruin objects.
*/
gatherAbandonedResourcesIntel(structures: Record<string, Structure[]>, ruins: Ruin[]) {
// Find origin room.
if (!Memory.strategy) return;
if (!Memory.strategy.roomList) return;
const strategyInfo = Memory.strategy.roomList[this.roomName];
if (!strategyInfo || !strategyInfo.origin) return;
const roomMemory = Memory.rooms[strategyInfo.origin];
if (!roomMemory) return;
if (!Game.rooms[strategyInfo.origin] || !Game.rooms[strategyInfo.origin].isMine()) {
delete roomMemory.abandonedResources;
return;
}
if (!roomMemory.abandonedResources) roomMemory.abandonedResources = {};
delete roomMemory.abandonedResources[this.roomName];
if (this.memory.owner) return;
if (!structures[STRUCTURE_STORAGE] && !structures[STRUCTURE_TERMINAL] && ruins.length === 0) return;
const resources = {};
const collections = [structures[STRUCTURE_STORAGE], structures[STRUCTURE_TERMINAL], ruins];
_.each(collections, objects => {
_.each(objects, object => {
if (!object.store) return;
_.each(object.store, (amount, resourceType) => {
resources[resourceType] = (resources[resourceType] || 0) + amount;
});
});
});
if (_.keys(resources).length === 0) return;
roomMemory.abandonedResources[this.roomName] = resources;
// @todo Consider resources from buildings that might need dismantling first.
// @todo Also consider saving containers with resources if it's not one
// of our harvest rooms, so we can "borrow" from other players.
}
gatherExitIntel(roomName: string) {
// Remember room exits.
this.memory.exits = Game.map.describeExits(roomName);
for (const dir in this.memory.exits) {
if (!this.isAvailableExitDirection(roomName, this.memory.exits[dir])) delete this.memory.exits[dir];
}
}
isAvailableExitDirection(roomName: string, otherRoomName: string): boolean {
return Game.map.getRoomStatus(otherRoomName).status === Game.map.getRoomStatus(roomName).status;
}
/**
* Commits info about invader outposts to memory.
*
* @param {object} structures
* An object containing Arrays of structures, keyed by structure type.
*/
gatherInvaderIntel(structures) {
delete this.memory.invaderInfo;
const core: StructureInvaderCore = _.first(structures[STRUCTURE_INVADER_CORE]);
if (!core) return;
// Commit basic invader core info.
this.memory.invaderInfo = {
level: core.level,
active: !core.ticksToDeploy,
activates: core.ticksToDeploy ? Game.time + core.ticksToDeploy : undefined,
collapses: null,
};
// Check when the core collapses.
for (const effect of core.effects) {
if (effect.effect === EFFECT_COLLAPSE_TIMER) {
this.memory.invaderInfo.collapses = Game.time + effect.ticksRemaining;
}
}
}
/**
* Commits pathfinding matrix to memory.
*
* @param {object} structures
* An object containing Arrays of structures, keyed by structure type.
* @param {object} constructionSites
* An object containing Arrays of construction sites, keyed by structure type.
*/
gatherPathfindingInfo(structures, constructionSites) {
const obstaclePositions = this.generateObstacleList(this.roomName, structures, constructionSites);
this.memory.costPositions = [
packCoordList(_.map(obstaclePositions.obstacles, deserializeCoords)),
packCoordList(_.map(obstaclePositions.roads, deserializeCoords)),
];
}
/**
* Generates an obstacle list as an alternative to cost matrixes.
*
* @param {string} roomName
* Name of the room to generate an obstacle list for.
* @param {object} structures
* Arrays of structures to navigate around, keyed by structure type.
* @param {object} constructionSites
* Arrays of construction sites to navigate around, keyed by structure type.
*
* @return {object}
* An object containing encoded room positions in the following keys:
* - obstacles: Any positions a creep cannot move through.
* - roads: Any positions where a creep travels with road speed.
*/
generateObstacleList(roomName, structures, constructionSites) {
const result = {
obstacles: [],
roads: [],
};
markBuildings(
roomName,
structures,
constructionSites,
structure => {
const location = serializeCoords(structure.pos.x, structure.pos.y);
if (!_.contains(result.obstacles, location)) {
result.roads.push(location);
}
},
structure => result.obstacles.push(serializePosition(structure.pos, roomName)),
(x, y) => {
const location = serializeCoords(x, y);
if (!_.contains(result.obstacles, location)) {
result.obstacles.push(location);
}
},
);
return result;
}
/**
* Gets coordinates of all known roads in the room.
*/
getRoadCoords(): Array<{x: number; y: number}> {
if (!this.memory.costPositions) return [];
return unpackCoordList(this.memory.costPositions[1]);
}
/**
* Returns number of ticks since intel on this room was last gathered.
*
* @return {number}
* Number of ticks since intel was last gathered in this room.
*/
getAge(): number {
return Game.time - (this.memory.lastScan || -100_000);
}
/**
* Checks whether this room could be claimed by a player.
*
* @return {boolean}
* True if the room has a controller.
*/
isClaimable(): boolean {
if (this.memory.hasController) return true;
return false;
}
/**
* Checks whether this room is claimed by another player.
*
* This checks ownership and reservations.
*
* @return {boolean}
* True if the room is claimed by another player.
*/
isClaimed(): boolean {
if (this.isOwned()) return true;
if (this.memory.reservation && this.memory.reservation.username && this.memory.reservation.username !== getUsername()) return true;
return false;
}
/**
* Gets info about a room's reservation status.
*/
getReservationStatus(): ReservationDefinition {
return this.memory.reservation;
}
/**
* Checks if the room is owned by another player.
*
* @return {boolean}
* True if the room is controlled by another player.
*/
isOwned(): boolean {
if (!this.memory.owner) return false;
if (this.memory.owner !== getUsername()) return true;
return false;
}
getOwner(): string {
return this.memory.owner;
}
/**
* Returns this room's last known rcl level.
*
* @return {number}
* Controller level of this room.
*/
getRcl(): number {
return this.memory.rcl || 0;
}
/**
* Returns position of energy sources in the room.
*
* @return {object[]}
* An Array of ob objects containing id, x and y position of the source.
*/
getSourcePositions(): Array<{x: number; y: number; id: Id<Source>; free: number}> {
return this.memory.sources || [];
}
/**
* Returns type of mineral source in the room, if available.
*
* @return {string}
* Type of this room's mineral source.
*/
getMineralTypes(): string[] {
const result = [];
for (const mineral of this.memory.minerals || []) {
result.push(mineral.type);
}
return result;
}
/**
* Returns position of mineral deposit in the room.
*
* @return {object}
* An Object containing id, type, x and y position of the mineral deposit.
*/
getMineralPositions(): Array<{x: number; y: number; id: Id<Mineral>; type: MineralConstant}> {
return this.memory.minerals || [];
}
getMineralAmounts(): Partial<Record<ResourceConstant, number>> {
const result = {};
for (const mineral of this.memory.minerals || []) {
result[mineral.type] = mineral.amount;
}
return result;
}
getDepositInfo(): DepositInfo[] {
return this.memory.deposits;
}
/**
* Returns a cost matrix for the given room.
*
* @return {PathFinder.CostMatrix}
* A cost matrix representing this room.
*/
getCostMatrix(): CostMatrix {
// @todo For some reason, calling this in console gives a different version of the cost matrix. Verify!
let obstaclePositions;
if (this.memory.costPositions) {
obstaclePositions = {
obstacles: unpackCoordListAsPosList(this.memory.costPositions[0], this.roomName),
roads: unpackCoordListAsPosList(this.memory.costPositions[1], this.roomName),
};
}
const matrix = new PathFinder.CostMatrix();
if (obstaclePositions) {
for (const pos of obstaclePositions.obstacles) {
matrix.set(pos.x, pos.y, 0xFF);
}
for (const pos of obstaclePositions.roads) {
if (matrix.get(pos.x, pos.y) === 0) {
matrix.set(pos.x, pos.y, 1);
}
}
}
// Also try not to drive through bays.
if (Game.rooms[this.roomName]?.isMine() && Game.rooms[this.roomName]?.roomPlanner) {
_.each(Game.rooms[this.roomName].roomPlanner.getLocations('bay_center'), pos => {
if (matrix.get(pos.x, pos.y) <= 20) {
matrix.set(pos.x, pos.y, 20);
}
});
// Also avoid blocking construction sites we may not have cached yet.
_.each(Game.rooms[this.roomName].find(FIND_MY_CONSTRUCTION_SITES), site => {
if (site.isWalkable()) return;
matrix.set(site.pos.x, site.pos.y, 0xFF);
});
}
return matrix;
}
/**
* Checks whether there is a previously generated cost matrix for this room.
*
* @return {bool}
* Whether a cost matrix has previously been generated for this room.
*/
hasCostMatrixData(): boolean {
if (this.memory.costPositions) return true;
return false;
}
/**
* Returns a list of rooms connected to this one, keyed by direction.
*
* @return {object}
* Exits as returned by Game.map.getExits().
*/
getExits = function (): Partial<Record<ExitKey, string>> {
return this.memory.exits || {};
};
/**
* Returns position of the Controller structure in this room.
*
* @return {RoomPosition}
* Position of this room's controller.
*/
getControllerPosition(): RoomPosition {
if (!this.memory.structures || !this.memory.structures[STRUCTURE_CONTROLLER]) return null;
const controller: {x: number; y: number} = _.sample(this.memory.structures[STRUCTURE_CONTROLLER]);
if (!controller) return null;
return new RoomPosition(controller.x, controller.y, this.roomName);
}
/**
* Returns position and id of certain structures.
*
* @param {string} structureType
* The type of structure to get info on.
*
* @return {object}
* An object keyed by structure id. The stored objects contain the properties
* x, y, hits and hitsMax.
*/
getStructures(structureType: StructureConstant): Record<string, {x: number; y: number; hits: number; hitsMax: number}> {
if (!this.memory.structures || !this.memory.structures[structureType]) return {};
return this.memory.structures[structureType];
}
/**
* Returns number of tiles of a certain type in a room.
*
* @param {string} type
* Tile type. Can be one of `plain`, `swamp`, `wall` or `exit`.
*
* @return {number}
* Number of tiles of the given type in this room.
*/
countTiles(type: string) {
if (!this.memory.terrain) return 0;
return this.memory.terrain[type] || 0;
}
/**
* Returns which exits of a room are considered safe.
*
* This is usually when they are dead ends or link up with other rooms
* owned by us that are sufficiently defensible.
*
* @param {object} options
* Further options for calculation, possible keys are:
* - safe: An array of room names which are considered safe no matter what.
* - unsafe: An array of room names which are considered unsafe no matter what.
*
* @return {object}
* An object describing adjacent room status, containing the following keys:
* - directions: An object with keys N, E, S, W of booleans describing
* whether that exit direction is considered safe.
* - safeRooms: An array of room names that are considered safe and nearby.
*/
calculateAdjacentRoomSafety(options?: {safe?: string[]; unsafe?: string[]}): {directions: Record<string, boolean>; safeRooms: string[]} {
return cache.inHeap('adjacentSafety:' + this.roomName, 100, () => {
if (!this.memory.exits) {
return {
directions: {
N: false,
E: false,
S: false,
W: false,
},
safeRooms: [],
};
}
if (Memory.rooms[this.roomName]?.isStripmine) {
return {
directions: {
N: false,
E: false,
S: false,
W: false,
},
safeRooms: [],
};
}
const dirMap = {
[TOP]: 'N',
[RIGHT]: 'E',
[BOTTOM]: 'S',
[LEFT]: 'W',
};
this.newStatus = {
N: true,
E: true,
S: true,
W: true,
};
const openList = {};
const closedList = {};
this.joinedDirs = {};
this.otherSafeRooms = options ? (options.safe || []) : [];
this.otherUnsafeRooms = options ? (options.unsafe || []) : [];
// Add initial directions to open list.
for (const moveDir of _.keys(this.memory.exits)) {
const dir = dirMap[moveDir];
const roomName = this.memory.exits[moveDir];
this.addAdjacentRoomToCheck(roomName, openList, {dir, range: 0});
}
// Process adjacent rooms until range has been reached.
while (_.size(openList) > 0) {
let minRange = null;
for (const roomName in openList) {
if (!minRange || minRange.range > openList[roomName].range) {
minRange = openList[roomName];
}
}
delete openList[minRange.room];
closedList[minRange.room] = minRange;
this.handleAdjacentRoom(minRange, openList, closedList);
}
// Unify status of directions which meet up somewhere.
for (const dir1 of _.keys(this.joinedDirs)) {
for (const dir2 of _.keys(this.joinedDirs[dir1])) {
this.newStatus[dir1] = this.newStatus[dir1] && this.newStatus[dir2];
this.newStatus[dir2] = this.newStatus[dir1] && this.newStatus[dir2];
}
}
// Keep a list of rooms declared as safe in memory.
const safeRooms = [];
for (const roomName of _.keys(closedList)) {
const roomDir = closedList[roomName].origin;
if (this.newStatus[roomDir]) {
safeRooms.push(roomName);
}
}
return {
directions: this.newStatus,
safeRooms,
};
});
}
/**
* Adds a room to check for adjacent safe rooms.
*
* @param {string} roomName
* Name of the room to add.
* @param {object} openList
* List of rooms that still need checking.
* @param {object} base
* Information about the room this operation is base on.
*/
addAdjacentRoomToCheck(roomName: string, openList: Record<string, {range: number; origin: string; room: string}>, base: {range: number; dir: string}) {
if (!this.isPotentiallyUnsafeRoom(roomName)) return;
openList[roomName] = {
range: base.range + 1,
origin: base.dir,
room: roomName,
};
}
isPotentiallyUnsafeRoom(roomName: string): boolean {
if (this.otherUnsafeRooms.includes(roomName)) return true;
if (this.otherSafeRooms.includes(roomName)) return false;
if (Game.rooms[roomName] && Game.rooms[roomName].isMine()) {
// This is one of our own rooms, and as such is possibly safe.
if ((Game.rooms[roomName].controller.level >= Math.min(5, this.getRcl() - 1)) && !Game.rooms[roomName].isEvacuating() && !Game.rooms[roomName].isStripmine()) return false;
if (roomName === this.roomName) return false;
}
return true;
}
/**
* Check if a room counts as safe room.
*
* @param {object} roomData
* Info about the room we're checking.
* @param {object} openList
* List of rooms that still need checking.
* @param {object} closedList
* List of rooms that have been checked.
*/
handleAdjacentRoom(roomData: {range: number; origin: string; room: string}, openList: Record<string, {range: number; origin: string; room: string}>, closedList: Record<string, {range: number; origin: string; room: string}>) {
const roomIntel = getRoomIntel(roomData.room);
if (roomIntel.getAge() > 100_000) {
// Room has no intel, declare it as unsafe.
this.newStatus[roomData.origin] = false;
return;
}
// Add new adjacent rooms to openList if available.
for (const roomName of _.values<string>(roomIntel.getExits())) {
if (roomData.range >= 3) {
// Room has open exits more than 3 rooms away.
// Mark direction as unsafe.
this.newStatus[roomData.origin] = false;
break;
}
const found = openList[roomName] || closedList[roomName];
if (found) {
if (found.origin !== roomData.origin) {
// Two different exit directions are joined here.
// Treat them as the same.
if (!this.joinedDirs[found.origin]) {
this.joinedDirs[found.origin] = {};
}
this.joinedDirs[found.origin][roomData.origin] = true;
}
continue;
}
this.addAdjacentRoomToCheck(roomName, openList, {dir: roomData.origin, range: roomData.range});
}
}
/**
* Registers a scout attempting to reach this room.
*/
registerScoutAttempt() {
this.memory.lastScout = Game.time;
}
/**
* Determiness the last time a scout was assigned to this room.
*
* @return {number}
* Game tick when a scout attempt was last registered, or 0.
*/
getLastScoutAttempt(): number {
return this.memory.lastScout || -100_000;
}
}
const intelCache: Record<string, RoomIntel> = {};
/**
* Factory method for room intel objects.
*
* @param {string} roomName
* The room for which to get intel.
*
* @return {RoomIntel}
* The requested RoomIntel object.
*/
function getRoomIntel(roomName: string): RoomIntel {
if (!hivemind.segmentMemory.isReady()) throw new Error('Memory is not ready to generate room intel for room ' + roomName + '.');
if (!intelCache[roomName]) {
intelCache[roomName] = new RoomIntel(roomName);
}
return intelCache[roomName];
}
function getRoomsWithIntel(): string[] {
const result: string[] = [];
if (!hivemind.segmentMemory.isReady()) return result;
hivemind.segmentMemory.each('intel:', key => {
result.push(key.slice(6));
});
return result;
}
export {
getRoomIntel,
getRoomsWithIntel,
};
global.getRoomIntel = getRoomIntel;