Mirroar/hivemind

View on GitHub
src/role/builder.mines.ts

Summary

Maintainability
F
5 days
Test Coverage
/* global FIND_DROPPED_RESOURCES RESOURCE_ENERGY OK
ERR_NO_PATH ERR_NOT_IN_RANGE FIND_STRUCTURES STRUCTURE_CONTAINER STRUCTURE_ROAD
FIND_MY_CONSTRUCTION_SITES LOOK_STRUCTURES MAX_CONSTRUCTION_SITES
LOOK_CONSTRUCTION_SITES */

// @todo Collect energy if it's lying on the path.

import cache from 'utils/cache';
import hivemind from 'hivemind';
import RemoteMiningOperation from 'operation/remote-mining';
import Role from 'role/role';
import {encodePosition, decodePosition, serializePositionPath} from 'utils/serialization';

declare global {
    interface MineBuilderCreep extends Creep {
        memory: MineBuilderCreepMemory;
        heapMemory: MineBuilderCreepHeapMemory;
        operation: RemoteMiningOperation;
    }

    interface MineBuilderCreepMemory extends CreepMemory {
        role: 'builder.mines';
        returning: boolean;
        source: string;
    }

    interface MineBuilderCreepHeapMemory extends CreepHeapMemory {
        energyPickupTarget: string;
    }
}

export default class MineBuilderRole extends Role {
    actionTaken: boolean;

    /**
     * Makes a creep behave like a mine builder.
     *
     * @param {Creep} creep
     *   The creep to run logic for.
     */
    run(creep: MineBuilderCreep) {
        // @todo Take from haulers when next to them. Also take from harvesters when building container so they don't have to.
        if (!hivemind.segmentMemory.isReady()) return;

        if (creep.heapMemory.suicideSpawn) {
            this.performRecycle(creep);
        }

        if (!creep.memory.source) {
            if (creep.pos.roomName !== creep.memory.sourceRoom) {
                creep.interRoomTravel(new RoomPosition(25, 25, creep.memory.sourceRoom));
            }
            else {
                creep.whenInRange(3, creep.room.storage || creep.room.terminal || creep.room.getStorageLocation(), () => {
                    // Wait until there's something to do.
                    this.determineTargetSource(creep);
                });
            }

            return;
        }

        if (creep.memory.returning) {
            // Repair / build roads on the way home.

            if (this.performBuildRoad(creep)) return;

            this.performReturnHome(creep);
            return;
        }

        this.performGoToSource(creep);
    }

    /**
     * Puts this creep into or out of delivery mode.
     *
     * @param {Creep} creep
     *   The creep to run logic for.
     * @param {boolean} returning
     *   Whether this creep should be delivering it's carried resources.
     */
    setReturning(creep: MineBuilderCreep, returning: boolean) {
        creep.memory.returning = returning;

        if (!returning) {
            this.determineTargetSource(creep);
        }

        const path = this.getPath(creep);
        if (!path) return;

        creep.setCachedPath(serializePositionPath(path), !returning, 1);
    }

    determineTargetSource(creep: MineBuilderCreep) {
        delete creep.memory.source;
        const harvestPositions = creep.room.getRemoteHarvestSourcePositions();
        const scoredPositions = [];
        for (const position of harvestPositions) {
            scoredPositions.push(this.scoreHarvestPosition(creep, position));
        }

        if (scoredPositions.length === 0) return;

        const bestPosition = _.max(_.filter(scoredPositions, p => p.work > 0), 'work');

        if (bestPosition?.position) {
            creep.memory.source = encodePosition(bestPosition.position);
            creep.memory.operation = 'mine:' + bestPosition.position.roomName;
        }
    }

    scoreHarvestPosition(creep: MineBuilderCreep, position: RoomPosition) {
        const targetPos = encodePosition(position);
        const operation = Game.operationsByType.mining['mine:' + position.roomName];
        if (!operation || operation.isUnderAttack()) return {position, work: -1000};

        const path = operation.getPaths()[targetPos];

        const hasBuilder = _.some(Game.creepsByRole['builder.mines'], (c: MineBuilderCreep) => c.memory.source === targetPos);
        if (hasBuilder) return {position, work: 0};

        const hasHarvester = _.some(Game.creepsByRole['harvester.remote'], (c: RemoteHarvesterCreep) => c.memory.source === targetPos);
        if (!hasHarvester) return {position, work: 0};

        const neededWork = operation.getNeededWork(targetPos);

        return {
            position,
            work: neededWork,
        };
    }

