1
0
Fork 0

Added producers and evaluators for TacticalAI

This commit is contained in:
Michaël Lemaire 2017-02-27 01:42:12 +01:00
parent 135123e7f1
commit 485d74dd7f
16 changed files with 375 additions and 98 deletions

2
.vscode/tasks.json vendored
View file

@ -18,7 +18,7 @@
"taskName": "test",
"isBuildCommand": false,
"isTestCommand": true,
"showOutput": "silent",
"showOutput": "always",
"problemMatcher": "$tsc"
}
]

View file

@ -68,6 +68,13 @@ module TS.SpaceTac {
return ichainit(imap(iarray(this.fleets), fleet => iarray(fleet.ships)));
}
/**
* Return an iterator over ships enemy of a player
*/
ienemies(player: Player, alive_only = false): Iterator<Ship> {
return ifilter(this.iships(), ship => ship.getPlayer() != player && (ship.alive || !alive_only));
}
// Check if a player is able to play
// This can be used by the UI to determine if player interaction is allowed
canPlay(player: Player): boolean {
@ -118,13 +125,7 @@ module TS.SpaceTac {
// Collect all ships within a given radius of a target
collectShipsInCircle(center: Target, radius: number, alive_only = false): Ship[] {
var result: Ship[] = [];
this.play_order.forEach(ship => {
if ((ship.alive || !alive_only) && Target.newFromShip(ship).getDistanceTo(center) <= radius) {
result.push(ship);
}
});
return result;
return imaterialize(ifilter(this.iships(), ship => (ship.alive || !alive_only) && Target.newFromShip(ship).getDistanceTo(center) <= radius));
}
// Ends a battle and sets the outcome
@ -221,9 +222,7 @@ module TS.SpaceTac {
this.turn = 0;
this.placeShips();
this.throwInitiative();
this.play_order.forEach((ship: Ship) => {
ship.startBattle();
});
iforeach(this.iships(), ship => ship.startBattle());
this.advanceToNextShip();
}

View file

@ -48,6 +48,7 @@ module TS.SpaceTac {
this.requirements = {};
this.permanent_effects = [];
this.target_effects = [];
this.action = new BaseAction("nothing", "Do nothing", false);
}
// Returns true if the equipment can be equipped on a ship

View file

@ -46,7 +46,7 @@ module TS.SpaceTac {
}
// Add a ship in this fleet
addShip(ship: Ship): Ship {
addShip(ship = new Ship()): Ship {
if (ship.fleet && ship.fleet != this) {
remove(ship.fleet.ships, ship);
}

View file

@ -3,7 +3,7 @@ module TS.SpaceTac {
/**
* A single action in the sequence result from the simulator
*/
type MoveFirePart = {
export type MoveFirePart = {
action: BaseAction
target: Target
ap: number
@ -12,7 +12,7 @@ module TS.SpaceTac {
/**
* A simulation result
*/
class MoveFireResult {
export class MoveFireResult {
// Simulation success, false only if no route can be found
success = false
// Ideal successive parts to make the full move+fire
@ -64,10 +64,11 @@ module TS.SpaceTac {
let distance = Math.sqrt(dx * dx + dy * dy);
let result = new MoveFireResult();
let ap = this.ship.values.power.get();
let action_radius = action.getRangeRadius(this.ship);
if (distance > action.getRangeRadius(this.ship)) {
if (action instanceof MoveAction || distance > action_radius) {
result.need_move = true;
let move_distance = distance - action.getRangeRadius(this.ship);
let move_distance = action instanceof MoveAction ? distance : distance - action_radius;
let move_target = new Target(this.ship.arena_x + dx * move_distance / distance, this.ship.arena_y + dy * move_distance / distance, null);
let engine = this.findBestEngine();
if (engine) {
@ -82,7 +83,7 @@ module TS.SpaceTac {
}
}
if (distance <= action.getRangeRadius(this.ship)) {
if (distance <= action_radius) {
result.success = true;
if (!(action instanceof MoveAction)) {
result.need_fire = true;

View file

@ -42,6 +42,19 @@ module TS.SpaceTac {
return equipment;
}
/**
* Add a weapon to a ship
*/
static addWeapon(ship: Ship, damage = 100, power_usage = 1, max_distance = 100, blast = 0): Equipment {
var equipment = ship.addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon));
equipment.action = new FireWeaponAction(equipment, blast != 0, "Test Weapon");
equipment.ap_usage = power_usage;
equipment.blast = blast;
equipment.distance = max_distance;
equipment.target_effects.push(new DamageEffect(damage));
return equipment;
}
// Set a ship action points, adding/updating an equipment if needed
static setShipAP(ship: Ship, points: number, recovery: number = 0): void {
var equipment = this.getOrGenEquipment(ship, SlotType.Power, new Equipments.BasicPowerCore());

View file

@ -35,29 +35,29 @@ module TS.SpaceTac {
}
}
protected customApply(battle: Battle, ship: Ship, target: Target) {
var affected: Ship[] = [];
var blast = this.getBlastRadius(ship);
/**
* Collect the effects applied by this action
*/
getEffects(battle: Battle, ship: Ship, target: Target): [Ship, BaseEffect][] {
let result: [Ship, BaseEffect][] = [];
let blast = this.getBlastRadius(ship);
let ships = blast ? battle.collectShipsInCircle(target, blast, true) : ((target.ship && target.ship.alive) ? [target.ship] : []);
ships.forEach(ship => {
this.equipment.target_effects.forEach(effect => result.push([ship, effect]));
});
return result;
}
protected customApply(battle: Battle, ship: Ship, target: Target) {
// Face the target
ship.rotate(Target.newFromShip(ship).getAngleTo(target));
// Collect affected ships
if (blast) {
affected = affected.concat(battle.collectShipsInCircle(target, blast, true));
} else if (target.ship && target.ship.alive) {
affected.push(target.ship);
}
// Fire event
ship.addBattleEvent(new FireEvent(ship, this.equipment, target));
// Apply all target effects
affected.forEach((affship: Ship) => {
this.equipment.target_effects.forEach((effect: BaseEffect) => {
effect.applyOnShip(affship);
});
});
// Apply effects
let effects = this.getEffects(battle, ship, target);
effects.forEach(([ship, effect]) => effect.applyOnShip(ship));
}
}
}

View file

@ -69,10 +69,11 @@ module TS.SpaceTac {
console.log(`${this.ai1.name} vs ${this.ai2.name} ...`);
let battle = Battle.newQuickRandom();
let playing = battle.playing_ship;
while (!battle.ended && battle.turn < 100) {
//console.debug(`Turn ${battle.turn} - Ship ${battle.play_order.indexOf(playing)}`);
let playing = battle.playing_ship;
let ai = (playing.fleet == battle.fleets[0]) ? this.ai1 : this.ai2;
ai.timer = Timer.synchronous;
ai.ship = playing;
@ -82,7 +83,6 @@ module TS.SpaceTac {
console.error(`${ai.name} did not end its turn !`);
battle.advanceToNextShip();
}
playing = battle.playing_ship;
}
if (battle.ended && !battle.outcome.draw) {

View file

@ -26,7 +26,7 @@ module TS.SpaceTac {
this.name = name || classname(this);
this.ship = ship;
this.workqueue = [];
this.random = new RandomGenerator();
this.random = RandomGenerator.global;
this.timer = timer;
}
@ -77,7 +77,7 @@ module TS.SpaceTac {
/**
* Get the time spent thinking by the AI.
*/
private getDuration() {
protected getDuration() {
return (new Date()).getTime() - this.started;
}

View file

@ -45,8 +45,7 @@ module TS.SpaceTac.Specs {
engine.ap_usage = 3;
engine.distance = 1;
ship.addSlot(SlotType.Engine).attach(engine);
ship.values.power.setMaximal(10);
ship.values.power.set(8);
TestTools.setShipAP(ship, 10);
var enemy = new Ship();
var ai = new BullyAI(ship, Timer.synchronous);
ai.ship = ship;
@ -54,6 +53,7 @@ module TS.SpaceTac.Specs {
var weapon = new Equipment(SlotType.Weapon);
weapon.ap_usage = 2;
weapon.distance = 3;
ship.addSlot(SlotType.Weapon).attach(weapon);
// enemy in range, the ship can fire without moving
ship.values.power.set(8);

View file

@ -1,6 +1,9 @@
module TS.SpaceTac {
// Ship maneuver for an artifical intelligence
// A maneuver is like a human player action, choosing an equipment and using it
/**
* Ship maneuver for an artifical intelligence
*
* A maneuver is like a human player action, choosing an equipment and using it
*/
export class Maneuver {
// Concerned ship
ship: Ship;
@ -11,17 +14,28 @@ module TS.SpaceTac {
// Target for the action;
target: Target;
// Result of move-fire simulation
simulation: MoveFireResult;
constructor(ship: Ship, equipment: Equipment, target: Target) {
this.ship = ship;
this.equipment = equipment;
this.target = target;
let simulator = new MoveFireSimulator(this.ship);
this.simulation = simulator.simulateAction(this.equipment.action, this.target);
}
// Apply the maneuver in current battle
/**
* Apply the maneuver in current battle
*/
apply(): void {
let result = this.equipment.action.apply(this.ship.getBattle(), this.ship, this.target);
if (!result) {
console.warn("AI could not apply maneuver", this);
if (this.simulation.success) {
this.simulation.parts.forEach(part => {
if (!part.action.apply(this.ship.getBattle(), this.ship, part.target)) {
console.error("AI cannot apply maneuver", this);
}
});
}
}
}

View file

@ -31,25 +31,5 @@ module TS.SpaceTac.Specs {
ai.play();
expect(applied).toEqual([7]);
});
it("produces direct weapon shots", function () {
let battle = new Battle();
let ship0a = battle.fleets[0].addShip(new Ship(null, "0A"));
let ship0b = battle.fleets[0].addShip(new Ship(null, "0B"));
let ship1a = battle.fleets[1].addShip(new Ship(null, "1A"));
let ship1b = battle.fleets[1].addShip(new Ship(null, "1B"));
let result = imaterialize(produceDirectWeapon(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));
result = imaterialize(produceDirectWeapon(ship0a, battle));
expect(result.length).toBe(4);
expect(result).toContain(new Maneuver(ship0a, weapon1, Target.newFromShip(ship1a)));
expect(result).toContain(new Maneuver(ship0a, weapon1, Target.newFromShip(ship1b)));
expect(result).toContain(new Maneuver(ship0a, weapon2, Target.newFromShip(ship1a)));
expect(result).toContain(new Maneuver(ship0a, weapon2, Target.newFromShip(ship1b)));
});
});
}

View file

@ -2,8 +2,8 @@
/// <reference path="Maneuver.ts"/>
module TS.SpaceTac {
type TacticalProducer = Iterator<Maneuver>;
type TacticalEvaluator = (Maneuver) => number;
export type TacticalProducer = Iterator<Maneuver>;
export type TacticalEvaluator = (Maneuver) => number;
/**
* AI that applies a set of tactical rules
@ -39,35 +39,39 @@ module TS.SpaceTac {
}
/**
* Single unit of work => produce a single maneuver and evaluate it
* Single unit of work => produce a batch of maneuvers and evaluate them
*
* The best produced maneuver (highest evaluation score) is kept to be played.
* If two maneuvers have nearly the same score, the best one is randomly chosen.
*/
private unitWork() {
if (this.producers.length == 0) {
return;
}
let done = 0;
// Produce a maneuver
let maneuver: Maneuver;
let producer = this.producers.shift();
[maneuver, producer] = producer();
while (done < 1000 && this.producers.length > 0) {
// Produce a maneuver
let maneuver: Maneuver;
let producer = this.producers.shift();
[maneuver, producer] = producer();
if (maneuver) {
this.producers.push(producer);
if (maneuver) {
this.producers.push(producer);
// Evaluate the maneuver
let score = this.evaluate(maneuver);
if (score > this.best_score) {
this.best = maneuver;
this.best_score = score;
// Evaluate the maneuver
let score = this.evaluate(maneuver);
//console.log(maneuver, score);
if ((Math.abs(score - this.best_score) < 0.0001 && this.random.bool()) || score > this.best_score) {
this.best = maneuver;
this.best_score = score;
}
}
done += 1;
}
// Continue or stop ?
if (this.producers.length > 0) {
// Continue or stop
if (this.producers.length > 0 && this.getDuration() < 3000) {
this.addWorkItem(() => this.unitWork());
} else if (this.best) {
// TODO Also apply after a certain time of not finding better
// TODO If not in range for action, make an approach move
this.best.apply();
}
}
@ -76,22 +80,25 @@ module TS.SpaceTac {
* Setup the default set of maneuver producers
*/
private setupDefaultProducers() {
this.producers.push(produceDirectWeapon(this.ship, this.ship.getBattle()));
let producers = [
TacticalAIHelpers.produceDirectShots,
TacticalAIHelpers.produceBlastShots,
TacticalAIHelpers.produceRandomMoves,
]
producers.forEach(producer => this.producers.push(producer(this.ship, this.ship.getBattle())));
}
/**
* Setup the default set of maneuver evaluators
*/
private setupDefaultEvaluators() {
let scaled = (evaluator: (...args) => number, factor: number) => (...args) => factor * evaluator(...args);
let evaluators = [
scaled(TacticalAIHelpers.evaluateTurnCost, 1),
scaled(TacticalAIHelpers.evaluateDamageToEnemy, 30),
scaled(TacticalAIHelpers.evaluateClustering, 3),
]
evaluators.forEach(evaluator => this.evaluators.push((maneuver: Maneuver) => evaluator(this.ship, this.ship.getBattle(), maneuver)));
}
}
/**
* Produce all "direct hit" weapon shots.
*/
export function produceDirectWeapon(ship: Ship, battle: Battle): TacticalProducer {
let enemies = ifilter(battle.iships(), iship => iship.getPlayer() != ship.getPlayer());
let weapons = iarray(ship.listEquipment(SlotType.Weapon));
return imap(icombine(enemies, weapons), ([enemy, weapon]) => new Maneuver(ship, weapon, Target.newFromShip(enemy)));
}
}

View file

@ -0,0 +1,145 @@
module TS.SpaceTac.Specs {
describe("TacticalAIHelpers", function () {
it("produces direct weapon shots", function () {
let battle = new Battle();
let ship0a = battle.fleets[0].addShip(new Ship(null, "0A"));
let ship0b = battle.fleets[0].addShip(new Ship(null, "0B"));
let ship1a = battle.fleets[1].addShip(new Ship(null, "1A"));
let ship1b = battle.fleets[1].addShip(new Ship(null, "1B"));
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));
result = imaterialize(TacticalAIHelpers.produceDirectShots(ship0a, battle));
expect(result.length).toBe(4);
expect(result).toContain(new Maneuver(ship0a, weapon1, Target.newFromShip(ship1a)));
expect(result).toContain(new Maneuver(ship0a, weapon1, Target.newFromShip(ship1b)));
expect(result).toContain(new Maneuver(ship0a, weapon2, Target.newFromShip(ship1a)));
expect(result).toContain(new Maneuver(ship0a, weapon2, Target.newFromShip(ship1b)));
});
it("produces random moves inside a grid", function () {
let battle = new Battle();
battle.width = 100;
battle.height = 100;
let ship = battle.fleets[0].addShip();
let result = imaterialize(TacticalAIHelpers.produceRandomMoves(ship, battle, 2, 1));
expect(result.length).toBe(0);
let engine = ship.addSlot(SlotType.Engine).attach(new Equipment(SlotType.Engine));
result = imaterialize(TacticalAIHelpers.produceRandomMoves(ship, battle, 2, 1, new SkewedRandomGenerator([0.5], true)));
expect(result).toEqual([
new Maneuver(ship, engine, Target.newFromLocation(25, 25)),
new Maneuver(ship, engine, Target.newFromLocation(75, 25)),
new Maneuver(ship, engine, Target.newFromLocation(25, 75)),
new Maneuver(ship, engine, Target.newFromLocation(75, 75)),
]);
});
it("produces blast shots", function () {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
let weapon = TestTools.addWeapon(ship, 50, 1, 1000, 105);
let result = imaterialize(TacticalAIHelpers.produceBlastShots(ship, battle));
expect(result.length).toBe(0);
let enemy1 = battle.fleets[1].addShip();
enemy1.setArenaPosition(500, 0);
result = imaterialize(TacticalAIHelpers.produceBlastShots(ship, battle));
expect(result.length).toBe(0);
let enemy2 = battle.fleets[1].addShip();
enemy2.setArenaPosition(700, 0);
result = imaterialize(TacticalAIHelpers.produceBlastShots(ship, battle));
expect(result).toEqual([
new Maneuver(ship, weapon, Target.newFromLocation(600, 0)),
]);
let enemy3 = battle.fleets[1].addShip();
enemy3.setArenaPosition(700, 300);
result = imaterialize(TacticalAIHelpers.produceBlastShots(ship, battle));
expect(result).toEqual([
new Maneuver(ship, weapon, Target.newFromLocation(600, 0)),
]);
});
it("evaluates turn cost", function () {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
let weapon = TestTools.addWeapon(ship, 50, 5, 100);
let engine = TestTools.addEngine(ship, 25);
let maneuver = new Maneuver(ship, weapon, Target.newFromLocation(100, 0));
expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(-Infinity);
TestTools.setShipAP(ship, 10);
expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(0.5); // 5 power remaining on 10
maneuver = new Maneuver(ship, weapon, Target.newFromLocation(110, 0));
expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(0.4); // 4 power remaining on 10
maneuver = new Maneuver(ship, weapon, Target.newFromLocation(140, 0));
expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(0.3); // 3 power remaining on 10
maneuver = new Maneuver(ship, weapon, Target.newFromLocation(310, 0));
expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(-1); // can't do in one turn
});
it("evaluates damage to enemies", function () {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
let weapon = TestTools.addWeapon(ship, 50, 5, 500, 100);
let enemy1 = battle.fleets[1].addShip();
enemy1.setArenaPosition(250, 0);
TestTools.setShipHP(enemy1, 50, 25);
let enemy2 = battle.fleets[1].addShip();
enemy2.setArenaPosition(300, 0);
TestTools.setShipHP(enemy2, 25, 0);
// no enemies hurt
let maneuver = new Maneuver(ship, weapon, Target.newFromLocation(100, 0));
expect(TacticalAIHelpers.evaluateDamageToEnemy(ship, battle, maneuver)).toEqual(0);
// one enemy loses half-life
maneuver = new Maneuver(ship, weapon, Target.newFromLocation(180, 0));
expect(TacticalAIHelpers.evaluateDamageToEnemy(ship, battle, maneuver)).toEqual(0.25);
// one enemy loses half-life, the other one is dead
maneuver = new Maneuver(ship, weapon, Target.newFromLocation(280, 0));
expect(TacticalAIHelpers.evaluateDamageToEnemy(ship, battle, maneuver)).toEqual(0.625);
});
it("evaluates ship clustering", function () {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
TestTools.addEngine(ship, 100);
TestTools.setShipAP(ship, 10);
let weapon = TestTools.addWeapon(ship, 100, 1, 100, 10);
let maneuver = new Maneuver(ship, weapon, Target.newFromLocation(200, 0));
expect(maneuver.simulation.move_location).toEqual(Target.newFromLocation(100, 0));
expect(TacticalAIHelpers.evaluateClustering(ship, battle, maneuver)).toEqual(0);
battle.fleets[1].addShip().setArenaPosition(battle.width, battle.height);
expect(TacticalAIHelpers.evaluateClustering(ship, battle, maneuver)).toBeCloseTo(-0.01, 2);
battle.fleets[1].addShip().setArenaPosition(120, 40);
expect(TacticalAIHelpers.evaluateClustering(ship, battle, maneuver)).toBeCloseTo(-0.4, 1);
battle.fleets[0].addShip().setArenaPosition(80, 60);
expect(TacticalAIHelpers.evaluateClustering(ship, battle, maneuver)).toBeCloseTo(-0.7, 1);
battle.fleets[0].addShip().setArenaPosition(110, 20);
expect(TacticalAIHelpers.evaluateClustering(ship, battle, maneuver)).toEqual(-1);
});
});
}

View file

@ -0,0 +1,110 @@
module TS.SpaceTac {
/**
* Standard producers and evaluators for TacticalAI
*
* These are static methods that may be used as base for TacticalAI ruleset.
*/
export class TacticalAIHelpers {
/**
* 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));
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 {
let engines = ship.listEquipment(SlotType.Engine);
if (engines.length == 0) {
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);
})
));
}
/**
* Produce blast weapon shots, with multiple targets.
*/
static produceBlastShots(ship: Ship, battle: Battle): TacticalProducer {
// TODO Work with groups of 3, 4 ...
let weapons = ifilter(iarray(ship.listEquipment(SlotType.Weapon)), weapon => weapon.blast > 0);
let enemies = battle.ienemies(ship.getPlayer());
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.blast * 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;
}
/**
* 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;
if (maneuver.simulation.total_fire_ap > ship.getAttribute("power_capacity")) {
return -Infinity;
} else if (powerusage > ship.getValue("power")) {
return -1;
} else {
return (ship.getValue("power") - powerusage) / ship.getAttribute("power_capacity");
}
}
/**
* Evaluate the damage done to the enemy, between -1 and 1
*/
static evaluateDamageToEnemy(ship: Ship, battle: Battle, maneuver: Maneuver): number {
let action = maneuver.equipment.action;
if (action instanceof FireWeaponAction) {
let enemies = imaterialize(battle.ienemies(ship.getPlayer(), true));
if (enemies.length == 0) {
return 0;
}
let damage = 0;
let dead = 0;
let effects = action.getEffects(battle, ship, maneuver.target);
effects.forEach(([ship, effect]) => {
if (effect instanceof DamageEffect && contains(enemies, ship)) {
let [shield, hull] = effect.getEffectiveDamage(ship);
damage += shield + hull;
if (hull == ship.getValue("hull")) {
dead += 1
}
}
});
let hp = sum(enemies.map(enemy => enemy.getValue("hull") + enemy.getValue("shield")));
let result = 0.5 * (damage / hp) + 0.5 * (dead / enemies.length);
return result;
} else {
return 0;
}
}
/**
* 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
let move_location = maneuver.simulation.move_location || Target.newFromShip(ship);
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);
//console.log(distances, result);
return result;
}
}
}
}

View file

@ -16,7 +16,10 @@ module TS.SpaceTac {
this.value = value;
}
applyOnShip(ship: Ship): boolean {
/**
* Get the effective damage done to both shield and hull (in this order)
*/
getEffectiveDamage(ship: Ship): [number, number] {
var damage = this.value;
var hull: number;
var shield: number;
@ -36,7 +39,11 @@ module TS.SpaceTac {
hull = damage;
}
// Effective damages on ship
return [shield, hull];
}
applyOnShip(ship: Ship): boolean {
let [shield, hull] = this.getEffectiveDamage(ship);
ship.addDamage(hull, shield);
return true;