Mirroar/hivemind

View on GitHub
src/manager.military.ts

Summary

Maintainability
F
4 days
Test Coverage
/* global Creep Room FIND_CREEPS BOOSTS ATTACK
RANGED_ATTACK HEAL STRUCTURE_TOWER TOWER_POWER_HEAL TOWER_POWER_ATTACK
ATTACK_POWER RANGED_ATTACK_POWER HEAL_POWER RANGED_HEAL_POWER
CARRY CLAIM MOVE TOUGH WORK TOWER_ENERGY_COST */

import hivemind from 'hivemind';

declare global {
    interface Room {
        assertMilitarySituation: () => void;
        assertMilitaryCreepPower: (creep: Creep | PowerCreep) => void;
        assertMilitaryStructurePower: (structure: Structure) => void;
        addMilitaryAssertion: (x: number, y: number, amount: number, type: string) => void;
        getMilitaryAssertion: (x: number, y: number, type: string) => number;
        assertTargetPriorities: () => void;
        getTowerTarget: () => Creep | null;
        drawMilitarySituation: () => void;

        _sitRepBuilt: boolean;
        sitRep: SitRep;
        militaryObjects: {
            creeps: Array<Creep | PowerCreep>;
            structures: Structure[];
            myCreeps: Array<Creep | PowerCreep>;
            myStructures: Structure[];
        };
    }

    interface Creep {
        getMilitaryValue: () => number;

        militaryPriority: number;
    }

    interface PowerCreep {
        getMilitaryValue: () => number;

        militaryPriority: number;
    }
}

interface SitRep {
    damage: Record<number, Record<number, number>>;
    healing: Record<number, Record<number, number>>;
    myDamage: Record<number, Record<number, number>>;
    myHealing: Record<number, Record<number, number>>;
}

/**
 * Scans the room for military targets, grades them, etc.
 */
Room.prototype.assertMilitarySituation = function (this: Room) {
    if (this._sitRepBuilt) return;

    this._sitRepBuilt = true;
    this.sitRep = {
        damage: {},
        healing: {},
        myDamage: {},
        myHealing: {},
    };

    this.militaryObjects = {
        creeps: [],
        structures: [],
        myCreeps: [],
        myStructures: [],
    };

    // @todo Look for enemy towers.
    // @todo Look for weak walls.
    // @todo Take enemy healing possibilities into account.
    // @todo Take into account that attacking melee creeps retaliates.
    // @todo Factor in boosts.

    // Parse military creeps in the room.
    const creeps = this.find(FIND_CREEPS);
    for (const creep of creeps) {
        if (creep.my) {
            // @todo Filter out civilian creeps to save on CPU.
            this.militaryObjects.myCreeps.push(creep);
        }
        else if (creep.isDangerous() && !hivemind.relations.isAlly(creep.owner.username)) {
            this.militaryObjects.creeps.push(creep);
        }
    }

    const powerCreeps = this.find(FIND_POWER_CREEPS);
    for (const powerCreep of powerCreeps) {
        if (powerCreep.my) {
            // @todo Filter out civilian creeps to save on CPU.
            this.militaryObjects.myCreeps.push(powerCreep);
        }
        else if (powerCreep.isDangerous() && !hivemind.relations.isAlly(powerCreep.owner.username)) {
            this.militaryObjects.creeps.push(powerCreep);
        }
    }

    // Parse military structures in the room.
    for (const structure of (this.structuresByType[STRUCTURE_TOWER] || [])) {
        this.assertMilitaryStructurePower(structure);
    }

    // Calculate values for all actors.
    for (const creep of this.militaryObjects.creeps) {
        this.assertMilitaryCreepPower(creep);
    }

    for (const creep of this.militaryObjects.myCreeps) {
        this.assertMilitaryCreepPower(creep);
    }

    // Determine target priorities from calculated values.
    this.assertTargetPriorities();

    // @todo Look for safe places in movement range.

    this.drawMilitarySituation();
};

/**
 * Estimate a creep's military capabilities.
 *
 * @param {Creep} creep
 *   The creep to asses.
 */