    getPath(creep: MineBuilderCreep): RoomPosition[] | null {
        if (!creep.operation) return null;

        const paths = creep.operation.getPaths();
        if (!paths[creep.memory.source] || !paths[creep.memory.source].accessible) return null;

        return paths[creep.memory.source].path;
    }

    /**
     * Makes a creep deliver resources to another room.
     *
     * @param {Creep} creep
     *   The creep to run logic for.
     */
    performReturnHome(creep: MineBuilderCreep) {
        // Refill at container if we emptied ourselves too much repairing it.
        const container = creep.operation?.getContainer(creep.memory.source);
        if (container && container.pos.roomName === creep.pos.roomName && creep.pos.getRangeTo(container) < 10
                && creep.store.getUsedCapacity() < creep.store.getCapacity() * 0.5
                && container.store.getUsedCapacity(RESOURCE_ENERGY) > container.store.getCapacity() * 0.1
        ) {
            // If we're close to source container, make sure we fill up before
            // returning home.
            creep.whenInRange(1, container, () => {
                creep.withdraw(container, RESOURCE_ENERGY);
            });

            return;
        }

        if (this.pickupNearbyEnergy(creep)) return;

        if (creep.room.name === creep.memory.sourceRoom) {
            if (creep.store.getFreeCapacity() === 0) {
                this.setReturning(creep, false);
                return;
            }

            const target = creep.room.getBestStorageSource(RESOURCE_ENERGY);

            if (target) {
                creep.whenInRange(1, target, () => {
                    if (creep.withdraw(target, RESOURCE_ENERGY) === OK) this.setReturning(creep, false);
                });
            }
            else {
                // Wait for energy to become available.
                creep.whenInRange(5, creep.room.getStorageLocation(), () => {});
            }

            return;
        }

        if (creep.hasCachedPath()) {
            creep.followCachedPath();
            if (creep.hasArrived()) {
                creep.clearCachedPath();
            }
            else {}
        }
        else {
            creep.moveToRange(new RoomPosition(25, 25, creep.memory.sourceRoom), 20);
        }
    }

    /**
     * Makes a creep get energy from different rooms.
     *
     * @param {Creep} creep
     *   The creep to run logic for.
     */
    performGoToSource(creep: MineBuilderCreep) {
        const sourcePosition = decodePosition(creep.memory.source);

        if (
            creep.pos.roomName === sourcePosition.roomName
            && this.getSource(creep)?.isDangerous()
            && creep.pos.getRangeTo(sourcePosition) <= 10
        ) {
            if (_.size(creep.room.creepsByRole.skKiller) > 0) {
                // We wait for SK killer to clean up.
                creep.whenInRange(6, sourcePosition, () => {});
            }
            else {
                // Too dangerous, return home.
                this.setReturning(creep, true);
            }
            return;
        }

        if (creep.hasCachedPath()) {
            creep.followCachedPath();
            this.performBuildRoad(creep);
            if (creep.hasArrived()) {
                creep.clearCachedPath();
            }
            else {
                return;
            }
        }
        else if (creep.pos.roomName !== sourcePosition.roomName || creep.pos.getRangeTo(sourcePosition) > 10) {
            // This creep _should_ be on a cached path!
            // It probably just spawned.
            creep.moveToRange(sourcePosition, 1);
            return;
        }

        const actionTaken = this.pickupNearbyEnergy(creep);

        if (!creep.operation) {
            // @todo Operation has probably ended. Return home and suicide?
            this.setReturning(creep, true);
            return;
        }

        // Get close to the source and then return home, building and refreshing energy as necessary.
        creep.whenInRange(2, sourcePosition, () => {
            this.setReturning(creep, true);
        });

        // Repair / build roads, even when just waiting for more energy.
        if (!actionTaken && !creep.room.isMine()) {
            this.performBuildRoad(creep);
        }
    }

    getSource(creep: MineBuilderCreep): Source {
        const sourcePosition = decodePosition(creep.memory.source);
        return creep.room.find(FIND_SOURCES, {
            filter: source => source.pos.x === sourcePosition.x && source.pos.y === sourcePosition.y,
        })[0];
    }

