diff --git a/package.json b/package.json index d0a3873..37eca4e 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,8 @@ ], "ignores": [ "snippets.js", - "mock/constants.ts" + "mock/constants.ts", + "src/utils/Profiler" ], "settings": { "import/resolver": { diff --git a/src/main.ts b/src/main.ts index 219498e..49d1a1a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,8 +10,7 @@ import './prototype/room'; import './prototype/structure'; // Create kernel object. -import {PROCESS_PRIORITY_ALWAYS, PROCESS_PRIORITY_LOW, PROCESS_PRIORITY_HIGH} from 'hivemind'; -import hivemind from 'hivemind'; +import hivemind, {PROCESS_PRIORITY_ALWAYS, PROCESS_PRIORITY_LOW, PROCESS_PRIORITY_HIGH} from 'hivemind'; import SegmentedMemory from 'utils/segmented-memory'; import container from 'utils/container'; @@ -58,7 +57,7 @@ import * as Profiler from 'utils/Profiler'; declare global { interface RawMemory { - _parsed: boolean; + _parsed: Memory; } namespace NodeJS { @@ -98,12 +97,14 @@ balancer.init(); // @todo make unarmed creeps run from hostiles. -const main = { +class BotKernel { + lastTime: number = 0; + lastMemory: Memory = null; /** * Runs main game loop. */ - loop() { + runTick() { this.useMemoryFromHeap(); hivemind.segmentMemory.manage(); @@ -213,10 +214,8 @@ const main = { this.showDebug(); this.recordStats(); - }, + } - lastTime: 0, - lastMemory: null, useMemoryFromHeap() { if (this.lastTime && this.lastMemory && Game.time === this.lastTime + 1) { delete global.Memory; @@ -233,7 +232,7 @@ const main = { } this.lastTime = Game.time; - }, + } /** * Saves CPU stats for the current tick to memory. @@ -252,7 +251,7 @@ const main = { stats.recordStat('cpu_total', time); stats.recordStat('bucket', Game.cpu.bucket); stats.recordStat('creeps', _.size(Game.creeps)); - }, + } /** * Periodically deletes unused data from memory. @@ -296,37 +295,12 @@ const main = { // Periodically clean old room memory. if (Game.time % 3738 === 2100 && hivemind.segmentMemory.isReady()) { - let count = 0; - _.each(Memory.rooms, (memory, roomName) => { - if (getRoomIntel(roomName).getAge() > 100_000) { - delete Memory.rooms[roomName]; - count++; - } - - if (memory.observeTargets && !Game.rooms[roomName]?.observer) { - delete memory.observeTargets; - } - }); - - if (count > 0) { - hivemind.log('main').debug('Pruned old memory for', count, 'rooms.'); - } + this.cleanupRoomMemory(); } // Periodically clean old squad memory. if (Game.time % 548 === 3) { - _.each(Memory.squads, (memory, squadName) => { - // Only delete if squad can't be spawned. - if (memory.spawnRoom && Game.rooms[memory.spawnRoom]) return; - - // Don't delete inter-shard squad that can't have a spawn room. - if (squadName === 'interShardExpansion') return; - - // Only delete if there are no creeps belonging to this squad. - if (_.size(_.filter(Game.creeps, creep => creep.memory.squadName === squadName)) > 0) return; - - delete Memory.squads[squadName]; - }); + this.cleanupSquadMemory(); } // Periodically garbage collect in caches. @@ -352,7 +326,40 @@ const main = { if (Game.time % 3625 == 0 && hivemind.segmentMemory.isReady()) { this.cleanupSegmentMemory(); } - }, + } + + cleanupRoomMemory() { + let count = 0; + _.each(Memory.rooms, (memory, roomName) => { + if (getRoomIntel(roomName).getAge() > 100_000) { + delete Memory.rooms[roomName]; + count++; + } + + if (memory.observeTargets && !Game.rooms[roomName]?.observer) { + delete memory.observeTargets; + } + }); + + if (count > 0) { + hivemind.log('main').debug('Pruned old memory for', count, 'rooms.'); + } + } + + cleanupSquadMemory() { + _.each(Memory.squads, (memory, squadName) => { + // Only delete if squad can't be spawned. + if (memory.spawnRoom && Game.rooms[memory.spawnRoom]) return; + + // Don't delete inter-shard squad that can't have a spawn room. + if (squadName === 'interShardExpansion') return; + + // Only delete if there are no creeps belonging to this squad. + if (_.size(_.filter(Game.creeps, creep => creep.memory.squadName === squadName)) > 0) return; + + delete Memory.squads[squadName]; + }); + } cleanupSegmentMemory() { // Clean old entries from remote path manager from segment memory. @@ -383,7 +390,7 @@ const main = { const isMyRoom = Game.rooms[roomName] && Game.rooms[roomName].isMine(); if (!isMyRoom) hivemind.segmentMemory.delete(key); }); - }, + } /** * @@ -396,10 +403,11 @@ const main = { Memory.hivemind.showProcessDebug--; hivemind.drawProcessDebug(); } - }, + } }; +const kernel = new BotKernel(); export const loop = ErrorMapper.wrapLoop(() => { - main.loop(); + kernel.runTick(); }); diff --git a/src/prototype/room.structures.ts b/src/prototype/room.structures.ts index c2ab570..d70d249 100644 --- a/src/prototype/room.structures.ts +++ b/src/prototype/room.structures.ts @@ -16,6 +16,8 @@ declare global { [STRUCTURE_LINK]: StructureLink[]; [STRUCTURE_NUKER]: StructureNuker[]; [STRUCTURE_OBSERVER]: StructureObserver[]; + [STRUCTURE_PORTAL]: StructurePortal[]; + [STRUCTURE_POWER_BANK]: StructurePowerBank[]; [STRUCTURE_POWER_SPAWN]: StructurePowerSpawn[]; [STRUCTURE_RAMPART]: StructureRampart[]; [STRUCTURE_SPAWN]: StructureSpawn[]; diff --git a/src/role/hauler.relay.ts b/src/role/hauler.relay.ts index 8daf2ed..b82da34 100644 --- a/src/role/hauler.relay.ts +++ b/src/role/hauler.relay.ts @@ -463,7 +463,7 @@ export default class RelayHaulerRole extends Role { filter: structure => structure.structureType === STRUCTURE_CONTAINER && structure.store.getFreeCapacity() < structure.store.getCapacity() * 0.1 && structure.store.getUsedCapacity(RESOURCE_ENERGY) > 100, - }); + }) as StructureContainer[]; if (container.length > 0) creep.heapMemory.energyPickupTarget = container[0].id; return container[0]; } diff --git a/src/room-intel.ts b/src/room-intel.ts index 56d4901..19a99bf 100644 --- a/src/room-intel.ts +++ b/src/room-intel.ts @@ -1,5 +1,5 @@ /* global PathFinder Room RoomPosition -STRUCTURE_KEEPER_LAIR STRUCTURE_CONTROLLER CONTROLLER_DOWNGRADE FIND_SOURCES +STRUCTURE_KEEPER_LAIR STRUCTURE_CONTROLLER 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 */ @@ -36,6 +36,12 @@ declare global { } } +type AdjacentRoomEntry = { + range: number; + origin: string; + room: string; +}; + export interface RoomIntelMemory { lastScan: number; exits: Partial<Record<ExitKey, string>>; @@ -104,7 +110,7 @@ export default class RoomIntel { otherUnsafeRooms: string[]; joinedDirs: Record<string, Record<string, boolean>>; - constructor(roomName) { + constructor(roomName: string) { this.roomName = roomName; const key = 'intel:' + roomName; @@ -139,9 +145,9 @@ export default class RoomIntel { this.gatherResourceIntel(room); const structures = room.structuresByType; - this.gatherPowerIntel(structures[STRUCTURE_POWER_BANK] as StructurePowerBank[]); + this.gatherPowerIntel(structures[STRUCTURE_POWER_BANK]); this.gatherDepositIntel(); - this.gatherPortalIntel(structures[STRUCTURE_PORTAL] as StructurePortal[]); + this.gatherPortalIntel(structures[STRUCTURE_PORTAL]); this.gatherInvaderIntel(structures); this.gatherExitIntel(room.name); @@ -180,7 +186,7 @@ export default class RoomIntel { this.memory.rcl = 0; this.memory.ticksToDowngrade = 0; this.memory.hasController = typeof room.controller !== 'undefined'; - if (room.controller && room.controller.owner) { + if (room.controller?.owner) { this.memory.owner = room.controller.owner.username; this.memory.rcl = room.controller.level; this.memory.ticksToDowngrade = room.controller.ticksToDowngrade; @@ -386,7 +392,7 @@ export default class RoomIntel { gatherPortalIntel(portals: StructurePortal[]) { delete this.memory.portals; - const targetRooms = []; + const targetRooms: string[] = []; for (const portal of portals || []) { // Ignore same-shard portals for now. if ('shard' in portal.destination) { @@ -456,13 +462,13 @@ export default class RoomIntel { if (this.memory.owner) return; if (!structures[STRUCTURE_STORAGE] && !structures[STRUCTURE_TERMINAL] && ruins.length === 0) return; - const resources = {}; + const resources: Partial<Record<ResourceConstant, number>> = {}; const collections = [structures[STRUCTURE_STORAGE], structures[STRUCTURE_TERMINAL], ruins]; _.each(collections, objects => { _.each(objects, object => { if (!object.store) return; - _.each(object.store, (amount, resourceType) => { + _.each(object.store, (amount: number, resourceType: ResourceConstant) => { resources[resourceType] = (resources[resourceType] || 0) + amount; }); }); @@ -497,10 +503,10 @@ export default class RoomIntel { * @param {object} structures * An object containing Arrays of structures, keyed by structure type. */ - gatherInvaderIntel(structures) { + gatherInvaderIntel(structures: Record<string, Structure[]>) { delete this.memory.invaderInfo; - const core: StructureInvaderCore = _.first(structures[STRUCTURE_INVADER_CORE]); + const core = _.first(structures[STRUCTURE_INVADER_CORE]) as StructureInvaderCore; if (!core) return; // Commit basic invader core info. @@ -619,7 +625,7 @@ export default class RoomIntel { */ isClaimed(): boolean { if (this.isOwned()) return true; - if (this.memory.reservation && this.memory.reservation.username && this.memory.reservation.username !== getUsername()) return true; + if (this.memory.reservation?.username && this.memory.reservation.username !== getUsername()) return true; return false; } @@ -675,7 +681,7 @@ export default class RoomIntel { * Type of this room's mineral source. */ getMineralTypes(): string[] { - const result = []; + const result: string[] = []; for (const mineral of this.memory.minerals || []) { result.push(mineral.type); @@ -716,7 +722,7 @@ export default class RoomIntel { */ getCostMatrix(): CostMatrix { // @todo For some reason, calling this in console gives a different version of the cost matrix. Verify! - let obstaclePositions; + let obstaclePositions: {obstacles: RoomPosition[]; roads: RoomPosition[]}; if (this.memory.costPositions) { obstaclePositions = { obstacles: unpackCoordListAsPosList(this.memory.costPositions[0], this.roomName), @@ -817,7 +823,7 @@ export default class RoomIntel { * @return {number} * Number of tiles of the given type in this room. */ - countTiles(type: string) { + countTiles(type: 'plain' | 'swamp' | 'wall' | 'exit') { if (!this.memory.terrain) return 0; return this.memory.terrain[type] || 0; @@ -871,7 +877,7 @@ export default class RoomIntel { [RIGHT]: 'E', [BOTTOM]: 'S', [LEFT]: 'W', - }; + } as const; this.newStatus = { N: true, @@ -880,14 +886,14 @@ export default class RoomIntel { W: true, }; - const openList = {}; - const closedList = {}; + const openList: Record<string, AdjacentRoomEntry> = {}; + const closedList: Record<string, AdjacentRoomEntry> = {}; 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]; + for (const moveDir in this.memory.exits) { + const dir: string = dirMap[moveDir]; const roomName = this.memory.exits[moveDir]; this.addAdjacentRoomToCheck(roomName, openList, {dir, range: 0}); @@ -895,7 +901,7 @@ export default class RoomIntel { // Process adjacent rooms until range has been reached. while (_.size(openList) > 0) { - let minRange = null; + let minRange: AdjacentRoomEntry = null; for (const roomName in openList) { if (!minRange || minRange.range > openList[roomName].range) { minRange = openList[roomName]; @@ -942,7 +948,7 @@ export default class RoomIntel { * @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}) { + addAdjacentRoomToCheck(roomName: string, openList: Record<string, AdjacentRoomEntry>, base: {range: number; dir: string}) { if (!this.isPotentiallyUnsafeRoom(roomName)) return; openList[roomName] = { @@ -975,7 +981,7 @@ export default class RoomIntel { * @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}>) { + handleAdjacentRoom(roomData: AdjacentRoomEntry, openList: Record<string, AdjacentRoomEntry>, closedList: Record<string, AdjacentRoomEntry>) { const roomIntel = getRoomIntel(roomData.room); if (roomIntel.getAge() > 100_000) { // Room has no intel, declare it as unsafe. diff --git a/src/utils/nav-mesh.ts b/src/utils/nav-mesh.ts index 4c37c65..7a8a9f8 100644 --- a/src/utils/nav-mesh.ts +++ b/src/utils/nav-mesh.ts @@ -3,7 +3,7 @@ TERRAIN_MASK_WALL STRUCTURE_KEEPER_LAIR */ import cache from 'utils/cache'; import hivemind from 'hivemind'; -import {encodePosition, serializePosition, deserializePosition, serializeCoords} from 'utils/serialization'; +import {encodePosition, deserializePosition, serializeCoords} from 'utils/serialization'; import {getCostMatrix} from 'utils/cost-matrix'; import {getRoomIntel} from 'room-intel'; import {handleMapArea} from 'utils/map'; @@ -88,8 +88,7 @@ export default class NavMesh { // Mesh doesn't need to be updated very often. // @todo Allow forcing update for when we dismantle a structure. if ( - this.memory.rooms[roomName] - && this.memory.rooms[roomName].paths + this.memory.rooms[roomName]?.paths && !hivemind.hasIntervalPassed(10_000, this.memory.rooms[roomName].gen) ) return; @@ -147,17 +146,18 @@ export default class NavMesh { * An array of exit information objects. */ getExitInfo(roomName: string): ExitInfo[] { - const exits: ExitInfo[] = []; - - this.collectExitGroups(roomName, exits, LEFT, true, 0); - this.collectExitGroups(roomName, exits, RIGHT, true, 49); - this.collectExitGroups(roomName, exits, TOP, false, 0); - this.collectExitGroups(roomName, exits, BOTTOM, false, 49); + const exits: ExitInfo[] = [ + ...this.collectExitGroups(roomName, LEFT, true, 0), + ...this.collectExitGroups(roomName, RIGHT, true, 49), + ...this.collectExitGroups(roomName, TOP, false, 0), + ...this.collectExitGroups(roomName, BOTTOM, false, 49), + ]; return exits; } - collectExitGroups(roomName: string, exits: ExitInfo[], dir: DirectionConstant, vertical: boolean, offset: number) { + collectExitGroups(roomName: string, dir: DirectionConstant, vertical: boolean, offset: number): ExitInfo[] { + const exits: ExitInfo[] = []; const isAvailable = this.isAvailableExitDirection(roomName, dir); let groupId = 1; let currentStart: number = null; @@ -187,6 +187,8 @@ export default class NavMesh { this.costMatrix.set(x, y, isAvailable ? (nextId + 100) : 255); } + + return exits; } isAvailableExitDirection(roomName: string, dir: DirectionConstant): boolean { @@ -311,17 +313,46 @@ export default class NavMesh { const costMatrix = getCostMatrix(roomName, {ignoreMilitary: true}); for (const region of regions) { - const centerXR = region.center % 50; - const centerYR = Math.floor(region.center / 50); + this.addConnectingPathsForRegion(region, paths, roomName, costMatrix); + } + + return paths; + } + + addConnectingPathsForRegion(region: RegionInfo, paths: Record<number, Record<number, number>>, roomName: string, costMatrix: CostMatrix) { + const centerXR = region.center % 50; + const centerYR = Math.floor(region.center / 50); + + for (const exitId of region.exits) { + const exit = this.exitLookup[exitId]; + const centerX = exit.vertical ? exit.offset : exit.center; + const centerY = exit.vertical ? exit.center : exit.offset; + + const result = PathFinder.search( + new RoomPosition(centerX, centerY, roomName), + new RoomPosition(centerXR, centerYR, roomName), + { + roomCallback: () => costMatrix, + maxRooms: 1, + }, + ); + + if (!result.incomplete) { + if (!paths[exitId]) paths[exitId] = {}; + paths[exitId][0] = result.path.length; + } + + for (const exitId2 of region.exits) { + if (exitId === exitId2) continue; + if (paths[exitId2] && paths[exitId2][exitId]) continue; - for (const exitId of region.exits) { - const exit = this.exitLookup[exitId]; - const centerX = exit.vertical ? exit.offset : exit.center; - const centerY = exit.vertical ? exit.center : exit.offset; + const exit2 = this.exitLookup[exitId2]; + const centerX2 = exit2.vertical ? exit2.offset : exit2.center; + const centerY2 = exit2.vertical ? exit2.center : exit2.offset; const result = PathFinder.search( new RoomPosition(centerX, centerY, roomName), - new RoomPosition(centerXR, centerYR, roomName), + new RoomPosition(centerX2, centerY2, roomName), { roomCallback: () => costMatrix, maxRooms: 1, @@ -330,35 +361,10 @@ export default class NavMesh { if (!result.incomplete) { if (!paths[exitId]) paths[exitId] = {}; - paths[exitId][0] = result.path.length; - } - - for (const exitId2 of region.exits) { - if (exitId === exitId2) continue; - if (paths[exitId2] && paths[exitId2][exitId]) continue; - - const exit2 = this.exitLookup[exitId2]; - const centerX2 = exit2.vertical ? exit2.offset : exit2.center; - const centerY2 = exit2.vertical ? exit2.center : exit2.offset; - - const result = PathFinder.search( - new RoomPosition(centerX, centerY, roomName), - new RoomPosition(centerX2, centerY2, roomName), - { - roomCallback: () => costMatrix, - maxRooms: 1, - }, - ); - - if (!result.incomplete) { - if (!paths[exitId]) paths[exitId] = {}; - paths[exitId][exitId2] = result.path.length; - } + paths[exitId][exitId2] = result.path.length; } } } - - return paths; } getPortals(roomName: string) { @@ -437,10 +443,6 @@ export default class NavMesh { const startTime = Game.cpu.getUsed(); const startRoom = startPos.roomName; const endRoom = endPos.roomName; - let availableExits: Array<{ - id: number; - center: number; - }> = []; const openList: NavMeshPathfindingEntry[] = []; const openListLookup: Record<string, boolean> = {}; const closedList: Record<string, boolean> = {}; @@ -452,60 +454,8 @@ export default class NavMesh { } const roomMemory = this.memory.rooms[startRoom]; - if (roomMemory.regions) { - const costMatrix = getCostMatrix(startRoom, {ignoreMilitary: true}); - for (const region of roomMemory.regions) { - // Check if we can reach region center. - const result = PathFinder.search( - startPos, - deserializePosition(region.center, startRoom), - { - roomCallback: () => costMatrix, - maxRooms: 1, - }, - ); - - if (result.incomplete) continue; - - // Exits for this region are available. - availableExits = _.filter(roomMemory.exits, exit => region.exits.includes(exit.id)); - } - } - else { - availableExits = roomMemory.exits; - } - - for (const exit of availableExits) { - const segmentLength = roomMemory.paths[exit.id] ? roomMemory.paths[exit.id][0] : 50; - const entry: NavMeshPathfindingEntry = { - exitId: exit.id, - pos: exit.center, - roomName: startRoom, - parent: null, - pathLength: segmentLength, - totalSteps: segmentLength, - heuristic: (Game.map.getRoomLinearDistance(startRoom, endRoom) - 1) * 50, - portal: false, - }; - openList.push(entry); - openListLookup[startRoom + '/' + entry.pos] = true; - } - - for (const portal of roomMemory.portals || []) { - const entry: NavMeshPathfindingEntry = { - exitId: null, - pos: portal.pos, - roomName: startRoom, - parent: null, - pathLength: 25, - totalSteps: 25, - heuristic: (Game.map.getRoomLinearDistance(startRoom, endRoom) - 1) * 50, - portal: true, - targetRoom: portal.room, - }; - openList.push(entry); - openListLookup[startRoom + '/' + entry.pos] = true; - } + this.addExitsToOpenList(this.getAvailableExits(startPos, startRoom), openList, openListLookup, startRoom, endRoom); + this.addPortalsToOpenList(openList, openListLookup, startRoom, endRoom); while (openList.length > 0) { if (Game.cpu.getUsed() - startTime > (options.maxCpu || 5)) { @@ -518,8 +468,7 @@ export default class NavMesh { const current = this.popBestCandidate(openList); const nextRoom = current.portal ? current.targetRoom : this.getAdjacentRoom(current.roomName, current.exitId); const correspondingExit = current.portal ? null : this.getCorrespondingExitId(current.exitId); - let costMultiplier = 1; - closedList[current.roomName + '/' + current.pos] = true; + closedList[`${current.roomName}/${current.pos}`] = true; if (nextRoom === endRoom) { // @todo There might be shorter paths to the actual endPosition. @@ -547,58 +496,29 @@ export default class NavMesh { if (current.portal) { const portalBack = _.find(roomMemory.portals, p => p.room === current.roomName); const exitPos = portalBack ? portalBack.pos : (25 + (50 * 25)); - if (closedList[nextRoom + '/' + exitPos]) continue; + if (closedList[`${nextRoom}/${exitPos}`]) continue; - closedList[nextRoom + '/' + exitPos] = true; + closedList[`${nextRoom}/${exitPos}`] = true; } else if (roomMemory.exits[correspondingExit]) { const exitPos = roomMemory.exits[correspondingExit].center; - if (closedList[nextRoom + '/' + exitPos]) continue; + if (closedList[`${nextRoom}/${exitPos}`]) continue; - closedList[nextRoom + '/' + exitPos] = true; + closedList[`${nextRoom}/${exitPos}`] = true; } if (hivemind.segmentMemory.isReady()) { const roomIntel = getRoomIntel(nextRoom); if (roomIntel.isOwned()) { if (!options.allowDanger && !hivemind.relations.isAlly(roomIntel.getOwner())) continue; - - costMultiplier *= 5; } - else if (roomIntel.isClaimed() && roomIntel.getReservationStatus().username !== 'Invader') { - costMultiplier *= 1.5; - } - else if (_.size(roomIntel.getStructures(STRUCTURE_KEEPER_LAIR)) > 0) { - // Allow pathing through source keeper rooms since we can safely avoid them. - costMultiplier *= 1.2; - } - } - - if (Memory.rooms[nextRoom]?.enemies && !Memory.rooms[nextRoom]?.enemies?.safe && !options.allowDanger) { - // Avoid rooms with enemies in them if possible. - costMultiplier *= 2; - } - - availableExits = []; - if (current.portal) { - availableExits = roomMemory.exits; - } - - if (roomMemory.regions) { - // Find region containing corresponding exit. - const region = _.find(roomMemory.regions, (region: any) => region.exits.includes(correspondingExit)); - if (!region) continue; - - availableExits = _.filter(roomMemory.exits, exit => exit.id !== correspondingExit && region.exits.includes(exit.id)); } - else { - availableExits = _.filter(roomMemory.exits, exit => exit.id !== correspondingExit); - } - - for (const exit of availableExits) { + + const costMultiplier = this.calculateCostMultiplier(nextRoom, options.allowDanger); + for (const exit of this.getAvailableExitsCorrespondingTo(nextRoom, correspondingExit, current.portal)) { // Check if in closed list. - if (closedList[nextRoom + '/' + exit.center]) continue; - if (openListLookup[nextRoom + '/' + exit.center]) continue; + if (closedList[`${nextRoom}/${exit.center}`]) continue; + if (openListLookup[`${nextRoom}/${exit.center}`]) continue; if (!current.portal) { // If there's a weird path mismatch, skip. @@ -627,14 +547,14 @@ export default class NavMesh { } openList.push(item); - openListLookup[nextRoom + '/' + exit.center] = true; - openListLookup[nextRoom + '/' + item.pos] = true; + openListLookup[`${nextRoom}/${exit.center}`] = true; + openListLookup[`${nextRoom}/${item.pos}`] = true; } for (const portal of roomMemory.portals || []) { // Check if in closed list. - if (closedList[nextRoom + '/' + portal.pos]) continue; - if (openListLookup[nextRoom + '/' + portal.pos]) continue; + if (closedList[`${nextRoom}/${portal.pos}`]) continue; + if (openListLookup[`${nextRoom}/${portal.pos}`]) continue; const item = { exitId: null, @@ -654,8 +574,7 @@ export default class NavMesh { } openList.push(item); - openListLookup[nextRoom + '/' + item.pos] = true; - openListLookup[nextRoom + '/' + item.pos] = true; + openListLookup[`${nextRoom}/${item.pos}`] = true; } } @@ -666,6 +585,114 @@ export default class NavMesh { }; } + getAvailableExits(startPos: RoomPosition, roomName: string): Array<{ + id: number; + center: number; + }> { + const roomMemory = this.memory.rooms[roomName]; + if (!roomMemory.regions) return roomMemory.exits; + + const costMatrix = getCostMatrix(roomName, {ignoreMilitary: true}); + for (const region of roomMemory.regions) { + // Check if we can reach region center. + const result = PathFinder.search( + startPos, + deserializePosition(region.center, roomName), + { + roomCallback: () => costMatrix, + maxRooms: 1, + }, + ); + + if (result.incomplete) continue; + + // Exits for this region are available. + return _.filter(roomMemory.exits, exit => region.exits.includes(exit.id)); + } + + return []; + } + + getAvailableExitsCorrespondingTo(roomName: string, exitId: number | null, isPortal: boolean): Array<{ + id: number; + center: number; + }> { + const roomMemory = this.memory.rooms[roomName]; + if (isPortal) return roomMemory.exits; + + if (!roomMemory.regions) return _.filter(roomMemory.exits, exit => exit.id !== exitId); + + // Find region containing corresponding exit. + const region = _.find(roomMemory.regions, (region: any) => region.exits.includes(exitId)); + if (!region) return []; + + return _.filter(roomMemory.exits, exit => exit.id !== exitId && region.exits.includes(exit.id)); + } + + addExitsToOpenList(exits: Array<{id: number; center: number;}>, openList: NavMeshPathfindingEntry[], openListLookup: Record<string, boolean>, roomName: string, endRoom: string) { + const roomMemory = this.memory.rooms[roomName]; + for (const exit of exits) { + const segmentLength = roomMemory.paths[exit.id] ? roomMemory.paths[exit.id][0] : 50; + const entry: NavMeshPathfindingEntry = { + exitId: exit.id, + pos: exit.center, + roomName, + parent: null, + pathLength: segmentLength, + totalSteps: segmentLength, + heuristic: (Game.map.getRoomLinearDistance(roomName, endRoom) - 1) * 50, + portal: false, + }; + openList.push(entry); + openListLookup[`${roomName}/${entry.pos}`] = true; + } + } + + addPortalsToOpenList(openList: NavMeshPathfindingEntry[], openListLookup: Record<string, boolean>, roomName: string, endRoom: string) { + const roomMemory = this.memory.rooms[roomName]; + for (const portal of roomMemory.portals || []) { + const entry: NavMeshPathfindingEntry = { + exitId: null, + pos: portal.pos, + roomName, + parent: null, + pathLength: 25, + totalSteps: 25, + heuristic: (Game.map.getRoomLinearDistance(roomName, endRoom) - 1) * 50, + portal: true, + targetRoom: portal.room, + }; + openList.push(entry); + openListLookup[`${roomName}/${entry.pos}`] = true; + } + } + + calculateCostMultiplier(roomName: string, allowDanger: boolean): number { + let costMultiplier = 1; + if (hivemind.segmentMemory.isReady()) { + const roomIntel = getRoomIntel(roomName); + if (roomIntel.isOwned()) { + if (!hivemind.relations.isAlly(roomIntel.getOwner())) { + costMultiplier *= 5; + } + } + else if (roomIntel.isClaimed() && roomIntel.getReservationStatus().username !== 'Invader') { + costMultiplier *= 1.5; + } + else if (_.size(roomIntel.getStructures(STRUCTURE_KEEPER_LAIR)) > 0) { + // Allow pathing through source keeper rooms since we can safely avoid them. + costMultiplier *= 1.2; + } + } + + if (Memory.rooms[roomName]?.enemies && !Memory.rooms[roomName]?.enemies?.safe && !allowDanger) { + // Avoid rooms with enemies in them if possible. + costMultiplier *= 2; + } + + return costMultiplier; + } + popBestCandidate(openList: NavMeshPathfindingEntry[]): NavMeshPathfindingEntry { // Find element id with lowest pathLength + heuristic. let minId = null;