1
0
Fork 0

AI now uses real simulated effects in evaluation, and produces all toggle actions

This commit is contained in:
Michaël Lemaire 2018-06-14 17:39:27 +02:00
parent 4e624dc9db
commit a6b0f51b9c
6 changed files with 129 additions and 30 deletions

View file

@ -83,8 +83,10 @@ Artificial Intelligence
----------------------- -----------------------
* If web worker is not responsive, or produces only errors, it should be disabled for the session * If web worker is not responsive, or produces only errors, it should be disabled for the session
* Prevent infinite loops of toggle/untoggle
* Produce interesting "angle" areas * Produce interesting "angle" areas
* Evaluate active effects * Evaluate vigilance actions
* Evaluate the "interest" of an active effect (e.g healing is better when harmed...)
* Evaluators result should be more specific (final state evaluation, diff evaluation, confidence...) * Evaluators result should be more specific (final state evaluation, diff evaluation, confidence...)
* Use a first batch of producers, and only if no "good" move has been found, go on with some infinite producers * Use a first batch of producers, and only if no "good" move has been found, go on with some infinite producers
* Abandon fight if the AI judges there is no hope of victory * Abandon fight if the AI judges there is no hope of victory

View file

@ -20,8 +20,8 @@ module TK.SpaceTac {
// Result of move-fire simulation // Result of move-fire simulation
simulation: MoveFireResult simulation: MoveFireResult
// List of guessed effects of this maneuver // List of guessed effects of this maneuver (lazy property)
effects: BaseBattleDiff[] _effects?: BaseBattleDiff[]
constructor(ship: Ship, action: BaseAction, target: Target, move_margin = 1) { constructor(ship: Ship, action: BaseAction, target: Target, move_margin = 1) {
this.ship = ship; this.ship = ship;
@ -31,16 +31,20 @@ module TK.SpaceTac {
let simulator = new MoveFireSimulator(this.ship); let simulator = new MoveFireSimulator(this.ship);
this.simulation = simulator.simulateAction(this.action, this.target, move_margin); this.simulation = simulator.simulateAction(this.action, this.target, move_margin);
this.effects = flatten(this.simulation.parts.map(part =>
part.action.getDiffs(this.ship, this.battle, part.target)
));
} }
jasmineToString() { jasmineToString() {
return `Use ${this.action.code} on ${this.target.jasmineToString()}`; return `Use ${this.action.code} on ${this.target.jasmineToString()}`;
} }
get effects(): BaseBattleDiff[] {
if (!this._effects) {
let simulator = new MoveFireSimulator(this.ship);
this._effects = simulator.getExpectedDiffs(this.battle, this.simulation);
}
return this._effects;
}
/** /**
* Returns true if the maneuver has at least one part doable * Returns true if the maneuver has at least one part doable
*/ */

View file

@ -95,7 +95,7 @@ module TK.SpaceTac {
TacticalAIHelpers.produceEndTurn, TacticalAIHelpers.produceEndTurn,
TacticalAIHelpers.produceDirectShots, TacticalAIHelpers.produceDirectShots,
TacticalAIHelpers.produceBlastShots, TacticalAIHelpers.produceBlastShots,
TacticalAIHelpers.produceDroneDeployments, TacticalAIHelpers.produceToggleActions,
TacticalAIHelpers.produceRandomMoves, TacticalAIHelpers.produceRandomMoves,
] ]
return producers.map(producer => producer(this.ship, this.ship.getBattle() || new Battle())); return producers.map(producer => producer(this.ship, this.ship.getBattle() || new Battle()));
@ -116,6 +116,7 @@ module TK.SpaceTac {
scaled(TacticalAIHelpers.evaluateOverheat, 3), scaled(TacticalAIHelpers.evaluateOverheat, 3),
scaled(TacticalAIHelpers.evaluateEnemyHealth, 5), scaled(TacticalAIHelpers.evaluateEnemyHealth, 5),
scaled(TacticalAIHelpers.evaluateAllyHealth, 20), scaled(TacticalAIHelpers.evaluateAllyHealth, 20),
scaled(TacticalAIHelpers.evaluateActiveEffects, 3),
scaled(TacticalAIHelpers.evaluateClustering, 4), scaled(TacticalAIHelpers.evaluateClustering, 4),
scaled(TacticalAIHelpers.evaluatePosition, 0.5), scaled(TacticalAIHelpers.evaluatePosition, 0.5),
scaled(TacticalAIHelpers.evaluateIdling, 2), scaled(TacticalAIHelpers.evaluateIdling, 2),

View file

@ -50,10 +50,8 @@ module TK.SpaceTac.Specs {
let battle = new Battle(); let battle = new Battle();
let ship = battle.fleets[0].addShip(); let ship = battle.fleets[0].addShip();
let weapon = TestTools.addWeapon(ship, 50, 1, 1000, 105); let weapon = TestTools.addWeapon(ship, 50, 1, 1000, 105);
TestTools.setShipModel(ship, 100, 0, 10, 1, [weapon]);
TestTools.setShipModel(ship, 100, 0, 10);
TestTools.setShipPlaying(battle, ship); TestTools.setShipPlaying(battle, ship);
ship.actions.addCustom(weapon);
let result = imaterialize(TacticalAIHelpers.produceInterestingBlastShots(ship, battle)); let result = imaterialize(TacticalAIHelpers.produceInterestingBlastShots(ship, battle));
check.equals(result.length, 0); check.equals(result.length, 0);
@ -83,6 +81,31 @@ module TK.SpaceTac.Specs {
]); ]);
}); });
test.case("produces toggle/untoggle actions", check => {
let battle = new Battle();
let ship = battle.fleets[0].addShip();
let action1 = new DeployDroneAction("Drone");
let action2 = new ToggleAction("Toggle");
let action3 = new VigilanceAction("Vigilance", { radius: 150 });
TestTools.setShipModel(ship, 100, 0, 10, 1, [action1, action2, action3]);
TestTools.addEngine(ship, 1000);
TestTools.setShipPlaying(battle, ship);
check.patch(TacticalAIHelpers, "scanArena", () => iarray([
Target.newFromLocation(1, 0),
Target.newFromLocation(0, 1),
]));
let result = imaterialize(TacticalAIHelpers.produceToggleActions(ship, battle));
check.equals(result, [
new Maneuver(ship, action2, Target.newFromShip(ship)),
new Maneuver(ship, action1, Target.newFromLocation(1, 0)),
new Maneuver(ship, action3, Target.newFromLocation(1, 0)),
new Maneuver(ship, action1, Target.newFromLocation(0, 1)),
new Maneuver(ship, action3, Target.newFromLocation(0, 1)),
]);
});
test.case("evaluates turn cost", check => { test.case("evaluates turn cost", check => {
let battle = new Battle(); let battle = new Battle();
let ship = battle.fleets[0].addShip(); let ship = battle.fleets[0].addShip();
@ -248,5 +271,43 @@ module TK.SpaceTac.Specs {
ship.actions.addCustom(weapon); ship.actions.addCustom(weapon);
check.equals(TacticalAIHelpers.evaluateOverheat(ship, battle, maneuver), 0); check.equals(TacticalAIHelpers.evaluateOverheat(ship, battle, maneuver), 0);
}); });
test.case("evaluates active effects", check => {
let battle = TestTools.createBattle();
let ship = battle.fleets[0].ships[0];
let enemy = battle.fleets[1].ships[0];
TestTools.setShipModel(ship, 1, 0, 1);
TestTools.setShipModel(enemy, 5, 5);
let action = new TriggerAction("Test", { range: 100, power: 1 });
ship.actions.addCustom(action);
let maneuver = new Maneuver(ship, action, Target.newFromShip(enemy));
check.equals(TacticalAIHelpers.evaluateActiveEffects(ship, battle, maneuver), 0);
action.effects = [new StickyEffect(new DamageEffect(1), 1)];
maneuver = new Maneuver(ship, action, Target.newFromShip(enemy));
check.nears(TacticalAIHelpers.evaluateActiveEffects(ship, battle, maneuver), 0.5);
maneuver = new Maneuver(ship, action, Target.newFromShip(ship));
check.nears(TacticalAIHelpers.evaluateActiveEffects(ship, battle, maneuver), -0.5);
action.effects = [new StickyEffect(new CooldownEffect(1), 1)];
maneuver = new Maneuver(ship, action, Target.newFromShip(enemy));
check.nears(TacticalAIHelpers.evaluateActiveEffects(ship, battle, maneuver), -0.5);
maneuver = new Maneuver(ship, action, Target.newFromShip(ship));
check.nears(TacticalAIHelpers.evaluateActiveEffects(ship, battle, maneuver), 0.5);
battle.fleets[0].addShip();
check.nears(TacticalAIHelpers.evaluateActiveEffects(ship, battle, maneuver), 0.3333333333333333);
action.effects = [new StickyEffect(new CooldownEffect(1), 1), new StickyEffect(new CooldownEffect(1), 1)];
maneuver = new Maneuver(ship, action, Target.newFromShip(enemy));
check.nears(TacticalAIHelpers.evaluateActiveEffects(ship, battle, maneuver), -0.6666666666666666);
action.effects = range(10).map(() => new StickyEffect(new CooldownEffect(1), 1));
maneuver = new Maneuver(ship, action, Target.newFromShip(enemy));
check.nears(TacticalAIHelpers.evaluateActiveEffects(ship, battle, maneuver), -1);
});
}); });
} }