Room.prototype.assertMilitaryCreepPower = function (this: Room, creep: Creep | PowerCreep) {
    let hostile: boolean;
    let targets: Array<Creep | PowerCreep>;
    let allies: Array<Creep | PowerCreep>;
    if (creep.my) {
        hostile = false;
        targets = this.militaryObjects.creeps;
        allies = this.militaryObjects.myCreeps;
    }
    else {
        this.visual.circle(creep.pos, {
            fill: 'transparent',
            stroke: 'red',
            radius: 0.45,
        });

        hostile = true;
        targets = this.militaryObjects.myCreeps;
        allies = this.militaryObjects.creeps;
    }

    // No need to do damage / healing calculations for power creeps.
    if (!(creep instanceof Creep)) return;

    // @todo Move boosted part calculation into a creep function.
    // @todo Factor in which parts get damaged first.
    const totalParts: Partial<Record<BodyPartConstant, number>> = {};
    for (const part of creep.body) {
        if (part.hits === 0) {
            // Body part is disabled.
            continue;
        }

        let amount = 1;
        if (part.boost) {
            if (part.type === ATTACK && BOOSTS[ATTACK][part.boost].attack) {
                amount *= BOOSTS[ATTACK][part.boost].attack;
            }
            else if (part.type === RANGED_ATTACK && BOOSTS[RANGED_ATTACK][part.boost].rangedAttack) {
                amount *= BOOSTS[RANGED_ATTACK][part.boost].rangedAttack;
            }
            else if (part.type === HEAL && BOOSTS[HEAL][part.boost].heal) {
                amount *= BOOSTS[HEAL][part.boost].heal;
            }
        }

        totalParts[part.type] = (totalParts[part.type] || 0) + amount;
    }

    const assertAllTargets = (targets: Array<Creep | PowerCreep>, range: number, amount: number, type: string) => {
        if (amount <= 0) return;

        for (const target of targets) {
            const pos = target.pos;
            if (creep.pos.getRangeTo(pos) > range) continue;

            this.addMilitaryAssertion(pos.x, pos.y, amount, type);
        }
    };

    // @todo Factor in creeps with WORK parts for doing 50 structure damage per tick.
    assertAllTargets(targets, 1, ATTACK_POWER * totalParts[ATTACK], hostile ? 'damage' : 'myDamage');

    // No need to factor in potential explosion use, as it does the same
    // or less damage per tick as a ranged attack.
    assertAllTargets(targets, 3, RANGED_ATTACK_POWER * totalParts[RANGED_ATTACK], hostile ? 'damage' : 'myDamage');

    assertAllTargets(allies, 3, RANGED_HEAL_POWER * totalParts[HEAL], hostile ? 'healing' : 'myHealing');
    // We substract RANGED_HEAL_POWER so we don't inflate the actual possible
    // healing value.
    assertAllTargets(allies, 1, (HEAL_POWER - RANGED_HEAL_POWER) * totalParts[HEAL], hostile ? 'healing' : 'myHealing');
};

/**
 * Estimate a structure's military capabilities.
 *
 * @param {Structure} structure
 *   The structure to asses.
 */
Room.prototype.assertMilitaryStructurePower = function (this: Room, structure: StructureTower) {
    if (structure.store[RESOURCE_ENERGY] < TOWER_ENERGY_COST) return;
    // Don't count our towers if they're almost empty so we don't shoot at targets
    // we can't kill after all.
    if (structure.my && structure.store[RESOURCE_ENERGY] < TOWER_ENERGY_COST * 10) return;

    let hostile: boolean;
    let targets: Array<Creep | PowerCreep>;
    let allies: Array<Creep | PowerCreep>;
    if (structure.my) {
        hostile = false;
        targets = this.militaryObjects.creeps;
        allies = this.militaryObjects.myCreeps;
    }
    else {
        hostile = true;
        targets = this.militaryObjects.myCreeps;
        allies = this.militaryObjects.creeps;
    }

    for (const ally of allies) {
        const pos = ally.pos;
        const power = structure.getPowerAtRange(structure.pos.getRangeTo(pos));
        this.addMilitaryAssertion(pos.x, pos.y, power * TOWER_POWER_HEAL, hostile ? 'healing' : 'myHealing');
    }

    for (const target of targets) {
        const pos = target.pos;
        const power = structure.getPowerAtRange(structure.pos.getRangeTo(pos));
        this.addMilitaryAssertion(pos.x, pos.y, power * TOWER_POWER_ATTACK, hostile ? 'damage' : 'myDamage');
    }

    // @todo Factor repair power.
};

/**
 * Saves military estimate for a certain position.
 *
 * @param {number} x
 *   X position for which to asses the value.
 * @param {number} y
 *   Y position for which to asses the value.
 * @param {number} amount
 *   Amount by which to increment.
 * @param {string} type
 *   The type of value to save.
 */
Room.prototype.addMilitaryAssertion = function (this: Room, x: number, y: number, amount: number, type: string): void {
    if (!amount) return;
    if (x < 0 || x > 49 || y < 0 || y > 49 || amount <= 0) return;

    if (!this.sitRep[type][x]) {
        this.sitRep[type][x] = {};
    }

    this.sitRep[type][x][y] = (this.sitRep[type][x][y] || 0) + amount;
};