    /**
     * Picks up dropped energy close to this creep.
     *
     * @param {Creep} creep
     *   The creep to run logic for.
     *
     * @return {boolean}
     *   True if a pickup was made this tick.
     */
    pickupNearbyEnergy(creep: MineBuilderCreep) {
        if (creep.store.getFreeCapacity(RESOURCE_ENERGY) < 20) return false;

        // @todo Allow hauler to pick up other resources as well, but respect that
        // when returning.
        // Check if energy is on the ground nearby and pick that up.
        let resource;
        if (creep.heapMemory.energyPickupTarget) {
            resource = Game.getObjectById(creep.heapMemory.energyPickupTarget);

            if (!resource) {
                delete creep.heapMemory.energyPickupTarget;
            }
            else if (resource.pos.roomName !== creep.pos.roomName) {
                resource = null;
                delete creep.heapMemory.energyPickupTarget;
            }
        }

        if (!resource) {
            // @todo Check if there's a valid (short) path to the resource.
            const resources = creep.pos.findInRange(FIND_DROPPED_RESOURCES, 3, {
                filter: resource => resource.resourceType === RESOURCE_ENERGY && resource.amount >= 100,
            });
            if (resources.length > 0) {
                resource = resources[0];
                creep.heapMemory.energyPickupTarget = resource.id;
            }
        }

        if (resource) {
            if (creep.pos.getRangeTo(resource) > 1) {
                creep.moveToRange(resource, 1);
                return false;
            }

            creep.pickup(resource);
            return true;
        }

        return false;
    }

    /**
     * Makes the creep build a road under itself on its way home.
     *
     * @param {Creep} creep
     *   The creep to run logic for.
     *
     * @return {boolean}
     *   Whether or not an action for building this road has been taken.
     */
    performBuildRoad(creep: MineBuilderCreep) {
        const workParts = creep.getActiveBodyparts(WORK);
        if (workParts === 0) return false;

        if ((creep.store[RESOURCE_ENERGY] || 0) === 0) return false;

        if (!creep.operation) return false;

        this.actionTaken = false;

        if (creep.hasCachedPath()) {
            if (this.buildRoadOnCachedPath(creep)) return true;
        }
        else if (this.repairNearby(creep)) return true;

        // Check source container and repair that, too.
        if (this.ensureRemoteHarvestContainerIsBuilt(creep)) return true;

        if (this.buildNearby(creep)) return true;

        return false;
    }

    /**
     * Builds and repairs roads along the creep's cached path.
     *
     * @param {Creep} creep
     *   The creep to run logic for.
     *
     * @return {boolean}
     *   Whether the creep should stay on this spot for further repairs.
     */
    buildRoadOnCachedPath(creep: MineBuilderCreep) {
        // Don't try to build roads in rooms owned by other players.
        if (creep.room.controller && creep.room.controller.owner && !creep.room.isMine()) return false;

        const workParts = creep.getActiveBodyparts(WORK);
        // @todo Get rid of this direct memory access
        const pos = creep.memory.cachedPath.position;
        const path = creep.getCachedPath();

        for (let i = pos - 2; i <= pos + 2; i++) {
            if (i < 0 || i >= path.length) continue;

            const position = path[i];
            if (position.roomName !== creep.pos.roomName) continue;

            // Check for roads around the current path position to repair.
            let tileHasRoad = false;
            const structures = position.lookFor(LOOK_STRUCTURES);
            for (const structure of structures) {
                if (structure.structureType !== STRUCTURE_ROAD) continue;

                tileHasRoad = true;

                if (structure.hits < structure.hitsMax - (workParts * REPAIR_POWER)) {
                    // Many repairs to do, so stay here for next tick.
                    if (this.actionTaken) return true;

                    if (creep.repair(structure) === OK) {
                        creep.operation.addResourceCost(workParts * REPAIR_COST * REPAIR_POWER, RESOURCE_ENERGY);
                        this.actionTaken = true;
                    }

                    // If structure is especially damaged, stay here to keep repairing.
                    if (structure.hits < structure.hitsMax - (workParts * 2 * REPAIR_POWER)) {
                        return true;
                    }

                    break;
                }
            }

            // In our owned rooms, the room manager will place construction sites.
            if (creep.room.isMine()) continue;

            // Create construction site in remote rooms.
            if (!tileHasRoad && _.size(Game.constructionSites) < MAX_CONSTRUCTION_SITES * 0.7) {
                const sites = position.lookFor(LOOK_CONSTRUCTION_SITES);
                const numberSites = _.filter(Game.constructionSites, site => site.pos.roomName === position.roomName).length;
                if (sites.length === 0 && numberSites < 5 && position.createConstructionSite(STRUCTURE_ROAD) === OK) {
                    // Stay here to build the new construction site.
                    return true;
                }
            }
        }

        return false;
    }

