AI now uses real simulated effects in evaluation, and produces all toggle actions
This commit is contained in:
parent
4e624dc9db
commit
a6b0f51b9c
4
TODO.md
4
TODO.md
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue