1
0
Fork 0

AI: consider overheat and heal effects

This commit is contained in:
Michaël Lemaire 2017-05-18 23:10:16 +02:00
parent ab8d9b4729
commit 17bee3a81f
8 changed files with 176 additions and 48 deletions

3
TODO
View file

@ -42,10 +42,7 @@
* Mobile: targetting in two times, using a draggable target indicator
* AI: use a first batch of producers, and only if no "good" move has been found, go on with some infinite producers
* AI: apply safety distances to move actions
* AI: do not always move first, they are defenders
* AI: add combination of random small move and actual maneuver, as producer
* AI: evaluate drone area effects
* AI: consider overheat/cooldown
* AI: new duel page with producers/evaluators tweaking
* Map: remove jump links that cross the radius of other systems
* Map: disable interaction (zoom, selection) while moving/jumping

View file

@ -0,0 +1,42 @@
module TS.SpaceTac.Specs {
describe("Maneuver", function () {
it("guesses weapon effects", function () {
let battle = new Battle();
let ship1 = battle.fleets[0].addShip();
let ship2 = battle.fleets[0].addShip();
let ship3 = battle.fleets[1].addShip();
let weapon = TestTools.addWeapon(ship1, 50, 0, 100, 10);
ship1.setArenaPosition(0, 0);
TestTools.setShipHP(ship1, 20, 20);
ship2.setArenaPosition(0, 5);
TestTools.setShipHP(ship2, 30, 30);
ship3.setArenaPosition(0, 15);
TestTools.setShipHP(ship3, 30, 30);
let maneuver = new Maneuver(ship1, weapon.action, Target.newFromLocation(0, 0));
expect(maneuver.effects).toEqual([
[ship1, new DamageEffect(50)],
[ship2, new DamageEffect(50)]
]);
});
it("guesses drone effects", function () {
let battle = new Battle();
let ship1 = battle.fleets[0].addShip();
let ship2 = battle.fleets[0].addShip();
let ship3 = battle.fleets[1].addShip();
let weapon = ship1.addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon));
weapon.action = new DeployDroneAction(weapon, 0, 100, 1, 10, [new ValueEffect("shield", 10)]);
ship1.setArenaPosition(0, 0);
TestTools.setShipHP(ship1, 20, 20);
ship2.setArenaPosition(0, 5);
TestTools.setShipHP(ship2, 30, 30);
ship3.setArenaPosition(0, 15);
TestTools.setShipHP(ship3, 30, 30);
let maneuver = new Maneuver(ship1, weapon.action, Target.newFromLocation(0, 0));
expect(maneuver.effects).toEqual([
[ship1, new ValueEffect("shield", 10)],
[ship2, new ValueEffect("shield", 10)]
]);
});
});
}

View file