    repairNearby(creep: MineBuilderCreep): boolean {
        const workParts = creep.getActiveBodyparts(WORK);
        const needsRepair = creep.pos.findClosestByRange(FIND_STRUCTURES, {
            filter: structure => (structure.structureType === STRUCTURE_ROAD || structure.structureType === STRUCTURE_CONTAINER) && structure.hits < structure.hitsMax - (workParts * 100),
        });
        if (needsRepair && creep.pos.getRangeTo(needsRepair) <= 3) {
            if (creep.repair(needsRepair) === OK) {
                creep.operation.addResourceCost(workParts, RESOURCE_ENERGY);
                this.actionTaken = true;
            }

            // If structure is especially damaged, stay here to keep repairing.
            if (needsRepair.hits < needsRepair.hitsMax - (workParts * 2 * 100)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Repairs or constructs a container near the source we're mining.
     *
     * @param {Creep} creep
     *   The creep to run logic for.
     *
     * @return {boolean}
     *   Whether the creep should stay on this spot for further repairs.
     */
    ensureRemoteHarvestContainerIsBuilt(creep: MineBuilderCreep) {
        if (!(creep.operation instanceof RemoteMiningOperation)) return false;
        if ((creep.store.energy || 0) === 0) return false;

        const workParts = creep.getActiveBodyparts(WORK) || 0;
        if (workParts === 0) return false;

        if (creep.operation.hasContainer(creep.memory.source)) {
            // Make sure container is in good condition.
            const container = creep.operation.getContainer(creep.memory.source);
            if (container) {
                if (creep.pos.getRangeTo(container) > 3 || container.hits > container.hitsMax - (workParts * 100)) return false;

                // Many repairs to do, so stay here for next tick.
                if (this.actionTaken) return true;

                if (creep.repair(container) === OK) {
                    creep.operation.addResourceCost(workParts, RESOURCE_ENERGY);
                    this.actionTaken = true;
                }

                // If structure is especially damaged, stay here to keep repairing.
                if (container.hits < container.hitsMax - (workParts * 2 * 100)) {
                    return true;
                }

                return false;
            }
        }

        // Check if there is a container or construction site nearby.
        const containerPosition: RoomPosition = creep.operation.getContainerPosition(creep.memory.source);
        if (!containerPosition || containerPosition.roomName !== creep.pos.roomName) return false;

        const sites = _.filter(containerPosition.lookFor(LOOK_CONSTRUCTION_SITES), site => site.structureType === STRUCTURE_CONTAINER);
        if (sites.length === 0) {
            // Place a container construction site for this source.
            containerPosition.createConstructionSite(STRUCTURE_CONTAINER);
        }

        return false;
    }

    buildNearby(creep: MineBuilderCreep): boolean {
        const workParts = creep.getActiveBodyparts(WORK);
        const needsBuilding = creep.pos.findClosestByRange(FIND_MY_CONSTRUCTION_SITES, {
            filter: site => site.structureType === STRUCTURE_CONTAINER || site.structureType === STRUCTURE_ROAD,
        });
        if (needsBuilding && creep.pos.getRangeTo(needsBuilding) <= 3) {
            if (this.actionTaken) {
                // Try again next time.
                return true;
            }

            if (creep.build(needsBuilding) === OK) {
                const buildCost = Math.min(creep.store.energy || 0, workParts * 5, needsBuilding.progressTotal - needsBuilding.progress);
                creep.operation.addResourceCost(buildCost, RESOURCE_ENERGY);
                this.actionTaken = true;
            }

            // Stay here if more building is needed.
            if (needsBuilding.progressTotal - needsBuilding.progress > workParts * 10) {
                return true;
            }
        }

        return false;
    }
}