Mirroar/hivemind

View on GitHub
src/spawn-role/brawler.ts

Summary

Maintainability
F
4 days
Test Coverage
/* global RoomPosition MOVE ATTACK HEAL RANGED_ATTACK ATTACK_POWER
RANGED_ATTACK_POWER HEAL_POWER RESOURCE_ENERGY */

import BodyBuilder from 'creep/body-builder';
import cache from 'utils/cache';
import container from 'utils/container';
import hivemind from 'hivemind';
import NavMesh from 'utils/nav-mesh';
import SpawnRole from 'spawn-role/spawn-role';
import {encodePosition, decodePosition} from 'utils/serialization';

interface BrawlerSpawnOption extends SpawnOption {
    targetPos?: string;
    pathTarget?: string;
    responseType?: number;
    operation?: string;
    trainStarter?: Id<Creep>;
    segmentType?: number;
}

declare global {
    interface RoomMemory {
        recentBrawler?: Record<string, number>;
    }
}

const RESPONSE_NONE = 0;
const RESPONSE_MINI_BRAWLER = 1;
const RESPONSE_MINI_BLINKY = 11;
const RESPONSE_FULL_BRAWLER = 2;
const RESPONSE_BLINKY = 3;
const RESPONSE_ATTACK_HEAL_TRAIN = 4;
const RESPONSE_ATTACK_BLINKY_TRAIN = 5;
const RESPONSE_RANGED_HEAL_TRAIN = 6;
const RESPONSE_RANGED_BLINKY_TRAIN = 7;
const RESPONSE_BLINKY_BLINKY_TRAIN = 8;
const RESPONSE_BLINKY_HEAL_TRAIN = 9;
const RESPONSE_ATTACKER = 10;

const SEGMENT_HEAL = 1;
const SEGMENT_BLINKY = 2;

export default class BrawlerSpawnRole extends SpawnRole {
    navMesh: NavMesh;

    /**
     * Adds brawler spawn options for the given room.
     *
     * @param {Room} room
     *   The room to add spawn options for.
     */
    getSpawnOptions(room: Room): BrawlerSpawnOption[] {
        return this.cacheEmptySpawnOptionsFor(room, 10, () => {
            const options: BrawlerSpawnOption[] = [];
            this.getRemoteDefenseSpawnOptions(room, options);
            this.getPowerHarvestDefenseSpawnOptions(room, options);
            this.getTrainPartSpawnOptions(room, options);
            this.getReclaimSpawnOptions(room, options);

            return options;
        });
    }

    /**
     * Adds brawler spawn options for remote harvest rooms.
     *
     * @param {Room} room
     *   The room to add spawn options for.
     * @param {Object[]} options
     *   A list of spawn options to add to.
     */
    getRemoteDefenseSpawnOptions(room: Room, options: BrawlerSpawnOption[]) {
        const harvestPositions: RoomPosition[] = room.getRemoteHarvestSourcePositions();
        for (const pos of harvestPositions) {
            const operation = Game.operationsByType.mining['mine:' + pos.roomName];

            // @todo If the operation has multiple source rooms, use the one
            // that has better spawn capacity or higher RCL.

            // Only spawn if there are enemies.
            if (!operation) continue;
            if (!operation.isUnderAttack() && !operation.hasInvaderCore()) continue;

            // Don't spawn simple source defenders in quick succession.
            // If they fail, there's a stronger enemy that we need to deal with
            // in a different way.
            const targetPos = encodePosition(new RoomPosition(25, 25, pos.roomName));
            const defenseTimeDiff = operation.isProfitable() ? 300 : 1500;
            if (room.memory.recentBrawler && Game.time - (room.memory.recentBrawler[targetPos] || -10_000) < defenseTimeDiff) continue;

            const brawlers = _.filter(Game.creepsByRole.brawler || [], (creep: Creep) => creep.memory.operation === 'mine:' + pos.roomName);
            if (_.size(brawlers) > 0) continue;

            const totalEnemyData = operation.getTotalEnemyData();
            const responseType = this.getDefenseCreepSize(room, totalEnemyData);

            if (responseType === RESPONSE_NONE) continue;

            if (operation.isUnderAttack() && operation.needsDismantler()) continue;

            const sourceLocation = encodePosition(pos);
            options.push({
                priority: 3,
                weight: 1,
                targetPos,
                pathTarget: sourceLocation,
                responseType,
                operation: operation.name,
            });
        }
    }