/**
 * Returns a military estimate for a position.
 *
 * @param {number} x
 *   X position for which to asses the value.
 * @param {number} y
 *   Y position for which to asses the value.
 * @param {string} type
 *   The type of value to get.
 *
 * @return {number}
 *   Current military assesment of the given type.
 */
Room.prototype.getMilitaryAssertion = function (this: Room, x: number, y: number, type: string) {
    if (this.sitRep[type] && this.sitRep[type][x] && this.sitRep[type][x][y]) {
        return this.sitRep[type][x][y];
    }

    return 0;
};

/**
 * Decides target priority values for all enemy creeps.
 */
Room.prototype.assertTargetPriorities = function (this: Room) {
    // @todo Use target's value / potential damage.
    for (const creep of this.militaryObjects.creeps) {
        const potentialDamage = this.getMilitaryAssertion(creep.pos.x, creep.pos.y, 'myDamage');
        const potentialHealing = this.getMilitaryAssertion(creep.pos.x, creep.pos.y, 'healing');
        // Potential damage is reduced if creep has boosted tough parts.
        const effectiveDamage = creep.getEffectiveDamage(potentialDamage);

        const visual = this.visual;

        const neededDamageFactor = creep.hits === creep.hitsMax ? 1.1 : 1;
        if (effectiveDamage > potentialHealing * neededDamageFactor) {
            // @todo Reduce priority (even stop targeting) when close to exit, to prevent tower drain by fleeing.
            creep.militaryPriority = creep.getMilitaryValue() * (effectiveDamage - potentialHealing) * (creep.hitsMax / creep.hits) * creep.ticksToLive / CREEP_LIFE_TIME;
            visual.text(creep.militaryPriority.toPrecision(2), creep.pos.x + 1, creep.pos.y + 0.2, {font: 0.5, color: 'yellow'});
        }
    }
};

/**
 * Chooses the best target for our tower to shoot at.
 *
 * @return {Creep}
 *   An enemy creep to shoot.
 */
Room.prototype.getTowerTarget = function (this: Room) {
    this.assertMilitarySituation();
    let max = null;
    for (const creep of this.militaryObjects.creeps) {
        if (!creep.militaryPriority) continue;
        if (creep.militaryPriority <= 0) continue;
        if (max && max.militaryPriority > creep.militaryPriority) continue;

        max = creep;
    }

    if (max) this.visual.circle(max.pos.x, max.pos.y, {radius: 2, fill: 'red'});

    return max;
};

/**
 * Uses RoomVisual to visualize military situation in a room.
 */
Room.prototype.drawMilitarySituation = function (this: Room) {
    const visual = this.visual;
    _.each(this.sitRep.damage, (colData, x: string) => {
        _.each(colData, (data, y: string) => {
            visual.text(data.toString(), Number(x), Number(y) - 0.1, {
                color: 'red',
                font: 0.5,
            });
        });
    });

    _.each(this.sitRep.healing, (colData, x: string) => {
        _.each(colData, (data, y: string) => {
            visual.text(data.toString(), Number(x), (Number(y)) + 0.4, {
                color: 'green',
                font: 0.5,
            });
        });
    });

    _.each(this.sitRep.myDamage, (colData, x: string) => {
        _.each(colData, (data, y: string) => {
            visual.text(data.toString(), Number(x), Number(y) - 0.1, {
                color: 'red',
                font: 0.5,
            });
        });
    });

    _.each(this.sitRep.myHealing, (colData, x: string) => {
        _.each(colData, (data, y: string) => {
            visual.text(data.toString(), Number(x), (Number(y)) + 0.4, {
                color: 'green',
                font: 0.5,
            });
        });
    });
};

const bodyPartValues = {
    [ATTACK]: 1,
    [CARRY]: 0,
    [CLAIM]: 10,
    [HEAL]: 5,
    [MOVE]: 0,
    [RANGED_ATTACK]: 2,
    [TOUGH]: 0,
    [WORK]: 1,
};

/**
 * Calculates military value of a creep.
 *
 * @return {number}
 *   The creep's perceived military value.
 */
Creep.prototype.getMilitaryValue = function (this: Creep) {
    // @todo Factor boosts.

    let value = 0;

    for (const part of this.body) {
        const factor = 0.1 + (0.9 * part.hits / 100);

        value += factor * (bodyPartValues[part.type] || 0);
    }

    return value;
};

PowerCreep.prototype.getMilitaryValue = function (this: PowerCreep) {
    return 10 + this.hits / this.hitsMax;
};

export default {

    init() {

        // @todo Add functions to Game context if necessary.

    },

};