231 lines
10 KiB
TypeScript
231 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 }): Iterable<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, from: IArenaLocation): Iterable<Target> {
|
|
return imap(battle.grid.iterate(from), 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, ship.location));
|
|
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, ship.location)), ([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, ship.location);
|
|
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);
|
|
}
|
|
}
|
|
} |