    getPowerHarvestDefenseSpawnOptions(room: Room, options: BrawlerSpawnOption[]) {
        if (!hivemind.settings.get('enablePowerMining')) return;
        if (!Memory.strategy || !Memory.strategy.power || !Memory.strategy.power.rooms) return;

        _.each(Memory.strategy.power.rooms, (info, roomName) => {
            if (!info.isActive) return;
            if (!info.spawnRooms[room.name]) return;

            const roomMemory = Memory.rooms[roomName];
            if (!roomMemory || !roomMemory.enemies) return;
            if (roomMemory.enemies.safe) return;

            const brawlers = _.filter(Game.creepsByRole.brawler || [], creep => creep.memory.target && decodePosition(creep.memory.target).roomName === roomName);
            if (_.size(brawlers) > 0) return;

            // We don't care about melee attacks, plenty of attack creeps in the
            // room when we're harvesting power.
            const enemies = _.cloneDeep(roomMemory.enemies);
            // @todo Retain information about enemy boosts affecting damage.
            enemies.damage -= (enemies.parts[ATTACK] || 0) * ATTACK_POWER * 0.9;
            if (enemies.parts[ATTACK]) enemies.parts[ATTACK] *= 0.1;

            const responseType = this.getDefenseCreepSize(room, enemies);
            if (responseType === RESPONSE_NONE) return;

            options.push({
                priority: 4,
                weight: 1,
                targetPos: encodePosition(new RoomPosition(24, 24, roomName)),
                responseType,
            });
        });
    }

