Mirroar/hivemind

View on GitHub
src/process/strategy/intershard.ts

Summary

Maintainability
C
1 day
Test Coverage
import cache from 'utils/cache';
import hivemind from 'hivemind';
import interShard from 'intershard';
import NavMesh from 'utils/nav-mesh';
import Process from 'process/process';
import Squad from 'manager.squad';
import {decodePosition} from 'utils/serialization';
import {getRoomIntel} from 'room-intel';

declare global {
    interface ShardMemory {
        info?: {
            ownedRooms?: number;
            ownedCreeps?: number;
            maxRoomLevel?: number;
            rooms?: {
                bestExpansion?: {
                    name: string;
                    score: number;
                };
                bestRoom?: {
                    name: string;
                    score: number;
                };
                worstRoom?: {
                    name: string;
                    score: number;
                };
                reclaimable?: Array<{
                    name: string;
                    safe: boolean;
                    rcl: number;
                    portalRoom: string;
                }>;
            };
            interShardExpansion?: {
                room: string;
                portalRoom: string;
                start: number;
            };
        };
        scouting?: Record<string, boolean>;
    }
}

// @todo This should be a setting.
const minRoomLevelToIntershardScout = 7;

/**
 * Chooses rooms for expansion and sends creeps there.
 */
export default class InterShardProcess extends Process {
    memory: ShardMemory;
    _shardData: Record<string, {
        rooms?: number;
        creeps?: number;
        neededCpu: number;
    }>;

    navMesh: NavMesh;

    /**
     * Makes decisions concerning inter-shard travel.
     */
    run() {
        this.memory = interShard.getLocalMemory();

        this.navMesh = new NavMesh();

        this.updateShardInfo();
        this.distributeCPU();
        this.manageScouting();
        this.manageExpanding();
        this.manageExpansionSupport();
        this.manageReclaiming();

        interShard.writeLocalMemory();
    }

    /**
     * Updates general info in intershard memory.
     */
    updateShardInfo() {
        if (!this.memory.info) this.memory.info = {};

        this.memory.info.ownedRooms = Game.myRooms.length;
        this.memory.info.ownedCreeps = _.size(Game.creeps);

        // Determine highest room level.
        this.memory.info.maxRoomLevel = 0;
        for (const room of Game.myRooms) {
            if (room.controller.level > this.memory.info.maxRoomLevel) {
                this.memory.info.maxRoomLevel = room.controller.level;
            }
        }

        // Determine significant rooms.
        this.memory.info.rooms = {};
        const roomStats = this.memory.info.rooms;
        _.each(Memory.strategy.roomList, (info, roomName) => {
            if (typeof info.expansionScore === 'undefined') return;

            if (!Game.rooms[roomName] || !Game.rooms[roomName].isMine()) {
                // The following scores only apply to unowned rooms.
                if (!roomStats.bestExpansion || roomStats.bestExpansion.score < info.expansionScore) {
                    roomStats.bestExpansion = {
                        name: roomName,
                        score: info.expansionScore,
                    };
                }
            }
            else {
                // The following scores only apply to owned rooms.
                if (!roomStats.bestRoom || roomStats.bestRoom.score < info.expansionScore) {
                    roomStats.bestRoom = {
                        name: roomName,
                        score: info.expansionScore,
                    };
                }

                if (!roomStats.worstRoom || roomStats.worstRoom.score > info.expansionScore) {
                    roomStats.worstRoom = {
                        name: roomName,
                        score: info.expansionScore,
                    };
                }
            }
        });
    }

    /**
     * Determines CPU allocation for each active shard.
     */
    distributeCPU() {
        // @todo Only run on "main" shard.
        if (!Game.cpu.setShardLimits) return;

        // Collect information about shards so we can estimate CPU needs.
        this._shardData = {
            total: {
                neededCpu: 0,
            },
        };
        this.addShardData(Game.shard.name, this.memory);

        // Based on collected information, assign CPU to each shard.
        const totalCpu = _.sum(Game.cpu.shardLimits);
        const newLimits = {};
        _.each(this._shardData, (data, shardName) => {
            if (shardName === 'total') return;

            newLimits[shardName] = Math.round(totalCpu * data.neededCpu / this._shardData.total.neededCpu);
        });
        Game.cpu.setShardLimits(newLimits);
    }