View file

@ -1,15 +1,4 @@
module TK.SpaceTac { module TK.SpaceTac {
/**
* Iterator of a list of "random" arena coordinates, based on a grid
*/
function scanArena(battle: Battle, cells = 10, random = RandomGenerator.global): Iterator<Target> {
return imap(irange(cells * cells), cellpos => {
let y = Math.floor(cellpos / cells);
let x = cellpos - y * cells;
return Target.newFromLocation((x + random.random()) * battle.width / cells, (y + random.random()) * battle.height / cells);
});
}
/** /**
* Get a list of all playable actions (like the actionbar for player) for a ship * Get a list of all playable actions (like the actionbar for player) for a ship
*/ */
@ -55,6 +44,17 @@ module TK.SpaceTac {
* These are static methods that may be used as base for TacticalAI ruleset. * These are static methods that may be used as base for TacticalAI ruleset.
*/ */
export class TacticalAIHelpers { export class TacticalAIHelpers {
/**
* Iterator of a list of "random" arena coordinates, based on a grid
*/
static scanArena(battle: Battle, cells = 10, random = RandomGenerator.global): Iterator<Target> {
return imap(irange(cells * cells), cellpos => {
let y = Math.floor(cellpos / cells);
let x = cellpos - y * cells;
return Target.newFromLocation((x + random.random()) * battle.width / cells, (y + random.random()) * battle.height / cells);
});
}
/** /**
* Produce a turn end. * Produce a turn end.
*/ */
@ -77,7 +77,7 @@ module TK.SpaceTac {
static produceRandomMoves(ship: Ship, battle: Battle, cells = 10, iterations = 1, random = RandomGenerator.global): TacticalProducer { static produceRandomMoves(ship: Ship, battle: Battle, cells = 10, iterations = 1, random = RandomGenerator.global): TacticalProducer {
let engines = ifilter(getPlayableActions(ship), action => action instanceof MoveAction); let engines = ifilter(getPlayableActions(ship), action => action instanceof MoveAction);
return ichainit(imap(irange(iterations), iteration => { return ichainit(imap(irange(iterations), iteration => {
let moves = icombine(engines, scanArena(battle, cells, random)); let moves = icombine(engines, TacticalAIHelpers.scanArena(battle, cells, random));
return imap(moves, ([engine, target]) => new Maneuver(ship, engine, target)); return imap(moves, ([engine, target]) => new Maneuver(ship, engine, target));
})); }));
} }
@ -101,7 +101,7 @@ module TK.SpaceTac {
*/ */
static produceRandomBlastShots(ship: Ship, battle: Battle): TacticalProducer { static produceRandomBlastShots(ship: Ship, battle: Battle): TacticalProducer {
let weapons = ifilter(getPlayableActions(ship), action => action instanceof TriggerAction && action.blast > 0); let weapons = ifilter(getPlayableActions(ship), action => action instanceof TriggerAction && action.blast > 0);
let candidates = ifilter(icombine(weapons, scanArena(battle)), ([weapon, location]) => (<TriggerAction>weapon).getEffects(ship, location).length > 0); let candidates = ifilter(icombine(weapons, TacticalAIHelpers.scanArena(battle)), ([weapon, location]) => (<TriggerAction>weapon).getEffects(ship, location).length > 0);
let result = imap(candidates, ([weapon, location]) => new Maneuver(ship, weapon, location)); let result = imap(candidates, ([weapon, location]) => new Maneuver(ship, weapon, location));
return result; return result;
} }
@ -114,12 +114,19 @@ module TK.SpaceTac {
} }
/** /**
* Produce drone deployments. * Produce toggle actions at random locations.
*/ */
static produceDroneDeployments(ship: Ship, battle: Battle): TacticalProducer { static produceToggleActions(ship: Ship, battle: Battle): TacticalProducer {
let drones = ifilter(getPlayableActions(ship), action => action instanceof DeployDroneAction); let toggles = ifilter(getPlayableActions(ship), action => action instanceof ToggleAction);
let grid = scanArena(battle);
return imap(icombine(grid, drones), ([target, drone]) => new Maneuver(ship, drone, target)); let self_toggles = ifilter(toggles, toggle => contains([ActionTargettingMode.SELF_CONFIRM, ActionTargettingMode.SELF], toggle.getTargettingMode(ship)));
let self_maneuvers = imap(self_toggles, toggle => new Maneuver(ship, toggle, Target.newFromShip(ship)));
let distant_toggles = ifilter(toggles, toggle => contains([ActionTargettingMode.SPACE, ActionTargettingMode.SURROUNDINGS], toggle.getTargettingMode(ship)));
let grid = TacticalAIHelpers.scanArena(battle);
let distant_maneuvers = imap(icombine(grid, distant_toggles), ([location, toggle]) => new Maneuver(ship, toggle, location));
return ichain(self_maneuvers, distant_maneuvers);
} }
/** /**
@ -223,5 +230,29 @@ module TK.SpaceTac {
return 0; return 0;
} }
} }
/**
* Evaluate the gain or loss of active effects
*/
static evaluateActiveEffects(ship: Ship, battle: Battle, maneuver: Maneuver): number {
let result = 0;
maneuver.effects.forEach(effect => {
if (effect instanceof ShipEffectAddedDiff || effect instanceof ShipEffectRemovedDiff) {
let target = battle.getShip(effect.ship_id);
let enemy = target && !target.isPlayedBy(ship.getPlayer());
let beneficial = effect.effect.isBeneficial();
if (effect instanceof ShipEffectRemovedDiff) {
beneficial = !beneficial;
}
// TODO Evaluate the "power" of the effect
if ((beneficial && !enemy) || (!beneficial && enemy)) {
result += 1;
} else {
result -= 1;
}
}
});
return clamp(result / battle.ships.count(), -1, 1);
}
} }
} }

View file

@ -362,7 +362,7 @@ module TK.SpaceTac.UI {
let arena = this.battleview.arena.getBoundaries(); let arena = this.battleview.arena.getBoundaries();
this.effects_messages.setPosition( this.effects_messages.setPosition(
(this.ship.arena_x < 100) ? -35 : ((this.ship.arena_x > arena.width - 100) ? (35 - this.effects_messages.width) : (-this.effects_messages.width * 0.5)), (this.ship.arena_x < 100) ? 0 : ((this.ship.arena_x > arena.width - 100) ? (-this.effects_messages.width) : (-this.effects_messages.width * 0.5)),
(this.ship.arena_y < arena.height * 0.9) ? 60 : (-60 - this.effects_messages.height) (this.ship.arena_y < arena.height * 0.9) ? 60 : (-60 - this.effects_messages.height)
); );