    getDefenseCreepSize(room: Room, enemyData: EnemyData): number {
        // Default defense creep has 4 attack and 3 heal parts.
        const defaultAttack = 5;
        const defaultHeal = 3;

        const defaultBlinkyAttack = 6;
        const defaultBlinkyHeal = 2;

        const meleeWithHealValue = 3;
        const healValue = 5;

        const enemyPower = enemyData.damage + (enemyData.heal * healValue);
        const isRangedEnemy = (enemyData.parts[RANGED_ATTACK] || 0) > 0;

        const smallDefenderPower = (defaultAttack * ATTACK_POWER) + (defaultHeal * HEAL_POWER * meleeWithHealValue);
        const smallBlinkyPower = (defaultBlinkyAttack * RANGED_ATTACK_POWER) + (defaultBlinkyHeal * HEAL_POWER * healValue);

        // Use a reasonable attacker for destroying invader cores.
        if (enemyData.hasInvaderCore && enemyPower < smallDefenderPower) {
            return RESPONSE_ATTACKER;
        }

        // For small attackers that should be defeated easily, use simple brawler.
        if (enemyPower < smallDefenderPower && !isRangedEnemy) {
            return RESPONSE_MINI_BRAWLER;
        }

        if (enemyPower < smallBlinkyPower) {
            return RESPONSE_MINI_BLINKY;
        }

        // If damage and heal suffices, use single melee / heal creep.
        const brawlerBody = this.getBrawlerCreepBody(room);
        const numberBrawlerAttack = _.filter(brawlerBody, p => p === ATTACK).length;
        const numberBrawlerHeal = _.filter(brawlerBody, p => p === HEAL).length;
        if (!isRangedEnemy && enemyPower < (numberBrawlerAttack * ATTACK_POWER) + (numberBrawlerHeal * HEAL_POWER * meleeWithHealValue)) {
            return RESPONSE_FULL_BRAWLER;
        }

        // If damage and heal suffices, use single range / heal creep.
        const blinkyBody = this.getBlinkyCreepBody(room);
        const numberBlinkyRanged = _.filter(blinkyBody, p => p === RANGED_ATTACK).length;
        const numberBlinkyHeal = _.filter(blinkyBody, p => p === HEAL).length;
        if (enemyPower < (numberBlinkyRanged * RANGED_ATTACK_POWER) + (numberBlinkyHeal * HEAL_POWER * healValue)) {
            return RESPONSE_BLINKY;
        }

        // If needed, use 2-creep train.
        const attackBody = this.getAttackCreepBody(room);
        const rangedBody = this.getRangedCreepBody(room);
        const healBody = this.getHealCreepBody(room);
        const numberTrainAttack = _.filter(attackBody, p => p === ATTACK).length;
        const numberTrainRanged = _.filter(rangedBody, p => p === RANGED_ATTACK).length;
        const numberTrainHeal = _.filter(healBody, p => p === HEAL).length;

        if (!isRangedEnemy && enemyPower < (numberTrainAttack * ATTACK_POWER) + (numberTrainHeal * HEAL_POWER * healValue)) {
            return RESPONSE_ATTACK_HEAL_TRAIN;
        }

        if (!isRangedEnemy && enemyPower < (numberTrainAttack * ATTACK_POWER) + (numberBlinkyRanged * RANGED_ATTACK_POWER) + (numberBlinkyHeal * HEAL_POWER * healValue)) {
            return RESPONSE_ATTACK_BLINKY_TRAIN;
        }

        if (enemyPower < ((numberTrainRanged + numberBlinkyRanged) * RANGED_ATTACK_POWER) + (numberBlinkyHeal * HEAL_POWER * healValue)) {
            return RESPONSE_RANGED_BLINKY_TRAIN;
        }

        if (enemyPower < (2 * numberBlinkyRanged * RANGED_ATTACK_POWER) + (2 * numberBlinkyHeal * HEAL_POWER * healValue)) {
            return RESPONSE_BLINKY_BLINKY_TRAIN;
        }

        if (enemyPower < (numberTrainRanged * RANGED_ATTACK_POWER) + (numberTrainHeal * HEAL_POWER * healValue)) {
            return RESPONSE_RANGED_HEAL_TRAIN;
        }

        if (enemyPower < (numberBlinkyRanged * RANGED_ATTACK_POWER) + ((numberTrainHeal + numberBlinkyHeal) * HEAL_POWER * healValue)) {
            return RESPONSE_BLINKY_HEAL_TRAIN;
        }

        // @todo Otherwise, decide on spawning a quad, once we can use one.

        // If attacker too strong, don't spawn defense at all.
        return RESPONSE_NONE;
    }

    /**
     * Spawns additional segments of a creep train.
     *
     * @param {Room} room
     *   The room to add spawn options for.
     * @param {Object[]} options
     *   A list of spawn options to add to.
     */
    getTrainPartSpawnOptions(room: Room, options: BrawlerSpawnOption[]) {
        const trainStarters = _.filter(room.creepsByRole.brawler || [], (creep: Creep) => creep.memory.train && _.size(creep.memory.train.partsToSpawn) > 0);

        for (const creep of trainStarters) {
            const segmentType = creep.memory.train.partsToSpawn[0];

            options.push({
                priority: 4,
                weight: 0,
                trainStarter: creep.id,
                segmentType,
            });
        }
    }

    getReclaimSpawnOptions(room: Room, options: BrawlerSpawnOption[]) {
        if (room.getEffectiveAvailableEnergy() < 10_000) return;

        for (const targetRoom of Game.myRooms) {
            if (room.name === targetRoom.name) continue;
            if (!this.canReclaimRoom(targetRoom, room)) continue;

            // @todo Only send brawlers when the room to reclaim might be
            // attacked.
            options.push({
                priority: 4,
                weight: 0,
                targetPos: encodePosition(targetRoom.roomPlanner.getRoomCenter()),
                responseType: RESPONSE_BLINKY,
            });
        }
    }

