Mirroar/hivemind

View on GitHub
src/process/rooms/highway.ts

Summary

Maintainability
A
1 hr
Test Coverage
import cache from 'utils/cache';
import Process from 'process/process';
import {getRoomIntel} from 'room-intel';
import {isCrossroads} from 'utils/room-name';

declare global {
    interface StrategyMemory {
        caravans?: Record<string, {
            creeps: Array<Id<Creep>>;
            dir: TOP | BOTTOM | LEFT | RIGHT;
            firstSeen: number;
            expires: number;
            rooms: Array<{
                name: string;
                time: number;
            }>;
            contents: Partial<Record<ResourceConstant, number>>;
        }>;
    }
}

export default class HighwayRoomProcess extends Process {
    room: Room;

    /**
     * Manages rooms we own.
     * @constructor
     *
     * @param {object} parameters
     *   Options on how to run this process.
     */
    constructor(parameters: RoomProcessParameters) {
        super(parameters);
        this.room = parameters.room;
    }

    /**
     * Manages one of our rooms.
     */
    run() {
        this.detectCaravans();
        this.pruneOldCaravans();
    }

    detectCaravans() {
        for (const creep of this.room.enemyCreeps[SYSTEM_USERNAME] || []) {
            const id = this.getCaravanId(creep);
            if (!id) continue;

            this.registerCaravan(id);
        }
    }

    getCaravanId(creep: Creep): string {
        if (!creep.name.includes('_', creep.name.length - 2)) return null;

        return creep.name.slice(0, Math.max(0, creep.name.length - 2));
    }

    registerCaravan(id: string) {
        if (!Memory.strategy) Memory.strategy = {};
        if (!Memory.strategy.caravans) Memory.strategy.caravans = {};

        const creeps = _.sortBy(_.filter(this.room.enemyCreeps[SYSTEM_USERNAME], c => c.name.startsWith(id)), c => c.name);
        if (Memory.strategy.caravans[id] && creeps.length < Memory.strategy.caravans[id].creeps.length) {
            // Don't update info about caravans that are already registered if we
            // can't see all previously known creeps.
            return;
        }

        const direction = this.detectDirection(creeps);
        if (!direction) return;

        const firstSeen = Memory.strategy.caravans[id]?.firstSeen || Game.time;
        const rooms = this.getTraversedRooms(direction, creeps, firstSeen);

        Memory.strategy.caravans[id] = {
            firstSeen,
            creeps: _.map<Creep, Id<Creep>>(creeps, 'id'),
            dir: direction,
            expires: rooms[rooms.length - 1].time + 50,
            rooms,
            contents: this.getStoreContents(creeps),
        };
    }

    detectDirection(creeps: Creep[]): TOP | BOTTOM | LEFT | RIGHT {
        const minX = _.min(creeps, c => c.pos.x);
        const maxX = _.max(creeps, c => c.pos.x);
        const minY = _.min(creeps, c => c.pos.y);
        const maxY = _.max(creeps, c => c.pos.y);

        const first = creeps[0].id;
        const last = creeps[creeps.length - 1].id;

        // If moving diagonally we need to adjust direction based on what kind
        // of highway room it is.
        const allowVertical = isCrossroads(this.room.name) || !this.room.name.endsWith('0');
        const allowHorizontal = isCrossroads(this.room.name) || this.room.name.endsWith('0');

        if (allowHorizontal && minX.id === first && maxX.id === last) return LEFT;
        if (allowHorizontal && maxX.id === first && minX.id === last) return RIGHT;
        if (allowVertical && minY.id === first && maxY.id === last) return TOP;
        if (allowVertical && maxY.id === first && minY.id === last) return BOTTOM;

        return null;
    }

    getTraversedRooms(direction: TOP | BOTTOM | LEFT | RIGHT, creeps: Creep[], firstSeen: number): Array<{name: string; time: number}> {
        const rooms: Array<{name: string; time: number}> = [];

        rooms.push({
            name: this.room.name,
            time: Game.time,
        });

        const skipFirstCrossroads = isCrossroads(this.room.name) && Game.time - firstSeen < 75;
        // @todo Estimate how far caravan needs to travel to room edge.
        let nextTime = Game.time + 50;
        let roomName = this.room.name;
        while (!isCrossroads(roomName) || (roomName === this.room.name && skipFirstCrossroads)) {
            const roomIntel = getRoomIntel(roomName);
            const exits = roomIntel.getAge() < 10_000 ? roomIntel.getExits() : Game.map.describeExits(roomName);
            if (!exits[direction]) break;

            roomName = exits[direction];
            rooms.push({
                name: roomName,
                time: nextTime,
            });
            nextTime += 99 - creeps.length;
        }

        return rooms;
    }

    getStoreContents(creeps: Creep[]): Record<string, number> {
        const result: Record<string, number> = {};

        for (const creep of creeps) {
            for (const resourceType in creep.store) {
                result[resourceType] = (result[resourceType] || 0) + (creep.store[resourceType] || 0);
            }
        }

        return result;
    }

    pruneOldCaravans() {
        cache.inHeap('pruneOldCaravans', 1000, () => {
            const caravanMemory = Memory?.strategy?.caravans || {};
            for (const id in caravanMemory) {
                if (caravanMemory[id].expires < Game.time - 2500) {
                    // This caravan is long gone. No need to keep it in memory.
                    delete caravanMemory[id];
                }
            }
        });
    }
}