    /**
     * Collects and stores CPU requirements for a shard and its neighbors.
     *
     * @param {String} shardName
     *   Name of the shard to check.
     * @param {object|null} shardMemory
     *   The shard's memory object, if available.
     */
    addShardData(shardName: string, shardMemory?: ShardMemory) {
        // Only handle each shard once.
        if (this._shardData[shardName]) return;

        if (!shardMemory) shardMemory = interShard.getRemoteMemory(shardName);

        this._shardData[shardName] = {
            rooms: 0,
            creeps: 0,
            neededCpu: 0,
        };

        if (shardMemory.info) {
            this._shardData[shardName].rooms = shardMemory.info.ownedRooms;
            this._shardData[shardName].creeps = shardMemory.info.ownedCreeps;
            this._shardData[shardName].neededCpu = 3 + this._shardData[shardName].rooms + (this._shardData[shardName].creeps / 10);

            if (shardMemory.info.interShardExpansion && this.isAdjacentShardFuntional(shardName)) {
                // Allow for more CPU while creating out first intershard room.
                this._shardData[shardName].neededCpu += 3;
            }
        }
        else if (this.isAdjacentShardFuntional(shardName)) {
            // If we have at least one high-level room, assign a little CPU to unexplored
            // adjacent shards for scouting.
            this._shardData[shardName].neededCpu = 0.5;
        }

        this._shardData.total.neededCpu += this._shardData[shardName].neededCpu;

        _.each(shardMemory.portals, (portals, otherShardName) => {
            this.addShardData(otherShardName);
        });
    }

    isAdjacentShardFuntional(shardName: string) {
        let isFunctional = false;
        _.each(this._shardData, (data, compareShard) => {
            if (compareShard === 'total') return null;

            const compareMemory = compareShard === Game.shard.name ? interShard.getLocalMemory() : interShard.getRemoteMemory(compareShard);
            if (!compareMemory.info) return null;
            if (!compareMemory.portals || !compareMemory.portals[shardName]) return null;
            if ((compareMemory.info.maxRoomLevel || 0) < minRoomLevelToIntershardScout) return null;

            isFunctional = true;
            return false;
        });

        return isFunctional;
    }

    /**
     * Manages scouting adjacent shards.
     */
    manageScouting() {
        this.memory.scouting = {};
        if (this.memory.info.maxRoomLevel < minRoomLevelToIntershardScout) return;

        // Scout nearby shards that have no rooms claimed.
        for (const shardName in this.memory.portals) {
            if (this._shardData[shardName].rooms === 0) {
                this.memory.scouting[shardName] = true;
            }
        }
    }

    /**
     * Manages requesting an expansion squad from a nearby shard.
     */
    manageExpanding() {
        if (!hivemind.segmentMemory.isReady()) return;

        if (this.memory.info.ownedRooms > 0) {
            // Remove expansion request when our room has hopefully stabilized.
            if (this.memory.info.maxRoomLevel >= 4) {
                this.removeIntershardExpansionRequest();
            }

            return;
        }

        // Don't recalculate if we've already set a target.
        // Unless expanding has failed, e.g. due to attacks.
        if (this.memory.info.interShardExpansion) {
            if (this.memory.info.interShardExpansion.start && Game.time - this.memory.info.interShardExpansion.start < 50 * CREEP_LIFE_TIME) return;

            this.failIntershardExpansion();
            return;
        }

        this.startIntershardExpansion();
    }

    failIntershardExpansion() {
        if (!Memory.strategy.expand) {
            Memory.strategy.expand = {
                failedExpansions: [],
                currentTarget: null,
                pathBlocked: null,
                evacuatingRoom: null,
            };
        }

        if (!Memory.strategy.expand.failedExpansions) {
            Memory.strategy.expand.failedExpansions = [];
        }

        Memory.strategy.expand.failedExpansions.push({
            roomName: this.memory.info.interShardExpansion.room,
            time: Game.time,
        });

        hivemind.log('strategy').notify('💀 Intershard expansion to ' + this.memory.info.interShardExpansion.room + ' has failed. A new target will be chosen soon.');

        this.removeIntershardExpansionRequest();
    }

    removeIntershardExpansionRequest() {
        delete this.memory.info.interShardExpansion;

        const squad = new Squad('interShardExpansion');
        squad.clearUnits();
        squad.disband();
    }

    startIntershardExpansion() {
        // Immediately try to expand to the best known room.
        // @todo Decide when we've scouted enough to claim a room.
        if (!this.memory.info.rooms.bestExpansion) return;

        const targetRoom = this.memory.info.rooms.bestExpansion.name;
        const roomIntel = getRoomIntel(targetRoom);

        const expansionInfo = {
            room: targetRoom,
            portalRoom: this.findClosestPortalToRoom(targetRoom),
            start: Game.time,
        };

        this.memory.info.interShardExpansion = expansionInfo;

        // Preliminarily create `interShardExpansion` squad. It will be filled
        // by creeps travelling here through a portal.
        const squad = new Squad('interShardExpansion');
        squad.clearUnits();
        squad.addUnit('singleClaim');
        squad.addUnit('builder');
        squad.setTarget(roomIntel.getControllerPosition());
    }