    canReclaimRoom(targetRoom: Room, room: Room): boolean {
        if (!targetRoom.needsReclaiming()) return false;
        if (!targetRoom.isSafeForReclaiming()) return false;
        if (!targetRoom.roomPlanner) return false;

        const remoteDefense = _.filter(Game.creepsByRole.brawler, (creep: Creep) => creep.memory.target === encodePosition(targetRoom.roomPlanner.getRoomCenter())).length;
        if (remoteDefense > 3) return false;

        const route = cache.inHeap('reclaimPath:' + targetRoom.name + '.' + room.name, 100, () => {
            if (!this.navMesh) this.navMesh = new NavMesh();
            return this.navMesh.findPath(room.roomPlanner.getRoomCenter(), targetRoom.roomPlanner.getRoomCenter(), {maxPathLength: 700});
        });
        if (route.incomplete) return false;

        return true;
    }

    /**
     * Gets the body of a creep to be spawned.
     *
     * @param {Room} room
     *   The room to add spawn options for.
     * @param {Object} option
     *   The spawn option for which to generate the body.
     *
     * @return {string[]}
     *   A list of body parts the new creep should consist of.
     */
    getCreepBody(room: Room, option: BrawlerSpawnOption): BodyPartConstant[] {
        if (option.responseType) {
            switch (option.responseType) {
                case RESPONSE_MINI_BRAWLER:
                case RESPONSE_FULL_BRAWLER:
                    return this.getBrawlerCreepBody(room, option.responseType === RESPONSE_MINI_BRAWLER ? 5 : null);

                case RESPONSE_BLINKY:
                case RESPONSE_MINI_BLINKY:
                case RESPONSE_BLINKY_BLINKY_TRAIN:
                case RESPONSE_BLINKY_HEAL_TRAIN:
                    return this.getBlinkyCreepBody(room, option.responseType === RESPONSE_MINI_BLINKY ? 6 : null);

                case RESPONSE_RANGED_HEAL_TRAIN:
                case RESPONSE_RANGED_BLINKY_TRAIN:
                    return this.getRangedCreepBody(room);

                case RESPONSE_ATTACKER:
                case RESPONSE_ATTACK_HEAL_TRAIN:
                case RESPONSE_ATTACK_BLINKY_TRAIN:
                    return this.getAttackCreepBody(room);

                default:
                    return this.getBrawlerCreepBody(room);
            }
        }
        else if (option.trainStarter) {
            switch (option.segmentType) {
                case SEGMENT_HEAL:
                    return this.getHealCreepBody(room);

                default:
                    return this.getBlinkyCreepBody(room);
            }
        }

        return this.getBrawlerCreepBody(room);
    }

    getBrawlerCreepBody(room: Room, maxAttackParts?: number): BodyPartConstant[] {
        return (new BodyBuilder())
            .setWeights({[ATTACK]: 2, [HEAL]: 1})
            .setPartLimit(ATTACK, maxAttackParts)
            .setEnergyLimit(Math.min(room.energyCapacityAvailable, Math.max(room.energyCapacityAvailable * 0.9, room.energyAvailable)))
            .setMoveBufferRatio(0.4)
            .build();
    }

    getBlinkyCreepBody(room: Room, maxAttackParts?: number): BodyPartConstant[] {
        return (new BodyBuilder())
            .setWeights({[RANGED_ATTACK]: 7, [HEAL]: 3})
            .setPartLimit(RANGED_ATTACK, maxAttackParts)
            .setEnergyLimit(Math.min(room.energyCapacityAvailable, Math.max(room.energyCapacityAvailable * 0.9, room.energyAvailable)))
            .setMoveBufferRatio(0.4)
            .build();
    }

