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
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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 {
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue