Continued work on restoring AI feature
This commit is contained in:
parent
b3fa27cb0e
commit
65f2008fd7
|
@ -230,14 +230,14 @@ module TK {
|
||||||
/**
|
/**
|
||||||
* Type filter, to return a list of instances of a given type
|
* Type filter, to return a list of instances of a given type
|
||||||
*/
|
*/
|
||||||
export function tfilter<T>(array: any[], filter: (item: any) => item is T): T[] {
|
export function tfilter<T>(array: ReadonlyArray<any>, filter: (item: any) => item is T): T[] {
|
||||||
return array.filter(filter);
|
return array.filter(filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class filter, to return a list of instances of a given type
|
* Class filter, to return a list of instances of a given type
|
||||||
*/
|
*/
|
||||||
export function cfilter<T>(array: any[], classref: { new(...args: any[]): T }): T[] {
|
export function cfilter<T>(array: ReadonlyArray<any>, classref: { new(...args: any[]): T }): T[] {
|
||||||
return array.filter((item): item is T => item instanceof classref);
|
return array.filter((item): item is T => item instanceof classref);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
29
src/core/ai/AIPlanProducers.spec.ts
Normal file
29
src/core/ai/AIPlanProducers.spec.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
module TK.SpaceTac.Specs {
|
||||||
|
testing("AIProducerTools", test => {
|
||||||
|
test.case("produces random moves", check => {
|
||||||
|
const battle = new Battle();
|
||||||
|
const ship = battle.fleets[0].addShip();
|
||||||
|
const engine = TestTools.addEngine(ship, 250);
|
||||||
|
|
||||||
|
const planning = new TurnPlanning(battle, ship.getPlayer());
|
||||||
|
|
||||||
|
check.in("random=0", check => {
|
||||||
|
AIProducerTools.addMove(planning, ship, engine, new SkewedRandomGenerator([0], true));
|
||||||
|
const plan = planning.getShipPlan(ship);
|
||||||
|
check.equals(plan.actions.length, 1);
|
||||||
|
check.equals(plan.actions[0].action, engine.id);
|
||||||
|
check.nears(nn(plan.actions[0].distance), engine.min_distance);
|
||||||
|
check.nears(nn(plan.actions[0].angle), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
check.in("random=1", check => {
|
||||||
|
AIProducerTools.addMove(planning, ship, engine, new SkewedRandomGenerator([1], true));
|
||||||
|
const plan = planning.getShipPlan(ship);
|
||||||
|
check.equals(plan.actions.length, 1);
|
||||||
|
check.equals(plan.actions[0].action, engine.id);
|
||||||
|
check.nears(nn(plan.actions[0].distance), 250);
|
||||||
|
check.nears(nn(plan.actions[0].angle), Math.PI * 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
76
src/core/ai/AIPlanProducers.ts
Normal file
76
src/core/ai/AIPlanProducers.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
module TK.SpaceTac {
|
||||||
|
export const AIProducerTools = <const>{
|
||||||
|
/**
|
||||||
|
* Add a random move action
|
||||||
|
*/
|
||||||
|
addMove(planning: TurnPlanning, ship: Ship, action: MoveAction, random: RandomGenerator): void {
|
||||||
|
const distance = random.random() * (action.max_distance - action.min_distance) + action.min_distance;
|
||||||
|
const angle = random.random() * Math.PI * 2;
|
||||||
|
planning.addAction(ship, action, distance, angle);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a random active or passive action
|
||||||
|
*/
|
||||||
|
addAction(planning: TurnPlanning, ship: Ship, action: BaseAction, random: RandomGenerator): void {
|
||||||
|
const range = action.getRangeRadius(ship);
|
||||||
|
if (range) {
|
||||||
|
const distance = 1 - random.random() * range;
|
||||||
|
const angle = random.random() * Math.PI * 2;
|
||||||
|
planning.addAction(ship, action, distance, angle);
|
||||||
|
} else {
|
||||||
|
planning.addAction(ship, action);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single random plan
|
||||||
|
*/
|
||||||
|
getRandomPlan(battle: Battle, player: Player, random: RandomGenerator): AIPlan {
|
||||||
|
const planning = new TurnPlanning(battle, player);
|
||||||
|
|
||||||
|
for (let ship of battle.ships.iterator()) {
|
||||||
|
if (ship.isPlayedBy(player)) {
|
||||||
|
// Passive actions
|
||||||
|
for (let action of ship.actions.listAll().filter(action => action.getCategory() == ActionCategory.PASSIVE)) {
|
||||||
|
if (random.bool()) {
|
||||||
|
this.addAction(planning, ship, action, random);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move
|
||||||
|
for (let action of ship.actions.listAll()) {
|
||||||
|
if (action instanceof MoveAction) {
|
||||||
|
if (random.bool()) {
|
||||||
|
this.addMove(planning, ship, action, random);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active action
|
||||||
|
for (let action of ship.actions.listAll().filter(action => action.getCategory() == ActionCategory.ACTIVE)) {
|
||||||
|
if (random.bool()) {
|
||||||
|
this.addAction(planning, ship, action, random);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AIPlan(planning.getTurnPlan(), battle, player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AIPlanProducers: { [name: string]: (...args: any[]) => AIPlanProducer } = {
|
||||||
|
random: (battle: Battle, player: Player, gen = RandomGenerator.global) => {
|
||||||
|
const builder = () => AIProducerTools.getRandomPlan(battle, player, gen);
|
||||||
|
function* producer() {
|
||||||
|
while (true) {
|
||||||
|
yield builder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return producer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,21 +1,29 @@
|
||||||
module TK.SpaceTac.Specs {
|
module TK.SpaceTac.Specs {
|
||||||
testing("AIScoringHelpers", test => {
|
testing("AIScoringHelpers", test => {
|
||||||
test.case("evaluates the drawback of doing nothing", check => {
|
test.case("evaluates the drawback of leaving power untouched", check => {
|
||||||
let battle = new Battle();
|
const battle = new Battle();
|
||||||
let ship = battle.fleets[0].addShip();
|
const ship = battle.fleets[0].addShip();
|
||||||
TestTools.setShipModel(ship, 100, 0, 10);
|
TestTools.setShipModel(ship, 100, 0, 10);
|
||||||
let weapon = TestTools.addWeapon(ship, 10, 2, 100, 10);
|
const weapon = TestTools.addWeapon(ship, 10, 2, 100, 10, 10);
|
||||||
let toggle = ship.actions.addCustom(new ToggleAction("test"));
|
const toggle = ship.actions.addCustom(new ToggleAction("test"));
|
||||||
|
const enemy = battle.fleets[1].addShip();
|
||||||
|
TestTools.setShipModel(enemy, 100, 0, 13);
|
||||||
|
|
||||||
let plan = planSingleAction(ship, weapon, Target.newFromLocation(0, 0));
|
let plan = new AIPlan(undefined, battle, ship.getPlayer());
|
||||||
check.equals(AIScoringHelpers.evaluateIdling(plan), 0.5, "fire");
|
check.equals(AIScoringHelpers.remainingPower(plan), -1, "no action");
|
||||||
|
|
||||||
|
plan = planSingleAction(ship, weapon, Target.newFromLocation(50, 0));
|
||||||
|
check.equals(AIScoringHelpers.remainingPower(plan), -0.8, "fire");
|
||||||
|
|
||||||
plan = planSingleAction(ship, toggle, Target.newFromShip(ship));
|
plan = planSingleAction(ship, toggle, Target.newFromShip(ship));
|
||||||
check.equals(AIScoringHelpers.evaluateIdling(plan), 0.5, "toggle on");
|
check.equals(AIScoringHelpers.remainingPower(plan), -0.9, "toggle on");
|
||||||
|
|
||||||
ship.actions.toggle(toggle, true);
|
ship.actions.toggle(toggle, true);
|
||||||
|
plan = new AIPlan(undefined, battle, ship.getPlayer());
|
||||||
|
check.equals(AIScoringHelpers.remainingPower(plan), -0.9, "toggle kept on");
|
||||||
|
|
||||||
plan = planSingleAction(ship, toggle, Target.newFromShip(ship));
|
plan = planSingleAction(ship, toggle, Target.newFromShip(ship));
|
||||||
check.equals(AIScoringHelpers.evaluateIdling(plan), -0.2, "toggle off");
|
check.equals(AIScoringHelpers.remainingPower(plan), -1, "toggle off");
|
||||||
});
|
});
|
||||||
|
|
||||||
test.case("evaluates damage to enemies", check => {
|
test.case("evaluates damage to enemies", check => {
|
||||||
|
@ -32,90 +40,46 @@ module TK.SpaceTac.Specs {
|
||||||
|
|
||||||
// no enemies hurt
|
// no enemies hurt
|
||||||
let plan = planSingleAction(ship, action, Target.newFromLocation(100, 0));
|
let plan = planSingleAction(ship, action, Target.newFromLocation(100, 0));
|
||||||
check.nears(AIScoringHelpers.evaluateEnemyHealth(plan), 0, 8);
|
check.nears(AIScoringHelpers.healthEnemies(plan), 0, 8);
|
||||||
|
|
||||||
// one enemy loses half-life
|
// one enemy loses half-life
|
||||||
plan = planSingleAction(ship, action, Target.newFromLocation(180, 0));
|
plan = planSingleAction(ship, action, Target.newFromLocation(180, 0));
|
||||||
check.nears(AIScoringHelpers.evaluateEnemyHealth(plan), 0.1666666666, 8);
|
check.nears(AIScoringHelpers.healthEnemies(plan), 0.1666666666, 8);
|
||||||
|
|
||||||
// one enemy loses half-life, the other one is dead
|
// one enemy loses half-life, the other one is dead
|
||||||
plan = planSingleAction(ship, action, Target.newFromLocation(280, 0));
|
plan = planSingleAction(ship, action, Target.newFromLocation(280, 0));
|
||||||
check.nears(AIScoringHelpers.evaluateEnemyHealth(plan), 0.6666666666, 8);
|
check.nears(AIScoringHelpers.healthEnemies(plan), 0.6666666666, 8);
|
||||||
});
|
|
||||||
|
|
||||||
test.case("evaluates ship clustering", check => {
|
|
||||||
let battle = new Battle();
|
|
||||||
let ship = battle.fleets[0].addShip();
|
|
||||||
TestTools.setShipModel(ship, 100, 0, 10);
|
|
||||||
TestTools.addEngine(ship, 1000);
|
|
||||||
let weapon = TestTools.addWeapon(ship, 100, 1, 100, 10);
|
|
||||||
|
|
||||||
let plan = planSingleAction(ship, weapon, Target.newFromLocation(200, 0));
|
|
||||||
check.equals(AIScoringHelpers.evaluateClustering(plan), 0);
|
|
||||||
|
|
||||||
battle.fleets[1].addShip().setArenaPosition(battle.width, battle.height);
|
|
||||||
check.nears(AIScoringHelpers.evaluateClustering(plan), -0.01, 2);
|
|
||||||
|
|
||||||
battle.fleets[1].addShip().setArenaPosition(120, 40);
|
|
||||||
check.nears(AIScoringHelpers.evaluateClustering(plan), -0.4, 1);
|
|
||||||
|
|
||||||
battle.fleets[0].addShip().setArenaPosition(80, 60);
|
|
||||||
check.nears(AIScoringHelpers.evaluateClustering(plan), -0.7, 1);
|
|
||||||
|
|
||||||
battle.fleets[0].addShip().setArenaPosition(110, 20);
|
|
||||||
check.equals(AIScoringHelpers.evaluateClustering(plan), -1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("evaluates ship position", check => {
|
|
||||||
let battle = new Battle(undefined, undefined, 200, 100);
|
|
||||||
let ship = battle.fleets[0].addShip();
|
|
||||||
let weapon = TestTools.addWeapon(ship, 1, 1, 400);
|
|
||||||
let action = weapon;
|
|
||||||
|
|
||||||
ship.setArenaPosition(0, 0);
|
|
||||||
let plan = planSingleAction(ship, action, new Target(0, 0));
|
|
||||||
check.equals(AIScoringHelpers.evaluatePosition(plan), -1);
|
|
||||||
|
|
||||||
ship.setArenaPosition(100, 0);
|
|
||||||
plan = planSingleAction(ship, action, new Target(0, 0));
|
|
||||||
check.equals(AIScoringHelpers.evaluatePosition(plan), -1);
|
|
||||||
|
|
||||||
ship.setArenaPosition(100, 10);
|
|
||||||
plan = planSingleAction(ship, action, new Target(0, 0));
|
|
||||||
check.equals(AIScoringHelpers.evaluatePosition(plan), -0.6);
|
|
||||||
|
|
||||||
ship.setArenaPosition(100, 50);
|
|
||||||
plan = planSingleAction(ship, action, new Target(0, 0));
|
|
||||||
check.equals(AIScoringHelpers.evaluatePosition(plan), 1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test.case("evaluates overheat", check => {
|
test.case("evaluates overheat", check => {
|
||||||
let battle = new Battle(undefined, undefined, 200, 100);
|
let battle = new Battle(undefined, undefined, 200, 100);
|
||||||
let ship = battle.fleets[0].addShip();
|
let ship = battle.fleets[0].addShip();
|
||||||
let weapon = TestTools.addWeapon(ship, 1, 1, 400);
|
let weapon = TestTools.addWeapon(ship, 1, 1, 400, 10);
|
||||||
|
let enemy = battle.fleets[1].addShip();
|
||||||
|
TestTools.addWeapon(enemy, 1, 1, 400);
|
||||||
|
|
||||||
let plan = planSingleAction(ship, weapon, new Target(0, 0));
|
let plan = planSingleAction(ship, weapon, new Target(0, 50));
|
||||||
check.equals(AIScoringHelpers.evaluateOverheat(plan), 0);
|
check.equals(AIScoringHelpers.overheat(plan), 0);
|
||||||
|
|
||||||
weapon.configureCooldown(1, 1);
|
weapon.configureCooldown(1, 1);
|
||||||
ship.actions.updateFromShip(ship);
|
ship.actions.updateFromShip(ship);
|
||||||
ship.actions.addCustom(weapon);
|
ship.actions.addCustom(weapon);
|
||||||
check.equals(AIScoringHelpers.evaluateOverheat(plan), -0.4);
|
check.equals(AIScoringHelpers.overheat(plan), -0.4);
|
||||||
|
|
||||||
weapon.configureCooldown(1, 2);
|
weapon.configureCooldown(1, 2);
|
||||||
ship.actions.updateFromShip(ship);
|
ship.actions.updateFromShip(ship);
|
||||||
ship.actions.addCustom(weapon);
|
ship.actions.addCustom(weapon);
|
||||||
check.equals(AIScoringHelpers.evaluateOverheat(plan), -0.8);
|
check.equals(AIScoringHelpers.overheat(plan), -0.8);
|
||||||
|
|
||||||
weapon.configureCooldown(1, 3);
|
weapon.configureCooldown(1, 3);
|
||||||
ship.actions.updateFromShip(ship);
|
ship.actions.updateFromShip(ship);
|
||||||
ship.actions.addCustom(weapon);
|
ship.actions.addCustom(weapon);
|
||||||
check.equals(AIScoringHelpers.evaluateOverheat(plan), -1);
|
check.equals(AIScoringHelpers.overheat(plan), -1);
|
||||||
|
|
||||||
weapon.configureCooldown(2, 1);
|
weapon.configureCooldown(2, 1);
|
||||||
ship.actions.updateFromShip(ship);
|
ship.actions.updateFromShip(ship);
|
||||||
ship.actions.addCustom(weapon);
|
ship.actions.addCustom(weapon);
|
||||||
check.equals(AIScoringHelpers.evaluateOverheat(plan), 0);
|
check.equals(AIScoringHelpers.overheat(plan), 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.case("evaluates active effects", check => {
|
test.case("evaluates active effects", check => {
|
||||||
|
@ -124,36 +88,37 @@ module TK.SpaceTac.Specs {
|
||||||
let enemy = battle.fleets[1].ships[0];
|
let enemy = battle.fleets[1].ships[0];
|
||||||
TestTools.setShipModel(ship, 5, 0, 1);
|
TestTools.setShipModel(ship, 5, 0, 1);
|
||||||
TestTools.setShipModel(enemy, 5, 5);
|
TestTools.setShipModel(enemy, 5, 5);
|
||||||
let action = new TriggerAction("Test", { range: 100, power: 1 });
|
enemy.setArenaPosition(50, 0);
|
||||||
|
let action = new TriggerAction("Test", { range: 100, power: 1, blast: 10 });
|
||||||
ship.actions.addCustom(action);
|
ship.actions.addCustom(action);
|
||||||
|
|
||||||
let plan = planSingleAction(ship, action, Target.newFromShip(enemy));
|
let plan = planSingleAction(ship, action, Target.newFromShip(enemy));
|
||||||
check.equals(AIScoringHelpers.evaluateActiveEffects(plan), 0);
|
check.equals(AIScoringHelpers.activeEffects(plan), 0);
|
||||||
|
|
||||||
action.effects = [new StickyEffect(new DamageEffect(1), 1)];
|
action.effects = [new StickyEffect(new DamageEffect(1), 1)];
|
||||||
plan = planSingleAction(ship, action, Target.newFromShip(enemy));
|
plan = planSingleAction(ship, action, Target.newFromShip(enemy));
|
||||||
check.nears(AIScoringHelpers.evaluateActiveEffects(plan), 0.5);
|
check.nears(AIScoringHelpers.activeEffects(plan), 0.5);
|
||||||
|
|
||||||
plan = planSingleAction(ship, action, Target.newFromShip(ship));
|
plan = planSingleAction(ship, action, Target.newFromLocation(4, 0));
|
||||||
check.nears(AIScoringHelpers.evaluateActiveEffects(plan), -0.5);
|
check.nears(AIScoringHelpers.activeEffects(plan), -0.5);
|
||||||
|
|
||||||
action.effects = [new StickyEffect(new CooldownEffect(1), 1)];
|
action.effects = [new StickyEffect(new CooldownEffect(1), 1)];
|
||||||
plan = planSingleAction(ship, action, Target.newFromShip(enemy));
|
plan = planSingleAction(ship, action, Target.newFromShip(enemy));
|
||||||
check.nears(AIScoringHelpers.evaluateActiveEffects(plan), -0.5);
|
check.nears(AIScoringHelpers.activeEffects(plan), -0.5);
|
||||||
|
|
||||||
plan = planSingleAction(ship, action, Target.newFromShip(ship));
|
plan = planSingleAction(ship, action, Target.newFromLocation(4, 0));
|
||||||
check.nears(AIScoringHelpers.evaluateActiveEffects(plan), 0.5);
|
check.nears(AIScoringHelpers.activeEffects(plan), 0.5);
|
||||||
|
|
||||||
battle.fleets[0].addShip();
|
battle.fleets[0].addShip();
|
||||||
check.nears(AIScoringHelpers.evaluateActiveEffects(plan), 0.3333333333333333);
|
check.nears(AIScoringHelpers.activeEffects(plan), 0.3333333333333333);
|
||||||
|
|
||||||
action.effects = [new StickyEffect(new CooldownEffect(1), 1), new StickyEffect(new CooldownEffect(1), 1)];
|
action.effects = [new StickyEffect(new CooldownEffect(1), 1), new StickyEffect(new CooldownEffect(1), 1)];
|
||||||
plan = planSingleAction(ship, action, Target.newFromShip(enemy));
|
plan = planSingleAction(ship, action, Target.newFromShip(enemy));
|
||||||
check.nears(AIScoringHelpers.evaluateActiveEffects(plan), -0.6666666666666666);
|
check.nears(AIScoringHelpers.activeEffects(plan), -0.6666666666666666);
|
||||||
|
|
||||||
action.effects = range(10).map(() => new StickyEffect(new CooldownEffect(1), 1));
|
action.effects = range(10).map(() => new StickyEffect(new CooldownEffect(1), 1));
|
||||||
plan = planSingleAction(ship, action, Target.newFromShip(enemy));
|
plan = planSingleAction(ship, action, Target.newFromShip(enemy));
|
||||||
check.nears(AIScoringHelpers.evaluateActiveEffects(plan), -1);
|
check.nears(AIScoringHelpers.activeEffects(plan), -1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ module TK.SpaceTac {
|
||||||
/**
|
/**
|
||||||
* Get the proportional effect done to a ship's health (in -1,1 range)
|
* Get the proportional effect done to a ship's health (in -1,1 range)
|
||||||
*/
|
*/
|
||||||
function getProportionalHealth(plan: AIPlan, ship: Ship): number {
|
function getHealthDiffShip(plan: AIPlan, ship: Ship): number {
|
||||||
let chull = ship.getAttribute("hull_capacity");
|
let chull = ship.getAttribute("hull_capacity");
|
||||||
let cshield = ship.getAttribute("shield_capacity");
|
let cshield = ship.getAttribute("shield_capacity");
|
||||||
let hull = ship.getValue("hull")
|
let hull = ship.getValue("hull")
|
||||||
|
@ -30,24 +30,12 @@ module TK.SpaceTac {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Functions to help scoring a turn plan produced by an AI
|
|
||||||
*/
|
|
||||||
export class AIScoringHelpers {
|
|
||||||
/**
|
|
||||||
* Evaluate doing nothing, between -1 and 1
|
|
||||||
*/
|
|
||||||
static evaluateIdling(plan: AIPlan): number {
|
|
||||||
// TODO evaluate summed used power over available power
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Evaluate the effect on health for a group of ships
|
* Evaluate the effect on health for a group of ships
|
||||||
*/
|
*/
|
||||||
static evaluateHealthEffect(plan: AIPlan, ships: Ship[]): number {
|
function getHealthDiffShips(plan: AIPlan, ships: Ship[]): number {
|
||||||
if (ships.length) {
|
if (ships.length) {
|
||||||
let diffs = ships.map(ship => getProportionalHealth(plan, ship));
|
let diffs = ships.map(ship => getHealthDiffShip(plan, ship));
|
||||||
let deaths = sum(diffs.map(i => i == -1 ? 1 : 0));
|
let deaths = sum(diffs.map(i => i == -1 ? 1 : 0));
|
||||||
return ((sum(diffs) * 0.5) - (deaths * 0.5)) / ships.length;
|
return ((sum(diffs) * 0.5) - (deaths * 0.5)) / ships.length;
|
||||||
} else {
|
} else {
|
||||||
|
@ -55,69 +43,92 @@ module TK.SpaceTac {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
* Evaluate the effect on health to the enemy, between -1 and 1
|
||||||
*/
|
*/
|
||||||
static evaluateEnemyHealth(plan: AIPlan): number {
|
static healthEnemies(plan: AIPlan): number {
|
||||||
let enemies = imaterialize(plan.battle.ienemies(plan.player, true));
|
let enemies = imaterialize(plan.battle.ienemies(plan.player, true));
|
||||||
return -AIScoringHelpers.evaluateHealthEffect(plan, enemies);
|
return -getHealthDiffShips(plan, enemies);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Evaluate the effect on health to allied ships, between -1 and 1
|
* Evaluate the effect on health to allied ships, between -1 and 1
|
||||||
*/
|
*/
|
||||||
static evaluateAllyHealth(plan: AIPlan): number {
|
static healthAllies(plan: AIPlan): number {
|
||||||
let allies = imaterialize(plan.battle.iallies(plan.player, true));
|
let allies = imaterialize(plan.battle.iallies(plan.player, true));
|
||||||
return AIScoringHelpers.evaluateHealthEffect(plan, allies);
|
return getHealthDiffShips(plan, allies);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Evaluate the clustering of ships, between -1 and 1
|
|
||||||
*/
|
|
||||||
static evaluateClustering(plan: AIPlan): number {
|
|
||||||
/*let move_location = maneuver.getFinalLocation();
|
|
||||||
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);
|
|
||||||
return result;
|
|
||||||
}*/
|
|
||||||
// TODO Compute all final locations
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Evaluate the global positioning of a ship on the arena, between -1 and 1
|
|
||||||
*/
|
|
||||||
static evaluatePosition(plan: AIPlan): number {
|
|
||||||
/*let pos = maneuver.getFinalLocation();
|
|
||||||
let distance = min([pos.x, pos.y, battle.width - pos.x, battle.height - pos.y]);
|
|
||||||
let factor = min([battle.width / 2, battle.height / 2]);
|
|
||||||
return -1 + 2 * distance / factor;*/
|
|
||||||
// TODO
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Evaluate the cost of overheating equipments
|
* Evaluate the cost of overheating equipments
|
||||||
*/
|
*/
|
||||||
static evaluateOverheat(plan: AIPlan): number {
|
static overheat(plan: AIPlan): number {
|
||||||
/*let cooldown = ship.actions.getCooldown(maneuver.action);
|
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()) {
|
if (cooldown.willOverheat()) {
|
||||||
return -Math.min(1, 0.4 * cooldown.cooling);
|
return -Math.min(1, 0.4 * cooldown.cooling);
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
return 0;
|
||||||
}*/
|
}
|
||||||
// TODO
|
} else {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Evaluate the gain or loss of active effects
|
* Evaluate the gain or loss of active effects
|
||||||
*/
|
*/
|
||||||
static evaluateActiveEffects(plan: AIPlan): number {
|
static activeEffects(plan: AIPlan): number {
|
||||||
let result = 0;
|
let result = 0;
|
||||||
plan.effects.forEach(effect => {
|
plan.effects.forEach(effect => {
|
||||||
if (effect instanceof ShipEffectAddedDiff || effect instanceof ShipEffectRemovedDiff) {
|
if (effect instanceof ShipEffectAddedDiff || effect instanceof ShipEffectRemovedDiff) {
|
||||||
|
|
22
src/core/ai/AISettings.ts
Normal file
22
src/core/ai/AISettings.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
module TK.SpaceTac {
|
||||||
|
export type AISettings = {
|
||||||
|
producer: AIPlanProducer
|
||||||
|
scoring: AIPlanScoring
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixed settings of AI
|
||||||
|
*/
|
||||||
|
export const AISettingsStock: { [name: string]: (battle: Battle, player: Player, ...args: any[]) => AISettings } = {
|
||||||
|
default: (battle: Battle, player: Player) => ({
|
||||||
|
producer: AIPlanProducers.random(battle, player),
|
||||||
|
scoring: AIScoringHelpers.sum(
|
||||||
|
AIScoringHelpers.scaled(AIScoringHelpers.overheat, 3),
|
||||||
|
AIScoringHelpers.scaled(AIScoringHelpers.healthEnemies, 5),
|
||||||
|
AIScoringHelpers.scaled(AIScoringHelpers.healthAllies, 20),
|
||||||
|
AIScoringHelpers.scaled(AIScoringHelpers.activeEffects, 3),
|
||||||
|
AIScoringHelpers.scaled(AIScoringHelpers.remainingPower, 2),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -74,7 +74,8 @@ module TK.SpaceTac {
|
||||||
* Process AI in current thread
|
* Process AI in current thread
|
||||||
*/
|
*/
|
||||||
async processHere(): Promise<TurnPlan> {
|
async processHere(): Promise<TurnPlan> {
|
||||||
let ai = new BruteAI(this.battle, this.player, this.debug); // TODO AI choice ?
|
const settings = AISettingsStock.default(this.battle, this.player); // TODO settings choice ?
|
||||||
|
let ai = new ContinuousAI(settings, this.debug);
|
||||||
ai.play();
|
ai.play();
|
||||||
const result = await ai.getPlan(); // TODO Only when human player is done
|
const result = await ai.getPlan(); // TODO Only when human player is done
|
||||||
return result.plan;
|
return result.plan;
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
module TK.SpaceTac.Specs {
|
|
||||||
class FixedPlan extends AIPlan {
|
|
||||||
constructor(score: number) {
|
|
||||||
super();
|
|
||||||
this.score = score;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testing("AbstractAI", test => {
|
|
||||||
test.acase("keeps track of the best produced plan so far", async check => {
|
|
||||||
const battle = new Battle();
|
|
||||||
|
|
||||||
const ai = new AbstractAI(battle, battle.fleets[0].player);
|
|
||||||
ai.timer = Timer.synchronous;
|
|
||||||
|
|
||||||
const producer = (...scores: number[]) => imap(iarray(scores), score => new FixedPlan(score));
|
|
||||||
check.patch(ai, "getPlanProducer", () => producer(1, -8, 4, 3, 7, 0, 6, 1));
|
|
||||||
check.patch(ai, "getPlanScoring", () => (plan: AIPlan) => (plan instanceof FixedPlan) ? plan.score : -Infinity);
|
|
||||||
|
|
||||||
await ai.play();
|
|
||||||
const played = await ai.getPlan();
|
|
||||||
check.equals(played.score, 7);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
module TK.SpaceTac.Specs {
|
|
||||||
testing("BruteAI", test => {
|
|
||||||
test.acase("produces something", async check => {
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
module TK.SpaceTac {
|
|
||||||
/**
|
|
||||||
* AI that produces random valid plans, exploring the whole set of possibilities
|
|
||||||
*/
|
|
||||||
export class BruteAI extends AbstractAI {
|
|
||||||
getPlanProducer(): AIPlanProducer {
|
|
||||||
const builder = () => this.getRandomPlan();
|
|
||||||
function* producer() {
|
|
||||||
while (true) {
|
|
||||||
yield builder();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return producer();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a single random plan
|
|
||||||
*/
|
|
||||||
getRandomPlan(): AIPlan {
|
|
||||||
const planning = new TurnPlanning(this.battle, this.player);
|
|
||||||
|
|
||||||
for (let ship of this.battle.ships.iterator()) {
|
|
||||||
if (ship.isPlayedBy(this.player)) {
|
|
||||||
for (let action of ship.actions.listAll()) {
|
|
||||||
if (action instanceof MoveAction) {
|
|
||||||
if (this.random.bool()) {
|
|
||||||
this.addMove(planning, ship, action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AIPlan(planning.getTurnPlan(), this.battle, this.player);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a random move action
|
|
||||||
*/
|
|
||||||
addMove(planning: TurnPlanning, ship: Ship, action: MoveAction): void {
|
|
||||||
const distance = this.random.random() * (action.max_distance - action.min_distance) + action.min_distance;
|
|
||||||
const angle = this.random.random() * Math.PI * 2;
|
|
||||||
planning.addAction(ship, action, distance, angle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
23
src/core/ai/ContinuousAI.spec.ts
Normal file
23
src/core/ai/ContinuousAI.spec.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
module TK.SpaceTac.Specs {
|
||||||
|
class FixedPlan extends AIPlan {
|
||||||
|
constructor(score: number) {
|
||||||
|
super();
|
||||||
|
this.score = score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testing("ContinuousAI", test => {
|
||||||
|
test.acase("keeps track of the best produced plan so far", async check => {
|
||||||
|
const settings: AISettings = {
|
||||||
|
producer: imap([1, -8, 4, 3, 7, 0, 6, 1], score => new FixedPlan(score)),
|
||||||
|
scoring: (plan: AIPlan) => (plan instanceof FixedPlan) ? plan.score : -Infinity
|
||||||
|
}
|
||||||
|
const ai = new ContinuousAI(settings);
|
||||||
|
ai.timer = Timer.synchronous;
|
||||||
|
|
||||||
|
await ai.play();
|
||||||
|
const played = await ai.getPlan();
|
||||||
|
check.equals(played.score, 7);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -3,38 +3,27 @@ module TK.SpaceTac {
|
||||||
export type AIPlanScoring = (plan: AIPlan) => number;
|
export type AIPlanScoring = (plan: AIPlan) => number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for all Artificial Intelligence interaction
|
* Standard system for Artificial Intelligence interaction
|
||||||
*
|
*
|
||||||
* An AI should work indefinitely on a battle state, to provide the best TurnPlan possible
|
* An AI should work indefinitely on a battle state, to provide the best TurnPlan possible
|
||||||
*/
|
*/
|
||||||
export class AbstractAI {
|
export class ContinuousAI {
|
||||||
// Name of the AI
|
|
||||||
name: string
|
|
||||||
|
|
||||||
// Random generator, if needed
|
|
||||||
random = RandomGenerator.global
|
|
||||||
|
|
||||||
// Timer for scheduled calls
|
// Timer for scheduled calls
|
||||||
timer = Timer.global
|
timer = Timer.global
|
||||||
|
|
||||||
// Time at which work as started
|
// Time at which work as started
|
||||||
private started = 0
|
started = 0
|
||||||
|
|
||||||
// Best plan so far
|
// Best plan so far
|
||||||
private best_plan = new AIPlan()
|
best_plan = new AIPlan()
|
||||||
|
|
||||||
// Number of plans produced
|
// Number of plans produced
|
||||||
private produced = 0
|
produced = 0
|
||||||
|
|
||||||
// Is the work interrupted
|
// Is the work interrupted
|
||||||
private interrupted = false
|
interrupted = false
|
||||||
|
|
||||||
constructor(protected readonly battle: Battle, protected readonly player: Player, protected readonly debug = false, name?: string) {
|
constructor(readonly settings: AISettings, public debug = false) {
|
||||||
this.name = name || classname(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
return this.name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -60,7 +49,7 @@ module TK.SpaceTac {
|
||||||
this.produced++;
|
this.produced++;
|
||||||
this.pushScoredPlan(plan);
|
this.pushScoredPlan(plan);
|
||||||
} else {
|
} else {
|
||||||
console.warn("AI produced an invalid plan", this.name, plan);
|
console.warn("AI produced an invalid plan", this, plan);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.interrupted) {
|
if (this.interrupted) {
|
||||||
|
@ -107,7 +96,7 @@ module TK.SpaceTac {
|
||||||
* The iterable may (in fact, should) be infinite.
|
* The iterable may (in fact, should) be infinite.
|
||||||
*/
|
*/
|
||||||
getPlanProducer(): AIPlanProducer {
|
getPlanProducer(): AIPlanProducer {
|
||||||
return [];
|
return this.settings.producer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -116,29 +105,14 @@ module TK.SpaceTac {
|
||||||
* A standard scoring system is provided by default
|
* A standard scoring system is provided by default
|
||||||
*/
|
*/
|
||||||
getPlanScoring(): AIPlanScoring {
|
getPlanScoring(): AIPlanScoring {
|
||||||
const scaled = (evaluator: AIPlanScoring, factor: number) => (plan: AIPlan) => factor * evaluator(plan);
|
return this.settings.scoring;
|
||||||
|
|
||||||
// TODO If a score is way out of bounds for one of these, it may not need to go further
|
|
||||||
|
|
||||||
const scorers = [
|
|
||||||
scaled(AIScoringHelpers.evaluateOverheat, 3),
|
|
||||||
scaled(AIScoringHelpers.evaluateEnemyHealth, 5),
|
|
||||||
scaled(AIScoringHelpers.evaluateAllyHealth, 20),
|
|
||||||
scaled(AIScoringHelpers.evaluateActiveEffects, 3),
|
|
||||||
scaled(AIScoringHelpers.evaluateClustering, 4),
|
|
||||||
scaled(AIScoringHelpers.evaluatePosition, 0.5),
|
|
||||||
scaled(AIScoringHelpers.evaluateIdling, 2),
|
|
||||||
]
|
|
||||||
|
|
||||||
return plan => sum(scorers.map(scorer => scorer(plan)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a scored plan to the memory (by default, it keeps only the best one)
|
* Add a scored plan to the memory (by default, it keeps only the best one)
|
||||||
*/
|
*/
|
||||||
pushScoredPlan(plan: AIPlan): void {
|
pushScoredPlan(plan: AIPlan): void {
|
||||||
const diff = plan.score - this.best_plan.score;
|
if (plan.score >= this.best_plan.score) {
|
||||||
if (diff > 0.0001 || (diff > -0.0001 && this.random.bool())) {
|
|
||||||
this.best_plan = plan;
|
this.best_plan = plan;
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue