1
0
Fork 0

Continued work on restoring AI feature

This commit is contained in:
Michaël Lemaire 2019-05-23 19:54:05 +02:00
parent b3fa27cb0e
commit 65f2008fd7
12 changed files with 274 additions and 250 deletions

View File

@ -230,14 +230,14 @@ module TK {
/**
* 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);
}
/**
* 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);
}

View 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);
});
});
});
}

View 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();
}
}
}

View File

@ -1,21 +1,29 @@
module TK.SpaceTac.Specs {
testing("AIScoringHelpers", test => {
test.case("evaluates the drawback of doing nothing", check => {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
test.case("evaluates the drawback of leaving power untouched", check => {
const battle = new Battle();
const ship = battle.fleets[0].addShip();
TestTools.setShipModel(ship, 100, 0, 10);
let weapon = TestTools.addWeapon(ship, 10, 2, 100, 10);
let toggle = ship.actions.addCustom(new ToggleAction("test"));
const weapon = TestTools.addWeapon(ship, 10, 2, 100, 10, 10);
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));
check.equals(AIScoringHelpers.evaluateIdling(plan), 0.5, "fire");
let plan = new AIPlan(undefined, battle, ship.getPlayer());
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));
check.equals(AIScoringHelpers.evaluateIdling(plan), 0.5, "toggle on");
check.equals(AIScoringHelpers.remainingPower(plan), -0.9, "toggle on");
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));
check.equals(AIScoringHelpers.evaluateIdling(plan), -0.2, "toggle off");
check.equals(AIScoringHelpers.remainingPower(plan), -1, "toggle off");
});
test.case("evaluates damage to enemies", check => {
@ -32,90 +40,46 @@ module TK.SpaceTac.Specs {
// no enemies hurt
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
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
plan = planSingleAction(ship, action, Target.newFromLocation(280, 0));
check.nears(AIScoringHelpers.evaluateEnemyHealth(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);
check.nears(AIScoringHelpers.healthEnemies(plan), 0.6666666666, 8);
});
test.case("evaluates overheat", check => {
let battle = new Battle(undefined, undefined, 200, 100);
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));
check.equals(AIScoringHelpers.evaluateOverheat(plan), 0);
let plan = planSingleAction(ship, weapon, new Target(0, 50));
check.equals(AIScoringHelpers.overheat(plan), 0);
weapon.configureCooldown(1, 1);
ship.actions.updateFromShip(ship);
ship.actions.addCustom(weapon);
check.equals(AIScoringHelpers.evaluateOverheat(plan), -0.4);
check.equals(AIScoringHelpers.overheat(plan), -0.4);
weapon.configureCooldown(1, 2);
ship.actions.updateFromShip(ship);
ship.actions.addCustom(weapon);
check.equals(AIScoringHelpers.evaluateOverheat(plan), -0.8);
check.equals(AIScoringHelpers.overheat(plan), -0.8);
weapon.configureCooldown(1, 3);
ship.actions.updateFromShip(ship);
ship.actions.addCustom(weapon);
check.equals(AIScoringHelpers.evaluateOverheat(plan), -1);
check.equals(AIScoringHelpers.overheat(plan), -1);
weapon.configureCooldown(2, 1);
ship.actions.updateFromShip(ship);
ship.actions.addCustom(weapon);
check.equals(AIScoringHelpers.evaluateOverheat(plan), 0);
check.equals(AIScoringHelpers.overheat(plan), 0);
});
test.case("evaluates active effects", check => {
@ -124,36 +88,37 @@ module TK.SpaceTac.Specs {
let enemy = battle.fleets[1].ships[0];
TestTools.setShipModel(ship, 5, 0, 1);
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);
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)];
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));
check.nears(AIScoringHelpers.evaluateActiveEffects(plan), -0.5);
plan = planSingleAction(ship, action, Target.newFromLocation(4, 0));
check.nears(AIScoringHelpers.activeEffects(plan), -0.5);
action.effects = [new StickyEffect(new CooldownEffect(1), 1)];
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));
check.nears(AIScoringHelpers.evaluateActiveEffects(plan), 0.5);
plan = planSingleAction(ship, action, Target.newFromLocation(4, 0));
check.nears(AIScoringHelpers.activeEffects(plan), 0.5);
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)];
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));
plan = planSingleAction(ship, action, Target.newFromShip(enemy));
check.nears(AIScoringHelpers.evaluateActiveEffects(plan), -1);
check.nears(AIScoringHelpers.activeEffects(plan), -1);
});
});
}

View File

@ -2,7 +2,7 @@ module TK.SpaceTac {
/**
* 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 cshield = ship.getAttribute("shield_capacity");
let hull = ship.getValue("hull")
@ -30,94 +30,105 @@ module TK.SpaceTac {
}
}
/**
* Evaluate the effect on health for a group of ships
*/
function getHealthDiffShips(plan: AIPlan, ships: Ship[]): number {
if (ships.length) {
let diffs = ships.map(ship => getHealthDiffShip(plan, ship));
let deaths = sum(diffs.map(i => i == -1 ? 1 : 0));
return ((sum(diffs) * 0.5) - (deaths * 0.5)) / ships.length;
} else {
return 0;
}
}
/**
* Functions to help scoring a turn plan produced by an AI
*
* Helpers should try to remain in the -1..1 range
*/
export class AIScoringHelpers {
/**
* Evaluate doing nothing, between -1 and 1
* Sum of other scoring methods
*/
static evaluateIdling(plan: AIPlan): number {
// TODO evaluate summed used power over available power
return 0;
static sum(...scorings: AIPlanScoring[]): AIPlanScoring {
return plan => sum(scorings.map(scoring => scoring(plan)));
}
/**
* Evaluate the effect on health for a group of ships
* Scaled version of another scoring method
*/
static evaluateHealthEffect(plan: AIPlan, ships: Ship[]): number {
if (ships.length) {
let diffs = ships.map(ship => getProportionalHealth(plan, ship));
let deaths = sum(diffs.map(i => i == -1 ? 1 : 0));
return ((sum(diffs) * 0.5) - (deaths * 0.5)) / ships.length;
} else {
return 0;
}
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
*/
static evaluateEnemyHealth(plan: AIPlan): number {
static healthEnemies(plan: AIPlan): number {
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
*/
static evaluateAllyHealth(plan: AIPlan): number {
static healthAllies(plan: AIPlan): number {
let allies = imaterialize(plan.battle.iallies(plan.player, true));
return AIScoringHelpers.evaluateHealthEffect(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;
return getHealthDiffShips(plan, allies);
}
/**
* Evaluate the cost of overheating equipments
*/
static evaluateOverheat(plan: AIPlan): number {
/*let cooldown = ship.actions.getCooldown(maneuver.action);
if (cooldown.willOverheat()) {
return -Math.min(1, 0.4 * cooldown.cooling);
} else {
return 0;
}*/
// TODO
return 0;
static overheat(plan: AIPlan): number {
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()) {
return -Math.min(1, 0.4 * cooldown.cooling);
} else {
return 0;
}
} else {
return 0;
}
} else {
return 0;
}
}));
}
/**
* Evaluate the gain or loss of active effects
*/
static evaluateActiveEffects(plan: AIPlan): number {
static activeEffects(plan: AIPlan): number {
let result = 0;
plan.effects.forEach(effect => {
if (effect instanceof ShipEffectAddedDiff || effect instanceof ShipEffectRemovedDiff) {

22
src/core/ai/AISettings.ts Normal file
View 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),
)
})
}
}

View File

@ -74,7 +74,8 @@ module TK.SpaceTac {
* Process AI in current thread
*/
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();
const result = await ai.getPlan(); // TODO Only when human player is done
return result.plan;

View File

@ -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);
});
});
}

