diff --git a/TODO.md b/TODO.md index 9dfb96c..739c486 100644 --- a/TODO.md +++ b/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 +* Prevent infinite loops of toggle/untoggle * 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...) * 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 diff --git a/src/core/ai/Maneuver.ts b/src/core/ai/Maneuver.ts index c9634e8..73a58eb 100644 --- a/src/core/ai/Maneuver.ts +++ b/src/core/ai/Maneuver.ts @@ -20,8 +20,8 @@ module TK.SpaceTac { // Result of move-fire simulation simulation: MoveFireResult - // List of guessed effects of this maneuver - effects: BaseBattleDiff[] + // List of guessed effects of this maneuver (lazy property) + _effects?: BaseBattleDiff[] constructor(ship: Ship, action: BaseAction, target: Target, move_margin = 1) { this.ship = ship; @@ -31,16 +31,20 @@ module TK.SpaceTac { let simulator = new MoveFireSimulator(this.ship); 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() { 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 */ diff --git a/src/core/ai/TacticalAI.ts b/src/core/ai/TacticalAI.ts index 0d8efa6..d58aa30 100644 --- a/src/core/ai/TacticalAI.ts +++ b/src/core/ai/TacticalAI.ts @@ -95,7 +95,7 @@ module TK.SpaceTac { TacticalAIHelpers.produceEndTurn, TacticalAIHelpers.produceDirectShots, TacticalAIHelpers.produceBlastShots, - TacticalAIHelpers.produceDroneDeployments, + TacticalAIHelpers.produceToggleActions, TacticalAIHelpers.produceRandomMoves, ] 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.evaluateEnemyHealth, 5), scaled(TacticalAIHelpers.evaluateAllyHealth, 20), + scaled(TacticalAIHelpers.evaluateActiveEffects, 3), scaled(TacticalAIHelpers.evaluateClustering, 4), scaled(TacticalAIHelpers.evaluatePosition, 0.5), scaled(TacticalAIHelpers.evaluateIdling, 2), diff --git a/src/core/ai/TacticalAIHelpers.spec.ts b/src/core/ai/TacticalAIHelpers.spec.ts index d004590..8680e87 100644 --- a/src/core/ai/TacticalAIHelpers.spec.ts +++ b/src/core/ai/TacticalAIHelpers.spec.ts @@ -50,10 +50,8 @@ module TK.SpaceTac.Specs { let battle = new Battle(); let ship = battle.fleets[0].addShip(); let weapon = TestTools.addWeapon(ship, 50, 1, 1000, 105); - - TestTools.setShipModel(ship, 100, 0, 10); + TestTools.setShipModel(ship, 100, 0, 10, 1, [weapon]); TestTools.setShipPlaying(battle, ship); - ship.actions.addCustom(weapon); let result = imaterialize(TacticalAIHelpers.produceInterestingBlastShots(ship, battle)); 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 => { let battle = new Battle(); let ship = battle.fleets[0].addShip(); @@ -248,5 +271,43 @@ module TK.SpaceTac.Specs { ship.actions.addCustom(weapon); 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); + }); }); } diff --git a/src/core/ai/TacticalAIHelpers.ts b/src/core/ai/TacticalAIHelpers.ts index 2d572a9..cb436df 100644 --- a/src/core/ai/TacticalAIHelpers.ts +++ b/src/core/ai/TacticalAIHelpers.ts @@ -1,15 +1,4 @@ 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 { - 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 */ @@ -55,6 +44,17 @@ module TK.SpaceTac { * These are static methods that may be used as base for TacticalAI ruleset. */ 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 { + 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. */ @@ -77,7 +77,7 @@ module TK.SpaceTac { static produceRandomMoves(ship: Ship, battle: Battle, cells = 10, iterations = 1, random = RandomGenerator.global): TacticalProducer { let engines = ifilter(getPlayableActions(ship), action => action instanceof MoveAction); 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)); })); } @@ -101,7 +101,7 @@ module TK.SpaceTac { */ static produceRandomBlastShots(ship: Ship, battle: Battle): TacticalProducer { let weapons = ifilter(getPlayableActions(ship), action => action instanceof TriggerAction && action.blast > 0); - let candidates = ifilter(icombine(weapons, scanArena(battle)), ([weapon, location]) => (weapon).getEffects(ship, location).length > 0); + let candidates = ifilter(icombine(weapons, TacticalAIHelpers.scanArena(battle)), ([weapon, location]) => (weapon).getEffects(ship, location).length > 0); let result = imap(candidates, ([weapon, location]) => new Maneuver(ship, weapon, location)); 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 { - let drones = ifilter(getPlayableActions(ship), action => action instanceof DeployDroneAction); - let grid = scanArena(battle); - return imap(icombine(grid, drones), ([target, drone]) => new Maneuver(ship, drone, target)); + static produceToggleActions(ship: Ship, battle: Battle): TacticalProducer { + let toggles = ifilter(getPlayableActions(ship), action => action instanceof ToggleAction); + + 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; } } + + /** + * 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); + } } } \ No newline at end of file diff --git a/src/ui/battle/ArenaShip.ts b/src/ui/battle/ArenaShip.ts index 9cef581..a0293bd 100644 --- a/src/ui/battle/ArenaShip.ts +++ b/src/ui/battle/ArenaShip.ts @@ -362,7 +362,7 @@ module TK.SpaceTac.UI { let arena = this.battleview.arena.getBoundaries(); 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) );