1
0
Fork 0

Improved tactical AI and set it as default

This commit is contained in:
Michaël Lemaire 2017-05-10 01:20:05 +02:00
parent 92129eb512
commit 33ec46e27a
8 changed files with 96 additions and 23 deletions

7
TODO
View file

@ -34,12 +34,11 @@
* Mobile: display tooltips larger and on the side of screen where the finger is not
* Mobile: targetting in two times, using a draggable target indicator
* AI: apply safety distances to move actions
* AI: use support equipments (repair drones...)
* AI: fix not being able to apply simulated maneuver
* AI: do not always move first, they are defenders
* TacticalAI: allow to play several moves in the same turn
* TacticalAI: add pauses to not play too quickly
* TacticalAI: replace BullyAI
* AI: allow to play several moves in the same turn (with pauses)
* AI: add combination of random small move and actual maneuver, as producer
* AI: evaluate based on simulated list of effects
* Map: restore fog of war
* Map: add information on current star/location + information on hovered location
* Map: generate random shops

@ -1 +1 @@
Subproject commit e3c03f16d3d1e953c1e1faffd2e28cba2c7c518c
Subproject commit 9957f153b1da5f1d30beebd670cc0a952d541304

View file

@ -221,7 +221,7 @@ module TS.SpaceTac {
if (this.playing_ship) {
if (!ai) {
// TODO Use an AI adapted to the fleet
ai = new BullyAI(this.playing_ship, this.timer);
ai = new TacticalAI(this.playing_ship, this.timer);
}
ai.play();
}

View file

@ -32,6 +32,14 @@ module TS.SpaceTac {
this.ship = ship;
}
jasmineToString() {
if (this.ship) {
return this.ship.jasmineToString();
} else {
return `(${this.x},${this.y})`;
}
}
// Constructor to target a single ship
static newFromShip(ship: Ship): Target {
return new Target(ship.arena_x, ship.arena_y, ship);
@ -43,14 +51,14 @@ module TS.SpaceTac {
}
// Get distance to another target
getDistanceTo(other: Target): number {
getDistanceTo(other: { x: number, y: number }): number {
var dx = other.x - this.x;
var dy = other.y - this.y;
return Math.sqrt(dx * dx + dy * dy);
}
// Get the normalized angle, in radians, to another target
getAngleTo(other: Target): number {
getAngleTo(other: { x: number, y: number }): number {
var dx = other.x - this.x;
var dy = other.y - this.y;
return Math.atan2(dy, dx);

View file

@ -26,6 +26,10 @@ module TS.SpaceTac {
this.simulation = simulator.simulateAction(this.equipment.action, this.target, move_margin);
}
jasmineToString() {
return `Use ${this.equipment.jasmineToString()} on ${this.target.jasmineToString()}`;
}
/**
* Apply the maneuver in current battle
*/
@ -49,5 +53,12 @@ module TS.SpaceTac {
return { x: this.ship.arena_x, y: this.ship.arena_y };
}
}
/**
* Get the total power usage of this maneuver
*/
getPowerUsage(): number {
return this.simulation.total_move_ap + this.simulation.total_fire_ap;
}
}
}

View file

@ -90,6 +90,7 @@ module TS.SpaceTac {
let producers = [
TacticalAIHelpers.produceDirectShots,
TacticalAIHelpers.produceBlastShots,
TacticalAIHelpers.produceDroneDeployments,
TacticalAIHelpers.produceRandomMoves,
]
producers.forEach(producer => this.producers.push(producer(this.ship, this.ship.getBattle() || new Battle())));
@ -103,8 +104,9 @@ module TS.SpaceTac {
let evaluators = [
scaled(TacticalAIHelpers.evaluateTurnCost, 1),
scaled(TacticalAIHelpers.evaluateDamageToEnemy, 30),
scaled(TacticalAIHelpers.evaluateClustering, 3),
scaled(TacticalAIHelpers.evaluateClustering, 8),
scaled(TacticalAIHelpers.evaluatePosition, 1),
scaled(TacticalAIHelpers.evaluateIdling, 5),
]
// TODO evaluator typing is lost
evaluators.forEach(evaluator => this.evaluators.push((maneuver: Maneuver) => evaluator(this.ship, this.ship.getBattle(), maneuver)));

View file

@ -10,8 +10,8 @@ module TS.SpaceTac.Specs {
let result = imaterialize(TacticalAIHelpers.produceDirectShots(ship0a, battle));
expect(result.length).toBe(0);
let weapon1 = ship0a.addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon));
let weapon2 = ship0a.addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon));
let weapon1 = TestTools.addWeapon(ship0a, 10);
let weapon2 = TestTools.addWeapon(ship0a, 15);
result = imaterialize(TacticalAIHelpers.produceDirectShots(ship0a, battle));
expect(result.length).toBe(4);
expect(result).toContain(new Maneuver(ship0a, weapon1, Target.newFromShip(ship1a)));
@ -60,6 +60,7 @@ module TS.SpaceTac.Specs {
result = imaterialize(TacticalAIHelpers.produceBlastShots(ship, battle));
expect(result).toEqual([
new Maneuver(ship, weapon, Target.newFromLocation(600, 0)),
new Maneuver(ship, weapon, Target.newFromLocation(600, 0)),
]);
let enemy3 = battle.fleets[1].addShip();
@ -68,6 +69,7 @@ module TS.SpaceTac.Specs {
result = imaterialize(TacticalAIHelpers.produceBlastShots(ship, battle));
expect(result).toEqual([
new Maneuver(ship, weapon, Target.newFromLocation(600, 0)),
new Maneuver(ship, weapon, Target.newFromLocation(600, 0)),
]);
});
@ -93,6 +95,28 @@ module TS.SpaceTac.Specs {
expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(-1); // can't do in one turn
});
it("evaluates the drawback of doing nothing", function () {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
TestTools.setShipAP(ship, 10, 5);
let engine = TestTools.addEngine(ship, 50);
let weapon = TestTools.addWeapon(ship, 10, 2, 100, 10);
let maneuver = new Maneuver(ship, weapon, Target.newFromLocation(0, 0));
expect(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver)).toEqual(-0.3);
maneuver = new Maneuver(ship, engine, Target.newFromLocation(0, 0));
expect(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver)).toEqual(-0.5);
ship.setValue("power", 2);
maneuver = new Maneuver(ship, weapon, Target.newFromLocation(0, 0));
expect(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver)).toEqual(0.5);
maneuver = new Maneuver(ship, engine, Target.newFromLocation(0, 0));
expect(TacticalAIHelpers.evaluateIdling(ship, battle, maneuver)).toEqual(0);
});
it("evaluates damage to enemies", function () {
let battle = new Battle();
let ship = battle.fleets[0].addShip();

View file

@ -1,4 +1,15 @@
module TS.SpaceTac {
/**
* 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);
});
}
/**
* Standard producers and evaluators for TacticalAI
*
@ -9,8 +20,8 @@ module TS.SpaceTac {
* Produce all "direct hit" weapon shots.
*/
static produceDirectShots(ship: Ship, battle: Battle): TacticalProducer {
let enemies = ifilter(battle.iships(), iship => iship.getPlayer() != ship.getPlayer());
let weapons = iarray(ship.listEquipment(SlotType.Weapon));
let enemies = ifilter(battle.iships(), iship => iship.alive && iship.getPlayer() !== ship.getPlayer());
let weapons = ifilter(iarray(ship.listEquipment(SlotType.Weapon)), weapon => weapon.action instanceof FireWeaponAction);
return imap(icombine(enemies, weapons), ([enemy, weapon]) => new Maneuver(ship, weapon, Target.newFromShip(enemy)));
}
@ -23,14 +34,9 @@ module TS.SpaceTac {
return IEMPTY;
}
return ichainit(imap(irange(iterations), iteration =>
imap(irange(cells * cells), cellpos => {
let y = Math.floor(cellpos / cells);
let x = cellpos - y * cells;
let target = Target.newFromLocation((x + random.random()) * battle.width / cells, (y + random.random()) * battle.height / cells);
return new Maneuver(ship, engines[0], target);
})
));
return ichainit(imap(irange(iterations), iteration => {
return imap(scanArena(battle, cells, random), target => new Maneuver(ship, engines[0], target))
}));
}
/**
@ -39,13 +45,23 @@ module TS.SpaceTac {
static produceBlastShots(ship: Ship, battle: Battle): TacticalProducer {
// TODO Work with groups of 3, 4 ...
let weapons = ifilter(iarray(ship.listEquipment(SlotType.Weapon)), weapon => weapon.action instanceof FireWeaponAction && weapon.action.blast > 0);
let enemies = battle.ienemies(ship.getPlayer());
let enemies = battle.ienemies(ship.getPlayer(), true);
// FIXME This produces duplicates (x, y) and (y, x)
let couples = ifilter(icombine(enemies, enemies), ([e1, e2]) => e1 != e2);
let candidates = ifilter(icombine(weapons, couples), ([weapon, [e1, e2]]) => Target.newFromShip(e1).getDistanceTo(Target.newFromShip(e2)) < weapon.action.getBlastRadius(ship) * 2);
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;
}
/**
* Produce drone deployments.
*/
static produceDroneDeployments(ship: Ship, battle: Battle): TacticalProducer {
let drones = ifilter(iarray(ship.listEquipment(SlotType.Weapon)), weapon => weapon.action instanceof DeployDroneAction);
let grid = scanArena(battle);
return imap(icombine(grid, drones), ([target, drone]) => new Maneuver(ship, drone, target));
}
/**
* Evaluate the number of turns necessary for the maneuver, between -1 and 1
*/
@ -60,6 +76,20 @@ module TS.SpaceTac {
}
}
/**
* Evaluate doing nothing, between -1 and 1
*/
static evaluateIdling(ship: Ship, battle: Battle, maneuver: Maneuver): number {
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 {
return 0;
}
}
/**
* Evaluate the damage done to the enemy, between -1 and 1
*/
@ -102,7 +132,6 @@ module TS.SpaceTac {
} else {
let factor = max([battle.width, battle.height]) * 0.01;
let result = -clamp(sum(distances.map(distance => factor / distance)), 0, 1);
//console.log(distances, result);
return result;
}
}