1
0
Fork 0
spacetac/src/core/ai/TacticalAIHelpers.ts
2018-07-17 16:58:42 +02:00

232 lines
10 KiB
TypeScript

module TK.SpaceTac {
/**
* Get a list of all playable actions (like the actionbar for player) for a ship
*/
function getPlayableActions<T extends BaseAction>(ship: Ship, type_class: { new(...args: any[]): T }): Iterator<T> {
let actions = cfilter(ship.actions.listAll(), type_class);
return ifilter(iarray(actions), action => !action.checkCannotBeApplied(ship));
}
/**
* Get the proportional effect done to a ship's health (in -1,1 range)
*/
function getProportionalHealth(maneuver: Maneuver, ship: Ship): number {
let chull = ship.getAttribute("hull_capacity");
let cshield = ship.getAttribute("shield_capacity");
let hull = ship.getValue("hull")
let shield = ship.getValue("shield");
let dhull = 0;
let dshield = 0;
maneuver.effects.forEach(diff => {
if (diff instanceof ShipValueDiff) {
if (ship.is(diff.ship_id)) {
if (diff.code == "hull") {
dhull += clamp(hull + diff.diff, 0, chull) - hull;
} else if (diff.code == "shield") {
dshield += clamp(shield + diff.diff, 0, cshield) - shield;
}
}
}
});
if (hull + dhull <= 0) {
return -1;
} else {
let diff = dhull + dshield;
return clamp(diff / (hull + shield), -1, 1);
}
}
/**
* Standard producers and evaluators for TacticalAI
*
* These are static methods that may be used as base for TacticalAI ruleset.
*/
export class TacticalAIHelpers {
/**
* Iterator of a list of arena coordinates
*/
static scanArena(battle: Battle): Iterator<Target> {
let start = { x: battle.width / 2, y: battle.height / 2 };
return imap(battle.grid.iterate(start), loc => Target.newFromLocation(loc.x, loc.y));
}
/**
* Produce a turn end.
*/
static produceEndTurn(ship: Ship, battle: Battle): TacticalProducer {
return isingle(new Maneuver(ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)));
}
/**
* Produce random moves inside arena cell
*/
static produceMoveActions(ship: Ship, battle: Battle): TacticalProducer {
let engines = getPlayableActions(ship, MoveAction);
let moves = icombine(engines, TacticalAIHelpers.scanArena(battle));
return imap(moves, ([engine, target]) => new Maneuver(ship, engine, target));
}
/**
* Produce trigger actions on ships
*/
static produceTriggerActionsOnShip(ship: Ship, battle: Battle): TacticalProducer {
let ships = battle.iships(true);
let weapons = ifilter(getPlayableActions(ship, TriggerAction), action => action.getTargettingMode(ship) == ActionTargettingMode.SHIP);
return imap(icombine(ships, weapons), ([iship, weapon]) => new Maneuver(ship, weapon, Target.newFromShip(iship)));
}
/**
* Produce trigger actions in space
*/
static produceTriggerActionsInSpace(ship: Ship, battle: Battle): TacticalProducer {
let weapons = ifilter(getPlayableActions(ship, TriggerAction), action => action.getTargettingMode(ship) == ActionTargettingMode.SPACE);
let candidates = ifilter(icombine(weapons, TacticalAIHelpers.scanArena(battle)), ([weapon, location]) => weapon.getEffects(ship, location).length > 0);
let result = imap(candidates, ([weapon, location]) => new Maneuver(ship, weapon, location));
return result;
}
/**
* Produce toggle actions at random locations.
*/
static produceToggleActions(ship: Ship, battle: Battle): TacticalProducer {
let toggles = getPlayableActions(ship, ToggleAction);
let self_toggles = ifilter(toggles, toggle => contains([ActionTargettingMode.SELF_CONFIRM, ActionTargettingMode.SELF], toggle.getTargettingMode(ship)));
let self_maneuvers = imap(self_toggles, toggle => new Maneuver(ship, toggle, Target.newFromShip(ship)));
let distant_toggles = ifilter(toggles, toggle => contains([ActionTargettingMode.SPACE, ActionTargettingMode.SURROUNDINGS], toggle.getTargettingMode(ship)));
let grid = TacticalAIHelpers.scanArena(battle);
let distant_maneuvers = imap(icombine(grid, distant_toggles), ([location, toggle]) => new Maneuver(ship, toggle, location));
return ichain(self_maneuvers, distant_maneuvers);
}
/**
* Evaluate the number of turns necessary for the maneuver, between -1 and 1
*/
static evaluateTurnCost(ship: Ship, battle: Battle, maneuver: Maneuver): number {
let powerusage = maneuver.simulation.total_move_ap + maneuver.simulation.total_fire_ap;
if (powerusage == 0) {
return -1;
} else if (maneuver.simulation.total_fire_ap > ship.getAttribute("power_capacity")) {
return -Infinity;
} else if (powerusage > ship.getValue("power")) {
return -1;
} else {
return (ship.getValue("power") - powerusage) / ship.getAttribute("power_capacity");
}
}
/**
* Evaluate doing nothing, between -1 and 1
*/
static evaluateIdling(ship: Ship, battle: Battle, maneuver: Maneuver): number {
let power_capacity = ship.getAttribute("power_capacity") || 1;
if (maneuver.action instanceof EndTurnAction) {
return -ship.getValue("power") / power_capacity;
} else if (maneuver.action instanceof TriggerAction) {
return 0.5;
} else if (maneuver.action instanceof ToggleAction) {
return ship.actions.isToggled(maneuver.action) ? -0.2 : 0.5;
} else if (maneuver.action instanceof MoveAction) {
return -(ship.getValue("power") - maneuver.getPowerUsage()) / power_capacity;
} else {
return 0;
}
}
/**
* Evaluate the effect on health for a group of ships
*/
static evaluateHealthEffect(maneuver: Maneuver, ships: Ship[]): number {
if (ships.length) {
let diffs = ships.map(ship => getProportionalHealth(maneuver, ship));
let deaths = sum(diffs.map(i => i == -1 ? 1 : 0));
return ((sum(diffs) * 0.5) - (deaths * 0.5)) / ships.length;
} else {
return 0;
}
}
/**
* Evaluate the effect on health to the enemy, between -1 and 1
*/
static evaluateEnemyHealth(ship: Ship, battle: Battle, maneuver: Maneuver): number {
let enemies = imaterialize(battle.ienemies(ship, true));
return -TacticalAIHelpers.evaluateHealthEffect(maneuver, enemies);
}
/**
* Evaluate the effect on health to allied ships, between -1 and 1
*/
static evaluateAllyHealth(ship: Ship, battle: Battle, maneuver: Maneuver): number {
let allies = imaterialize(battle.iallies(ship, true));
return TacticalAIHelpers.evaluateHealthEffect(maneuver, allies);
}
/**
* Evaluate the clustering of ships, between -1 and 1
*/
static evaluateClustering(ship: Ship, battle: Battle, maneuver: Maneuver): number {
// TODO Take into account blast radius of in-play weapons
let move_location = maneuver.getFinalLocation();
let distances = imaterialize(imap(ifilter(battle.iships(), iship => iship != ship), iship => Target.newFromShip(iship).getDistanceTo(move_location)));
if (distances.length == 0) {
return 0;
} else {
let factor = max([battle.width, battle.height]) * 0.01;
let result = -clamp(sum(distances.map(distance => factor / distance)), 0, 1);
return result;
}
}
/**
* Evaluate the global positioning of a ship on the arena, between -1 and 1
*/
static evaluatePosition(ship: Ship, battle: Battle, maneuver: Maneuver): number {
let pos = maneuver.getFinalLocation();
let distance = min([pos.x, pos.y, battle.width - pos.x, battle.height - pos.y]);
let factor = min([battle.width / 2, battle.height / 2]);
return -1 + 2 * distance / factor;
}
/**
* Evaluate the cost of overheating an equipment
*/
static evaluateOverheat(ship: Ship, battle: Battle, maneuver: Maneuver): number {
let cooldown = ship.actions.getCooldown(maneuver.action);
if (cooldown.willOverheat()) {
return -Math.min(1, 0.4 * cooldown.cooling);
} else {
return 0;
}
}
/**
* Evaluate the gain or loss of active effects
*/
static evaluateActiveEffects(ship: Ship, battle: Battle, maneuver: Maneuver): number {
let result = 0;
maneuver.effects.forEach(effect => {
if (effect instanceof ShipEffectAddedDiff || effect instanceof ShipEffectRemovedDiff) {
let target = battle.getShip(effect.ship_id);
let enemy = target && !target.isPlayedBy(ship.getPlayer());
let beneficial = effect.effect.isBeneficial();
if (effect instanceof ShipEffectRemovedDiff) {
beneficial = !beneficial;
}
// TODO Evaluate the "power" of the effect
if ((beneficial && !enemy) || (!beneficial && enemy)) {
result += 1;
} else {
result -= 1;
}
}
});
return clamp(result / battle.ships.count(), -1, 1);
}
}
}