module TK.SpaceTac { /** * Get the proportional effect done to a ship's health (in -1,1 range) */ function getHealthDiffShip(plan: AIPlan, 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; plan.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); } } /** * Evaluate the effect on health for a group of ships */ function getHealthDiffShips(plan: AIPlan, ships: Ship[]): number { if (ships.length) { let diffs = ships.map(ship => getHealthDiffShip(plan, ship)); let deaths = sum(diffs.map(i => i == -1 ? 1 : 0)); return ((sum(diffs) * 0.5) - (deaths * 0.5)) / ships.length; } else { return 0; } } /** * Functions to help scoring a turn plan produced by an AI * * Helpers should try to remain in the -1..1 range */ export class AIScoringHelpers { /** * Sum of other scoring methods */ static sum(...scorings: AIPlanScoring[]): AIPlanScoring { return plan => sum(scorings.map(scoring => scoring(plan))); } /** * Scaled version of another scoring method */ static scaled(evaluator: AIPlanScoring, factor: number): AIPlanScoring { return (plan: AIPlan) => factor * evaluator(plan); }; /** * Negatively score the fleet's remaining power, between -1 and 0 */ static remainingPower(plan: AIPlan): number { const toggled = isum(imap(plan.battle.iallies(plan.player), ship => sum(ship.getToggleActions(true).map(action => action.getPowerUsage(ship))))); const available = isum(imap(plan.battle.iallies(plan.player), ship => ship.getAttribute("power_capacity"))); const used = sum(cfilter(plan.effects, ShipActionUsedDiff).map(diff => { const ship = plan.battle.getShip(diff.ship_id); if (ship && ship.isPlayedBy(plan.player)) { const action = ship.actions.getById(diff.action); if (action) { return action.getPowerUsage(ship); } else { return 0; } } else { return 0; } })); return available ? (used - toggled - available) / available : 0; } /** * Evaluate the effect on health to the enemy, between -1 and 1 */ static healthEnemies(plan: AIPlan): number { let enemies = imaterialize(plan.battle.ienemies(plan.player, true)); return -getHealthDiffShips(plan, enemies); } /** * Evaluate the effect on health to allied ships, between -1 and 1 */ static healthAllies(plan: AIPlan): number { let allies = imaterialize(plan.battle.iallies(plan.player, true)); return getHealthDiffShips(plan, allies); } /** * Evaluate the cost of overheating equipments */ static overheat(plan: AIPlan): number { return sum(cfilter(plan.effects, ShipActionUsedDiff).map(diff => { const ship = plan.battle.getShip(diff.ship_id); if (ship && ship.isPlayedBy(plan.player)) { const action = ship.actions.getById(diff.action); if (action) { const cooldown = ship.actions.getCooldown(action); if (cooldown.willOverheat()) { return -Math.min(1, 0.4 * cooldown.cooling); } else { return 0; } } else { return 0; } } else { return 0; } })); } /** * Evaluate the gain or loss of active effects */ static activeEffects(plan: AIPlan): number { let result = 0; plan.effects.forEach(effect => { if (effect instanceof ShipEffectAddedDiff || effect instanceof ShipEffectRemovedDiff) { let target = plan.battle.getShip(effect.ship_id); let enemy = target && !target.isPlayedBy(plan.player); 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 / plan.battle.ships.count(), -1, 1); } } }