    getAttackCreepBody(room: Room): BodyPartConstant[] {
        return (new BodyBuilder())
            .setWeights({[ATTACK]: 1})
            .setEnergyLimit(Math.min(room.energyCapacityAvailable, Math.max(room.energyCapacityAvailable * 0.9, room.energyAvailable)))
            .setMoveBufferRatio(0.4)
            .build();
    }

    getRangedCreepBody(room: Room): BodyPartConstant[] {
        return (new BodyBuilder())
            .setWeights({[RANGED_ATTACK]: 1})
            .setEnergyLimit(Math.min(room.energyCapacityAvailable, Math.max(room.energyCapacityAvailable * 0.9, room.energyAvailable)))
            .setMoveBufferRatio(0.4)
            .build();
    }

    getHealCreepBody(room: Room): BodyPartConstant[] {
        return (new BodyBuilder())
            .setWeights({[HEAL]: 1})
            .setEnergyLimit(Math.min(room.energyCapacityAvailable, Math.max(room.energyCapacityAvailable * 0.9, room.energyAvailable)))
            .setMoveBufferRatio(0.4)
            .build();
    }

    /**
     * Gets memory for a new creep.
     *
     * @param {Room} room
     *   The room to add spawn options for.
     * @param {Object} option
     *   The spawn option for which to generate the body.
     *
     * @return {Object}
     *   The boost compound to use keyed by body part type.
     */
    getCreepMemory(room: Room, option: BrawlerSpawnOption): CreepMemory {
        const memory = {
            target: option.targetPos || encodePosition(room.controller.pos),
            pathTarget: option.pathTarget,
            operation: option.operation,
            disableNotifications: true,
            train: null,
        };

        switch (option.responseType) {
            case RESPONSE_ATTACK_HEAL_TRAIN:
            case RESPONSE_RANGED_HEAL_TRAIN:
            case RESPONSE_BLINKY_HEAL_TRAIN:
                memory.train = {
                    starter: true,
                    partsToSpawn: [SEGMENT_HEAL],
                };
                break;

            case RESPONSE_ATTACK_BLINKY_TRAIN:
            case RESPONSE_RANGED_BLINKY_TRAIN:
            case RESPONSE_BLINKY_BLINKY_TRAIN:
                memory.train = {
                    starter: true,
                    partsToSpawn: [SEGMENT_BLINKY],
                };
                break;

            default:
                // No other segments need spawning.
                break;
        }

        if (option.trainStarter) {
            memory.train = {
                id: option.trainStarter,
            };
        }

        return memory;
    }

    /**
     * Act when a creep belonging to this spawn role is successfully spawning.
     *
     * @param {Room} room
     *   The room the creep is spawned in.
     * @param {Object} option
     *   The spawn option which caused the spawning.
     * @param {string[]} body
     *   The body generated for this creep.
     * @param {string} name
     *   The name of the new creep.
     */
    onSpawn(room: Room, option: BrawlerSpawnOption, body: BodyPartConstant[], name: string) {
        if (option.trainStarter) {
            // Remove segment from train spawn queue.
            const creep = Game.getObjectById(option.trainStarter);
            creep.memory.train.partsToSpawn = creep.memory.train.partsToSpawn.slice(1);
        }

        if (!option.operation) return;

        const position = option.targetPos;
        if (!position) return;

        const operation = Game.operations[option.operation];
        if (operation) {
            // @todo This will probably not record costs of later parts of a train.
            operation.addResourceCost(this.calculateBodyCost(body), RESOURCE_ENERGY);
        }

        if (!room.memory.recentBrawler) room.memory.recentBrawler = {};
        room.memory.recentBrawler[position] = Game.time;

        hivemind.log('creeps', room.name).info('Spawning new brawler', name, 'to defend', position);
    }
}