2017-09-24 22:23:22 +00:00
|
|
|
module TK.SpaceTac {
|
2017-05-09 23:20:05 +00:00
|
|
|
/**
|
|
|
|
* Iterator of a list of "random" arena coordinates, based on a grid
|
|
|
|
*/
|
|
|
|
function scanArena(battle: Battle, cells = 10, random = RandomGenerator.global): Iterator<Target> {
|
|
|
|
return imap(irange(cells * cells), cellpos => {
|
|
|
|
let y = Math.floor(cellpos / cells);
|
|
|
|
let x = cellpos - y * cells;
|
|
|
|
return Target.newFromLocation((x + random.random()) * battle.width / cells, (y + random.random()) * battle.height / cells);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-05-17 22:07:16 +00:00
|
|
|
/**
|
|
|
|
* Get a list of all playable actions (like the actionbar for player) for a ship
|
|
|
|
*/
|
|
|
|
function getPlayableActions(ship: Ship): Iterator<BaseAction> {
|
|
|
|
let actions = ship.getAvailableActions();
|
|
|
|
return ifilter(iarray(actions), action => !action.checkCannotBeApplied(ship));
|
|
|
|
}
|
|
|
|
|
2017-05-18 21:10:16 +00:00
|
|
|
/**
|
|
|
|
* 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(([iship, effect]) => {
|
|
|
|
if (iship === ship) {
|
|
|
|
if (effect instanceof DamageEffect) {
|
|
|
|
let [ds, dh] = effect.getEffectiveDamage(ship);
|
|
|
|
dhull -= dh;
|
|
|
|
dshield -= ds;
|
|
|
|
} else if (effect instanceof ValueEffect) {
|
|
|
|
if (effect.valuetype == "hull") {
|
|
|
|
dhull = clamp(hull + effect.value, 0, chull) - hull;
|
|
|
|
} else if (effect.valuetype == "shield") {
|
|
|
|
dshield += clamp(shield + effect.value, 0, cshield) - shield;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (hull + dhull <= 0) {
|
|
|
|
return -1;
|
|
|
|
} else {
|
|
|
|
let diff = dhull + dshield;
|
|
|
|
return clamp(diff / (hull + shield), -1, 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-27 00:42:12 +00:00
|
|
|
/**
|
|
|
|
* Standard producers and evaluators for TacticalAI
|
|
|
|
*
|
|
|
|
* These are static methods that may be used as base for TacticalAI ruleset.
|
|
|
|
*/
|
|
|
|
export class TacticalAIHelpers {
|
2017-05-17 23:03:33 +00:00
|
|
|
/**
|
|
|
|
* Produce a turn end.
|
|
|
|
*/
|
|
|
|
static produceEndTurn(ship: Ship, battle: Battle): TacticalProducer {
|
|
|
|
return isingle(new Maneuver(ship, new EndTurnAction(), Target.newFromShip(ship)));
|
|
|
|
}
|
|
|
|
|
2017-02-27 00:42:12 +00:00
|
|
|
/**
|
|
|
|
* Produce all "direct hit" weapon shots.
|
|
|
|
*/
|
|
|
|
static produceDirectShots(ship: Ship, battle: Battle): TacticalProducer {
|
2017-05-09 23:20:05 +00:00
|
|
|
let enemies = ifilter(battle.iships(), iship => iship.alive && iship.getPlayer() !== ship.getPlayer());
|
2017-10-03 16:11:30 +00:00
|
|
|
let weapons = ifilter(getPlayableActions(ship), action => action instanceof TriggerAction);
|
2017-02-27 00:42:12 +00:00
|
|
|
return imap(icombine(enemies, weapons), ([enemy, weapon]) => new Maneuver(ship, weapon, Target.newFromShip(enemy)));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Produce random moves inside arena cell
|
|
|
|
*/
|
|
|
|
static produceRandomMoves(ship: Ship, battle: Battle, cells = 10, iterations = 1, random = RandomGenerator.global): TacticalProducer {
|
2017-05-17 22:07:16 +00:00
|
|
|
let engines = ifilter(getPlayableActions(ship), action => action instanceof MoveAction);
|
2017-05-09 23:20:05 +00:00
|
|
|
return ichainit(imap(irange(iterations), iteration => {
|
2017-05-17 22:07:16 +00:00
|
|
|
let moves = icombine(engines, scanArena(battle, cells, random));
|
|
|
|
return imap(moves, ([engine, target]) => new Maneuver(ship, engine, target));
|
2017-05-09 23:20:05 +00:00
|
|
|
}));
|
2017-02-27 00:42:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Produce blast weapon shots, with multiple targets.
|
|
|
|
*/
|
2017-05-18 16:13:07 +00:00
|
|
|
static produceInterestingBlastShots(ship: Ship, battle: Battle): TacticalProducer {
|
2017-02-27 00:42:12 +00:00
|
|
|
// TODO Work with groups of 3, 4 ...
|
2017-10-03 16:11:30 +00:00
|
|
|
let weapons = <Iterator<TriggerAction>>ifilter(getPlayableActions(ship), action => action instanceof TriggerAction && action.blast > 0);
|
2017-05-09 23:20:05 +00:00
|
|
|
let enemies = battle.ienemies(ship.getPlayer(), true);
|
|
|
|
// FIXME This produces duplicates (x, y) and (y, x)
|
2017-02-27 00:42:12 +00:00
|
|
|
let couples = ifilter(icombine(enemies, enemies), ([e1, e2]) => e1 != e2);
|
2017-10-03 16:11:30 +00:00
|
|
|
let candidates = ifilter(icombine(weapons, couples), ([weapon, [e1, e2]]) => Target.newFromShip(e1).getDistanceTo(Target.newFromShip(e2)) < weapon.blast * 2);
|
2017-02-27 00:42:12 +00:00
|
|
|
let result = imap(candidates, ([weapon, [e1, e2]]) => new Maneuver(ship, weapon, Target.newFromLocation((e1.arena_x + e2.arena_x) / 2, (e1.arena_y + e2.arena_y) / 2)));
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2017-05-18 16:13:07 +00:00
|
|
|
/**
|
|
|
|
* Produce random blast weapon shots, on a grid.
|
|
|
|
*/
|
2017-05-18 21:10:16 +00:00
|
|
|
static produceRandomBlastShots(ship: Ship, battle: Battle): TacticalProducer {
|
2017-10-03 16:11:30 +00:00
|
|
|
let weapons = ifilter(getPlayableActions(ship), action => action instanceof TriggerAction && action.blast > 0);
|
|
|
|
let candidates = ifilter(icombine(weapons, scanArena(battle)), ([weapon, location]) => (<TriggerAction>weapon).getEffects(ship, location).length > 0);
|
2017-05-18 16:13:07 +00:00
|
|
|
let result = imap(candidates, ([weapon, location]) => new Maneuver(ship, weapon, location));
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Produce interesting then random blast shots
|
|
|
|
*/
|
|
|
|
static produceBlastShots(ship: Ship, battle: Battle): TacticalProducer {
|
|
|
|
return ichain(TacticalAIHelpers.produceInterestingBlastShots(ship, battle), TacticalAIHelpers.produceRandomBlastShots(ship, battle));
|
|
|
|
}
|
|
|
|
|
2017-05-09 23:20:05 +00:00
|
|
|
/**
|
|
|
|
* Produce drone deployments.
|
|
|
|
*/
|
|
|
|
static produceDroneDeployments(ship: Ship, battle: Battle): TacticalProducer {
|
2017-05-17 22:07:16 +00:00
|
|
|
let drones = ifilter(getPlayableActions(ship), action => action instanceof DeployDroneAction);
|
2017-05-09 23:20:05 +00:00
|
|
|
let grid = scanArena(battle);
|
|
|
|
return imap(icombine(grid, drones), ([target, drone]) => new Maneuver(ship, drone, target));
|
|
|
|
}
|
|
|
|
|
2017-02-27 00:42:12 +00:00
|
|
|
/**
|
|
|
|
* 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;
|
2017-06-19 23:04:27 +00:00
|
|
|
if (powerusage == 0) {
|
|
|
|
return -1;
|
|
|
|
} else if (maneuver.simulation.total_fire_ap > ship.getAttribute("power_capacity")) {
|
2017-02-27 00:42:12 +00:00
|
|
|
return -Infinity;
|
|
|
|
} else if (powerusage > ship.getValue("power")) {
|
|
|
|
return -1;
|
|
|
|
} else {
|
|
|
|
return (ship.getValue("power") - powerusage) / ship.getAttribute("power_capacity");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-09 23:20:05 +00:00
|
|
|
/**
|
|
|
|
* Evaluate doing nothing, between -1 and 1
|
|
|
|
*/
|
|
|
|
static evaluateIdling(ship: Ship, battle: Battle, maneuver: Maneuver): number {
|
2017-06-11 20:44:12 +00:00
|
|
|
let lost = ship.getValue("power") - maneuver.getPowerUsage() + ship.getAttribute("power_generation") - ship.getAttribute("power_capacity");
|
2017-05-09 23:20:05 +00:00
|
|
|
if (lost > 0) {
|
|
|
|
return -lost / ship.getAttribute("power_capacity");
|
2017-10-03 16:11:30 +00:00
|
|
|
} else if (maneuver.action instanceof TriggerAction || maneuver.action instanceof DeployDroneAction) {
|
2017-05-18 21:10:16 +00:00
|
|
|
if (maneuver.effects.length == 0) {
|
|
|
|
return -1;
|
|
|
|
} else {
|
|
|
|
return 0.5;
|
|
|
|
}
|
2017-05-09 23:20:05 +00:00
|
|
|
} else {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-27 00:42:12 +00:00
|
|
|
/**
|
2017-05-18 21:10:16 +00:00
|
|
|
* Evaluate the effect on health for a group of ships
|
2017-02-27 00:42:12 +00:00
|
|
|
*/
|
2017-05-18 21:10:16 +00:00
|
|
|
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;
|
2017-02-27 00:42:12 +00:00
|
|
|
} else {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-17 23:17:07 +00:00
|
|
|
/**
|
2017-05-18 21:10:16 +00:00
|
|
|
* Evaluate the effect on health to the enemy, between -1 and 1
|
2017-05-17 23:17:07 +00:00
|
|
|
*/
|
2017-05-18 21:10:16 +00:00
|
|
|
static evaluateEnemyHealth(ship: Ship, battle: Battle, maneuver: Maneuver): number {
|
2017-05-17 23:17:07 +00:00
|
|
|
let enemies = imaterialize(battle.ienemies(ship.getPlayer(), true));
|
2017-05-18 21:10:16 +00:00
|
|
|
return -TacticalAIHelpers.evaluateHealthEffect(maneuver, enemies);
|
2017-05-17 23:17:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-05-18 21:10:16 +00:00
|
|
|
* Evaluate the effect on health to allied ships, between -1 and 1
|
2017-05-17 23:17:07 +00:00
|
|
|
*/
|
2017-05-18 21:10:16 +00:00
|
|
|
static evaluateAllyHealth(ship: Ship, battle: Battle, maneuver: Maneuver): number {
|
2017-05-17 23:17:07 +00:00
|
|
|
let allies = imaterialize(battle.iallies(ship.getPlayer(), true));
|
2017-05-18 21:10:16 +00:00
|
|
|
return TacticalAIHelpers.evaluateHealthEffect(maneuver, allies);
|
2017-05-17 23:17:07 +00:00
|
|
|
}
|
|
|
|
|
2017-02-27 00:42:12 +00:00
|
|
|
/**
|
|
|
|
* 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
|
2017-05-09 21:09:44 +00:00
|
|
|
let move_location = maneuver.getFinalLocation();
|
2017-02-27 00:42:12 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
2017-05-09 21:09:44 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
2017-05-18 21:10:16 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Evaluate the cost of overheating an equipment
|
|
|
|
*/
|
|
|
|
static evaluateOverheat(ship: Ship, battle: Battle, maneuver: Maneuver): number {
|
|
|
|
if (maneuver.action.equipment && maneuver.action.equipment.cooldown.willOverheat()) {
|
2017-05-22 16:29:04 +00:00
|
|
|
return -Math.min(1, 0.4 * maneuver.action.equipment.cooldown.cooling);
|
2017-05-18 21:10:16 +00:00
|
|
|
} else {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
2017-02-27 00:42:12 +00:00
|
|
|
}
|
|
|
|
}
|