    /**
     * Manages a squad that sends claimers and builders for intershard expansion.
     */
    manageExpansionSupport() {
        // Clear squad unit assignments for when no support is needed.
        const squad = new Squad('interShardSupport');
        squad.clearUnits();

        // Check if a shard we're scouting is trying to expand.
        _.each(this.memory.portals, (portals, shardName) => {
            const remoteMemory = interShard.getRemoteMemory(shardName);
            if (!remoteMemory.info || !remoteMemory.info.interShardExpansion) return;

            const targetRoom = remoteMemory.info.interShardExpansion.portalRoom;
            const bestPortal = this.findClosestPortalToSpawnTo(targetRoom, shardName);
            if (!bestPortal) return;

            // Send units to facilitate expansion.
            if (remoteMemory.info.maxRoomLevel === 0) squad.addUnit('singleClaim');
            squad.setUnitCount('builder', 2);
            squad.setTarget(bestPortal.pos);
            squad.setSpawn(bestPortal.origin);
        });
    }

    /**
     * Finds the portal that leads to the target room and is close to an owned room.
     *
     * @param {String} roomName
     *   The name of the room we're trying to reach via portal.
     * @param {String} targetShard
     *   The shard we're trying to reach.
     *
     * @return {RoomPosition}
     *   Position of the portal we should take to get to the target room.
     */
    findClosestPortalToSpawnTo(roomName: string, targetShard: string) {
        let bestPortal;
        _.each(this.memory.portals, (portals, shardName) => {
            if (shardName !== targetShard) return;

            _.each(portals, (portalInfo, portalPosition) => {
                if (portalInfo.dest !== roomName) return;

                const pos = decodePosition(portalPosition);
                const bestSourceRoom = this.findClosestSpawn(pos.roomName);
                if (!bestSourceRoom) return;

                if (bestPortal && bestPortal.range <= bestSourceRoom.range) return;

                bestPortal = {
                    pos,
                    range: bestSourceRoom.range,
                    origin: bestSourceRoom.name,
                };
            });
        });

        return bestPortal;
    }

    /**
     * Finds the closest valid spawn location for an intershard expansion.
     *
     * @param {string} targetRoom
     *   Name of the room we're expanding to.
     *
     * @return {string}
     *   Name of the room to spawn from.
     */
    findClosestSpawn(targetRoom: string): {name: string; range: number} | null {
        let bestRoom = null;
        let bestLength = 0;
        for (const room of Game.myRooms) {
            if (room.controller.level < 5) continue;
            if (room.name === targetRoom) continue;
            if (room.getEffectiveAvailableEnergy() < 30_000) continue;
            if (Game.map.getRoomLinearDistance(room.name, targetRoom) > 7) continue;

            const path = this.navMesh.findPath(new RoomPosition(25, 25, room.name), new RoomPosition(25, 25, targetRoom), {maxPathLength: 350});
            if (!path || path.incomplete) continue;

            if (!bestRoom || bestLength > path.path.length) {
                bestRoom = room;
                bestLength = path.path.length;
            }
        }

        return bestRoom && {
            name: bestRoom.name,
            range: bestLength,
        };
    }

    manageReclaiming() {
        const roomStats = this.memory.info.rooms;

        const needsReclaiming = [];
        for (const room of Game.myRooms) {
            if (!room.needsReclaiming()) continue;

            needsReclaiming.push({
                name: room.name,
                safe: room.isSafeForReclaiming(),
                rcl: room.controller.level,
                portalRoom: this.findClosestPortalToRoom(room.name),
            });

            const squad = new Squad('intershardReclaim:' + room.name);
            squad.setUnitCount('builder', 1);
            squad.setUnitCount('brawler', 1);
            squad.setTarget(new RoomPosition(25, 25, room.name));
            squad.setSpawn(null);
        }

        if (needsReclaiming.length > 0) {
            roomStats.reclaimable = needsReclaiming;
        }
        else {
            delete roomStats.reclaimable;
        }
    }

    findClosestPortalToRoom(roomName: string) {
        return cache.inHeap('portalRoomName:' + roomName, 2 * CREEP_LIFE_TIME, () => {
            let bestPortal;
            _.each(this.memory.portals, (portals, shardName) => {
                if (shardName === Game.shard.name) return;

                _.each(portals, (portalInfo, portalLocation) => {
                    const portalPosition = decodePosition(portalLocation);
                    if (Game.map.getRoomLinearDistance(portalPosition.roomName, roomName) > 10) return;

                    // Console.log('Checking if we can reach ' + roomName + ' from portal at ' + portalPosition + '...');

                    const path = this.navMesh.findPath(portalPosition, new RoomPosition(25, 25, roomName), {maxPathLength: 700});
                    // Console.log(path.incomplete ? 'incomplete' : path.path.length);
                    if (!path || path.incomplete) return;

                    if (bestPortal && bestPortal.range <= path.path.length) return;

                    bestPortal = {
                        portalPosition,
                        range: path.path.length,
                    };
                });
            });

            return bestPortal?.portalPosition.roomName;
        });
    }
}