AI: consider overheat and heal effects
This commit is contained in:
parent
ab8d9b4729
commit
17bee3a81f
3
TODO
3
TODO
|
@ -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
|
||||
|
|
42
src/core/ai/Maneuver.spec.ts
Normal file
42
src/core/ai/Maneuver.spec.ts
Normal 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)]
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue