diff --git a/src/common/Tools.ts b/src/common/Tools.ts index 0e1c6e4..f6f151f 100644 --- a/src/common/Tools.ts +++ b/src/common/Tools.ts @@ -230,14 +230,14 @@ module TK { /** * Type filter, to return a list of instances of a given type */ - export function tfilter(array: any[], filter: (item: any) => item is T): T[] { + export function tfilter(array: ReadonlyArray, 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(array: any[], classref: { new(...args: any[]): T }): T[] { + export function cfilter(array: ReadonlyArray, classref: { new(...args: any[]): T }): T[] { return array.filter((item): item is T => item instanceof classref); } diff --git a/src/core/ai/AIPlanProducers.spec.ts b/src/core/ai/AIPlanProducers.spec.ts new file mode 100644 index 0000000..b6dcd80 --- /dev/null +++ b/src/core/ai/AIPlanProducers.spec.ts @@ -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); + }); + }); + }); +} diff --git a/src/core/ai/AIPlanProducers.ts b/src/core/ai/AIPlanProducers.ts new file mode 100644 index 0000000..a299f6e --- /dev/null +++ b/src/core/ai/AIPlanProducers.ts @@ -0,0 +1,76 @@ +module TK.SpaceTac { + export const AIProducerTools = { + /** + * 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(); + } + } +} diff --git a/src/core/ai/AIScoringHelpers.spec.ts b/src/core/ai/AIScoringHelpers.spec.ts index 0fbe56c..03fd39e 100644 --- a/src/core/ai/AIScoringHelpers.spec.ts +++ b/src/core/ai/AIScoringHelpers.spec.ts @@ -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); }); }); } diff --git a/src/core/ai/AIScoringHelpers.ts b/src/core/ai/AIScoringHelpers.ts index b1c313b..8f0ad69 100644 --- a/src/core/ai/AIScoringHelpers.ts +++ b/src/core/ai/AIScoringHelpers.ts @@ -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) { diff --git a/src/core/ai/AISettings.ts b/src/core/ai/AISettings.ts new file mode 100644 index 0000000..7191643 --- /dev/null +++ b/src/core/ai/AISettings.ts @@ -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), + ) + }) + } +} \ No newline at end of file diff --git a/src/core/ai/AIWorker.ts b/src/core/ai/AIWorker.ts index 2bf295d..690d7b4 100644 --- a/src/core/ai/AIWorker.ts +++ b/src/core/ai/AIWorker.ts @@ -74,7 +74,8 @@ module TK.SpaceTac { * Process AI in current thread */ async processHere(): Promise { - 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; diff --git a/src/core/ai/AbstractAI.spec.ts b/src/core/ai/AbstractAI.spec.ts deleted file mode 100644 index 7e7961d..0000000 --- a/src/core/ai/AbstractAI.spec.ts +++ /dev/null @@ -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); - }); - }); -} \ No newline at end of file diff --git a/src/core/ai/BruteAI.spec.ts b/src/core/ai/BruteAI.spec.ts deleted file mode 100644 index 129adc8..0000000 --- a/src/core/ai/BruteAI.spec.ts +++ /dev/null @@ -1,6 +0,0 @@ -module TK.SpaceTac.Specs { - testing("BruteAI", test => { - test.acase("produces something", async check => { - }); - }); -} diff --git a/src/core/ai/BruteAI.ts b/src/core/ai/BruteAI.ts deleted file mode 100644 index 3547248..0000000 --- a/src/core/ai/BruteAI.ts +++ /dev/null @@ -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); - } - } -} diff --git a/src/core/ai/ContinuousAI.spec.ts b/src/core/ai/ContinuousAI.spec.ts new file mode 100644 index 0000000..410c58c --- /dev/null +++ b/src/core/ai/ContinuousAI.spec.ts @@ -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); + }); + }); +} \ No newline at end of file diff --git a/src/core/ai/AbstractAI.ts b/src/core/ai/ContinuousAI.ts similarity index 67% rename from src/core/ai/AbstractAI.ts rename to src/core/ai/ContinuousAI.ts index 9ea61eb..a293a4b 100644 --- a/src/core/ai/AbstractAI.ts +++ b/src/core/ai/ContinuousAI.ts @@ -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; } }