View File

@ -1,6 +0,0 @@
module TK.SpaceTac.Specs {
testing("BruteAI", test => {
test.acase("produces something", async check => {
});
});
}

View File

@ -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);
}
}
}

View 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);
});
});
}

View File

@ -3,38 +3,27 @@ module TK.SpaceTac {
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
*/
export class AbstractAI {
// Name of the AI
name: string
// Random generator, if needed
random = RandomGenerator.global
export class ContinuousAI {
// Timer for scheduled calls
timer = Timer.global
// Time at which work as started
private started = 0
started = 0
// Best plan so far
private best_plan = new AIPlan()
best_plan = new AIPlan()
// Number of plans produced
private produced = 0
produced = 0
// Is the work interrupted
private interrupted = false
interrupted = false
constructor(protected readonly battle: Battle, protected readonly player: Player, protected readonly debug = false, name?: string) {
this.name = name || classname(this);
}
toString() {
return this.name;
constructor(readonly settings: AISettings, public debug = false) {
}
/**
@ -60,7 +49,7 @@ module TK.SpaceTac {
this.produced++;
this.pushScoredPlan(plan);
} else {
console.warn("AI produced an invalid plan", this.name, plan);
console.warn("AI produced an invalid plan", this, plan);
}
if (this.interrupted) {
@ -107,7 +96,7 @@ module TK.SpaceTac {
* The iterable may (in fact, should) be infinite.
*/
getPlanProducer(): AIPlanProducer {
return [];
return this.settings.producer;
}
/**
@ -116,29 +105,14 @@ module TK.SpaceTac {
* A standard scoring system is provided by default
*/
getPlanScoring(): AIPlanScoring {
const scaled = (evaluator: AIPlanScoring, factor: number) => (plan: AIPlan) => factor * evaluator(plan);
// 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)));
return this.settings.scoring;
}
/**
* Add a scored plan to the memory (by default, it keeps only the best one)
*/
pushScoredPlan(plan: AIPlan): void {
const diff = plan.score - this.best_plan.score;
if (diff > 0.0001 || (diff > -0.0001 && this.random.bool())) {
if (plan.score >= this.best_plan.score) {
this.best_plan = plan;
}
}