@ -6,24 +6,33 @@ module TS.SpaceTac {
*/
export class Maneuver {
// Concerned ship
ship: Ship;
ship: Ship
// Reference to battle
battle: Battle
// Action to use
action: BaseAction;
action: BaseAction
// Target for the action;
target: Target;
// Target for the action
target: Target
// Result of move-fire simulation
simulation: MoveFireResult;
simulation: MoveFireResult
// List of guessed effects of this maneuver
effects: [Ship, BaseEffect][]
constructor(ship: Ship, action: BaseAction, target: Target, move_margin = 0.1) {
this.ship = ship;
this.battle = nn(ship.getBattle());
this.action = action;
this.target = target;
let simulator = new MoveFireSimulator(this.ship);
this.simulation = simulator.simulateAction(this.action, this.target, move_margin);
this.effects = this.guessEffects();
}
jasmineToString() {
@ -60,5 +69,23 @@ module TS.SpaceTac {
getPowerUsage(): number {
return this.simulation.total_move_ap + this.simulation.total_fire_ap;
}
/**
* Guess what will be the effects applied on any ship by this maneuver
*/
guessEffects(): [Ship, BaseEffect][] {
let result: [Ship, BaseEffect][] = [];
if (this.action instanceof FireWeaponAction) {
result = result.concat(this.action.getEffects(this.ship, this.target));
} else if (this.action instanceof DeployDroneAction) {
let ships = this.battle.collectShipsInCircle(this.target, this.action.effect_radius, true);
this.action.effects.forEach(effect => {
result = result.concat(ships.map(ship => <[Ship, BaseEffect]>[ship, effect]));
});
}
return result;
}
}
}

View file

@ -5,7 +5,9 @@ module TS.SpaceTac.Specs {
class FixedManeuver extends Maneuver {
score: number;
constructor(score: number) {
super(new Ship(), new BaseAction("nothing", "Do nothing", true), new Target(0, 0));
let battle = new Battle();
let ship = battle.fleets[0].addShip();
super(ship, new BaseAction("nothing", "Do nothing", true), new Target(0, 0));
this.score = score;
}
apply() {
@ -23,7 +25,8 @@ module TS.SpaceTac.Specs {
});
it("applies the highest evaluated maneuver", function () {
let ai = new TacticalAI(new Ship(), Timer.synchronous);
let battle = new Battle();
let ai = new TacticalAI(battle.fleets[0].addShip(), Timer.synchronous);
spyOn(ai, "getDefaultProducers").and.returnValue([
producer(1, -8, 4),

View file

@ -102,12 +102,13 @@ module TS.SpaceTac {
getDefaultEvaluators() {
let scaled = (evaluator: (...args: any[]) => number, factor: number) => (...args: any[]) => factor * evaluator(...args);
let evaluators = [
scaled(TacticalAIHelpers.evaluateTurnCost, 3),
scaled(TacticalAIHelpers.evaluateDamageToEnemy, 20),
scaled(TacticalAIHelpers.evaluateDamageToAllies, 30),
scaled(TacticalAIHelpers.evaluateClustering, 8),
scaled(TacticalAIHelpers.evaluateTurnCost, 1),
scaled(TacticalAIHelpers.evaluateOverheat, 10),
scaled(TacticalAIHelpers.evaluateEnemyHealth, 100),
scaled(TacticalAIHelpers.evaluateAllyHealth, 200),
scaled(TacticalAIHelpers.evaluateClustering, 3),
scaled(TacticalAIHelpers.evaluatePosition, 1),
scaled(TacticalAIHelpers.evaluateIdling, 5),
scaled(TacticalAIHelpers.evaluateIdling, 1),
]
// TODO evaluator typing is lost

View file

@ -140,15 +140,15 @@ module TS.SpaceTac.Specs {
// no enemies hurt
let maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(100, 0));
expect(TacticalAIHelpers.evaluateDamageToEnemy(ship, battle, maneuver)).toEqual(0);
expect(TacticalAIHelpers.evaluateEnemyHealth(ship, battle, maneuver)).toBeCloseTo(0, 8);
// one enemy loses half-life
maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(180, 0));
expect(TacticalAIHelpers.evaluateDamageToEnemy(ship, battle, maneuver)).toEqual(0.35);
expect(TacticalAIHelpers.evaluateEnemyHealth(ship, battle, maneuver)).toBeCloseTo(0.1666666666, 8);
// one enemy loses half-life, the other one is dead
maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(280, 0));
expect(TacticalAIHelpers.evaluateDamageToEnemy(ship, battle, maneuver)).toEqual(0.775);
expect(TacticalAIHelpers.evaluateEnemyHealth(ship, battle, maneuver)).toBeCloseTo(0.6666666666, 8);
});
it("evaluates ship clustering", function () {
@ -196,5 +196,26 @@ module TS.SpaceTac.Specs {
maneuver = new Maneuver(ship, weapon.action, new Target(0, 0), 0);
expect(TacticalAIHelpers.evaluatePosition(ship, battle, maneuver)).toEqual(1);
});
it("evaluates overheat", function () {
let battle = new Battle(undefined, undefined, 200, 100);
let ship = battle.fleets[0].addShip();
let weapon = TestTools.addWeapon(ship, 1, 1, 400);
let maneuver = new Maneuver(ship, weapon.action, new Target(0, 0));
expect(TacticalAIHelpers.evaluateOverheat(ship, battle, maneuver)).toEqual(0);
weapon.cooldown.configure(1, 0);
expect(TacticalAIHelpers.evaluateOverheat(ship, battle, maneuver)).toEqual(-0.4);
weapon.cooldown.configure(1, 1);
expect(TacticalAIHelpers.evaluateOverheat(ship, battle, maneuver)).toEqual(-0.8);
weapon.cooldown.configure(1, 2);
expect(TacticalAIHelpers.evaluateOverheat(ship, battle, maneuver)).toEqual(-1);
weapon.cooldown.configure(2, 0);
expect(TacticalAIHelpers.evaluateOverheat(ship, battle, maneuver)).toEqual(0);
});
});
}

View file

@ -18,6 +18,41 @@ module TS.SpaceTac {
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(([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);
}
}
/**
* Standard producers and evaluators for TacticalAI
*
@ -68,9 +103,9 @@ module TS.SpaceTac {
/**
* Produce random blast weapon shots, on a grid.
*/
static produceRandomBlastShots(ship: Ship, battle: Battle, cells = 20, iterations = 2, random = RandomGenerator.global): TacticalProducer {
static produceRandomBlastShots(ship: Ship, battle: Battle): TacticalProducer {
let weapons = ifilter(getPlayableActions(ship), action => action instanceof FireWeaponAction && action.blast > 0);
let candidates = icombine(weapons, scanArena(battle, cells, random));
let candidates = ifilter(icombine(weapons, scanArena(battle)), ([weapon, location]) => (<FireWeaponAction>weapon).getEffects(ship, location).length > 0);
let result = imap(candidates, ([weapon, location]) => new Maneuver(ship, weapon, location));
return result;
}
@ -112,53 +147,44 @@ module TS.SpaceTac {
let lost = ship.getValue("power") - maneuver.getPowerUsage() + ship.getAttribute("power_recovery") - ship.getAttribute("power_capacity");
if (lost > 0) {
return -lost / ship.getAttribute("power_capacity");
} else if (maneuver.simulation.need_fire) {
return 0.5;
} else if (maneuver.action instanceof FireWeaponAction || maneuver.action instanceof DeployDroneAction) {
if (maneuver.effects.length == 0) {
return -1;
} else {
return 0.5;
}
} else {
return 0;
}
}
/**
* Evaluate the damage done to a set of ships, between -1 and 1
* Evaluate the effect on health for a group of ships
*/
static evaluateDamage(ship: Ship, battle: Battle, maneuver: Maneuver, others: Ship[]): number {
let action = maneuver.action;
if (action instanceof FireWeaponAction) {
let damage = 0;
let dead = 0;
let effects = action.getEffects(ship, maneuver.target);
effects.forEach(([other, effect]) => {
if (effect instanceof DamageEffect && contains(others, other)) {
let [shield, hull] = effect.getEffectiveDamage(other);
damage += shield + hull;
if (hull == other.getValue("hull")) {
dead += 1
}
}
});
let hp = sum(others.map(other => other.getValue("hull") + other.getValue("shield")));
let result = (damage ? 0.2 : 0) + 0.3 * (damage / hp) + (dead ? 0.2 : 0) + 0.3 * (dead / others.length);
return result;
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 damage done to the enemy, between -1 and 1
* Evaluate the effect on health to the enemy, between -1 and 1
*/
static evaluateDamageToEnemy(ship: Ship, battle: Battle, maneuver: Maneuver): number {
static evaluateEnemyHealth(ship: Ship, battle: Battle, maneuver: Maneuver): number {
let enemies = imaterialize(battle.ienemies(ship.getPlayer(), true));
return TacticalAIHelpers.evaluateDamage(ship, battle, maneuver, enemies);
return -TacticalAIHelpers.evaluateHealthEffect(maneuver, enemies);
}
/**
* Evaluate the damage done to allied ships, between -1 and 1
* Evaluate the effect on health to allied ships, between -1 and 1
*/
static evaluateDamageToAllies(ship: Ship, battle: Battle, maneuver: Maneuver): number {
static evaluateAllyHealth(ship: Ship, battle: Battle, maneuver: Maneuver): number {
let allies = imaterialize(battle.iallies(ship.getPlayer(), true));
return -TacticalAIHelpers.evaluateDamage(ship, battle, maneuver, allies);
return TacticalAIHelpers.evaluateHealthEffect(maneuver, allies);
}
/**
@ -186,5 +212,16 @@ module TS.SpaceTac {
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 {
if (maneuver.action.equipment && maneuver.action.equipment.cooldown.willOverheat()) {
return -Math.min(1, 0.4 * (maneuver.action.equipment.cooldown.cooling + 1));
} else {
return 0;
}
}
}
}

View file

@ -16,7 +16,7 @@ module TS.SpaceTac.UI {
this.parent = parent;
this.circle = new Phaser.Graphics(this.game, 0, 0);
this.addChild(this.circle);
this.add(this.circle);
this.primary = null;
}