diff --git a/TODO.md b/TODO.md index 826ff41..3961a35 100644 --- a/TODO.md +++ b/TODO.md @@ -79,12 +79,14 @@ Artificial Intelligence ----------------------- * Produce interesting "angle" areas -* AI seems unwanting to use the laser -* Evaluate diffs instead of effects +* Evaluate active effects +* Account for luck +* 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 * Add combination of random small move and actual maneuver, as producer * New duel page with producers/evaluators tweaking +* Use tree techniques to account for potential future moves * Prototype of evolving AI Common UI diff --git a/out/tests.html b/out/tests.html index e3e9d88..b28ffd5 100644 --- a/out/tests.html +++ b/out/tests.html @@ -10,6 +10,10 @@ canvas { display: none; } + + .jasmine-result-message { + white-space: pre-wrap !important; + } diff --git a/src/core/actions/BaseAction.ts b/src/core/actions/BaseAction.ts index 126c649..f7ef471 100644 --- a/src/core/actions/BaseAction.ts +++ b/src/core/actions/BaseAction.ts @@ -179,38 +179,23 @@ module TK.SpaceTac { /** * Get the full list of diffs caused by applying this action + * + * This does not perform any check, and assumes the action is doable */ getDiffs(ship: Ship, battle: Battle, target = this.getDefaultTarget(ship)): BaseBattleDiff[] { - let reject = this.checkCannotBeApplied(ship); - if (reject) { - console.warn(`Action rejected - ${reject}`, ship, this, target); - return []; - } - - let checked_target = this.checkTarget(ship, target); - if (!checked_target) { - console.warn("Action rejected - invalid target", ship, this, target); - return []; - } - - let cost = this.getActionPointsUsage(ship, checked_target); - if (ship.getValue("power") < cost) { - console.warn("Action rejected - not enough power", ship, this, checked_target); - return []; - } - let result: BaseBattleDiff[] = []; // Action usage - result.push(new ShipActionUsedDiff(ship, this, checked_target)); + result.push(new ShipActionUsedDiff(ship, this, target)); // Power usage + let cost = this.getActionPointsUsage(ship, target); if (cost) { result = result.concat(ship.getValueDiffs("power", -cost, true)); } // Action effects - result = result.concat(this.getSpecificDiffs(ship, battle, checked_target)); + result = result.concat(this.getSpecificDiffs(ship, battle, target)); return result; } @@ -224,19 +209,34 @@ module TK.SpaceTac { /** * Apply the action on a battle state + * + * This will first check that the action can be done, then get the battle diffs and apply them. */ apply(battle: Battle, ship: Ship, target = this.getDefaultTarget(ship)): boolean { - if (this.checkTarget(ship, target)) { - let diffs = this.getDiffs(ship, battle, target); - if (diffs.length) { - battle.applyDiffs(diffs); - return true; - } else { - console.error("Could not apply action, no diff produced"); - return false; - } + let reject = this.checkCannotBeApplied(ship); + if (reject) { + console.warn(`Action rejected - ${reject}`, ship, this, target); + return false; + } + + let checked_target = this.checkTarget(ship, target); + if (!checked_target) { + console.warn("Action rejected - invalid target", ship, this, target); + return false; + } + + let cost = this.getActionPointsUsage(ship, checked_target); + if (ship.getValue("power") < cost) { + console.warn("Action rejected - not enough power", ship, this, checked_target); + return false; + } + + let diffs = this.getDiffs(ship, battle, checked_target); + if (diffs.length) { + battle.applyDiffs(diffs); + return true; } else { - console.error("Could not apply action, target rejected"); + console.error("Could not apply action, no diff produced"); return false; } } diff --git a/src/core/ai/Maneuver.spec.ts b/src/core/ai/Maneuver.spec.ts index 8426717..a3d9bf0 100644 --- a/src/core/ai/Maneuver.spec.ts +++ b/src/core/ai/Maneuver.spec.ts @@ -1,72 +1,35 @@ module TK.SpaceTac.Specs { testing("Maneuver", test => { - function compare_maneuver_effects(check: TestContext, meff1: ManeuverEffect[], meff2: ManeuverEffect[]): void { - check.equals(meff1.map(ef => ef.ship), meff2.map(ef => ef.ship), "impacted ships"); - compare_effects(check, meff1.map(ef => ef.effect), meff2.map(ef => ef.effect)); - check.equals(meff1.map(ef => ef.success), meff2.map(ef => ef.success), "success factor"); - } - - test.case("guesses weapon effects", check => { + test.case("uses move-fire simulation to build a list of battle diffs", check => { let battle = new Battle(); let ship1 = battle.fleets[0].addShip(); - let ship2 = battle.fleets[0].addShip(); + let ship2 = battle.fleets[1].addShip(); let ship3 = battle.fleets[1].addShip(); - let weapon = TestTools.addWeapon(ship1, 50, 0, 100, 10); + let ship4 = battle.fleets[1].addShip(); + let weapon = TestTools.addWeapon(ship1, 50, 2, 200, 100); + let engine = TestTools.addEngine(ship1, 100); ship1.setArenaPosition(0, 0); TestTools.setShipHP(ship1, 20, 20); - ship2.setArenaPosition(0, 5); - TestTools.setShipHP(ship2, 30, 30); - ship3.setArenaPosition(0, 15); - TestTools.setShipHP(ship3, 30, 30); - let maneuver = new Maneuver(ship1, nn(weapon.action), Target.newFromLocation(0, 0)); - compare_maneuver_effects(check, maneuver.effects, [ - { ship: ship1, effect: new DamageEffect(50), success: 1 }, - { ship: ship2, effect: new DamageEffect(50), success: 1 }, - ]); - }); + TestTools.setShipAP(ship1, 10); + ship2.setArenaPosition(500, 0); + TestTools.setShipHP(ship2, 70, 100); + ship3.setArenaPosition(560, 0); + TestTools.setShipHP(ship3, 80, 30); + ship4.setArenaPosition(640, 0); + TestTools.setShipHP(ship4, 30, 30); - test.case("guesses drone effects", check => { - let battle = new Battle(); - let ship1 = battle.fleets[0].addShip(); - let ship2 = battle.fleets[0].addShip(); - let ship3 = battle.fleets[1].addShip(); - let weapon = ship1.addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon)); - weapon.action = new DeployDroneAction(weapon, 0, 100, 10, [new ValueEffect("shield", 10)]); - ship1.setArenaPosition(0, 0); - TestTools.setShipHP(ship1, 20, 20); - ship2.setArenaPosition(0, 5); - TestTools.setShipHP(ship2, 30, 30); - ship3.setArenaPosition(0, 15); - TestTools.setShipHP(ship3, 30, 30); - let maneuver = new Maneuver(ship1, weapon.action, Target.newFromLocation(0, 0)); - compare_maneuver_effects(check, maneuver.effects, [ - { ship: ship1, effect: new ValueEffect("shield", 10), success: 1 }, - { ship: ship2, effect: new ValueEffect("shield", 10), success: 1 }, - ]); - }); - - test.case("guesses area effects on final location", check => { - let battle = new Battle(); - let ship = battle.fleets[0].addShip(); - let engine = TestTools.addEngine(ship, 500); - let move = nn(engine.action); - TestTools.setShipAP(ship, 10); - let drone = new Drone(ship); - drone.effects = [new AttributeEffect("maneuvrability", 1)]; - drone.x = 100; - drone.y = 0; - drone.radius = 50; - battle.addDrone(drone); - - let maneuver = new Maneuver(ship, move, Target.newFromLocation(40, 30)); - check.containing(maneuver.getFinalLocation(), { x: 40, y: 30 }); - check.equals(maneuver.effects, []); - - maneuver = new Maneuver(ship, move, Target.newFromLocation(100, 30)); - check.containing(maneuver.getFinalLocation(), { x: 100, y: 30 }); - compare_maneuver_effects(check, maneuver.effects, [ - { ship: ship, effect: new AttributeEffect("maneuvrability", 1), success: 1 }, - ]); + let maneuver = new Maneuver(ship1, nn(weapon.action), Target.newFromLocation(530, 0)); + check.contains(maneuver.effects, new ShipActionUsedDiff(ship1, nn(engine.action), Target.newFromLocation(331, 0)), "engine use"); + check.contains(maneuver.effects, new ShipValueDiff(ship1, "power", -4), "engine power"); + check.contains(maneuver.effects, new ShipMoveDiff(ship1, ship1.location, new ArenaLocationAngle(331, 0), engine), "move"); + check.contains(maneuver.effects, new ShipActionUsedDiff(ship1, nn(weapon.action), Target.newFromLocation(530, 0)), "weapon use"); + check.contains(maneuver.effects, new ProjectileFiredDiff(ship1, weapon, Target.newFromLocation(530, 0)), "weapon power"); + check.contains(maneuver.effects, new ShipValueDiff(ship1, "power", -2), "weapon power"); + check.contains(maneuver.effects, new ShipValueDiff(ship2, "shield", -50), "ship2 shield value"); + check.contains(maneuver.effects, new ShipValueDiff(ship3, "shield", -30), "ship3 shield value"); + check.contains(maneuver.effects, new ShipValueDiff(ship3, "hull", -20), "ship3 hull value"); + check.contains(maneuver.effects, new ShipDamageDiff(ship2, 0, 50, 50), "ship2 damage"); + check.contains(maneuver.effects, new ShipDamageDiff(ship3, 20, 30, 50), "ship3 damage"); }); }); } diff --git a/src/core/ai/Maneuver.ts b/src/core/ai/Maneuver.ts index 2370219..7254456 100644 --- a/src/core/ai/Maneuver.ts +++ b/src/core/ai/Maneuver.ts @@ -1,11 +1,4 @@ module TK.SpaceTac { - // Single effect of a maneuver - export type ManeuverEffect = { - ship: Ship - effect: BaseEffect - success: number - } - /** * Ship maneuver for an artifical intelligence * @@ -28,7 +21,7 @@ module TK.SpaceTac { simulation: MoveFireResult // List of guessed effects of this maneuver - effects: ManeuverEffect[] + effects: BaseBattleDiff[] constructor(ship: Ship, action: BaseAction, target: Target, move_margin = 1) { this.ship = ship; @@ -39,7 +32,9 @@ module TK.SpaceTac { let simulator = new MoveFireSimulator(this.ship); this.simulation = simulator.simulateAction(this.action, this.target, move_margin); - this.effects = this.guessEffects(); + this.effects = flatten(this.simulation.parts.map(part => + part.action.getDiffs(this.ship, this.battle, part.target) + )); } jasmineToString() { @@ -78,37 +73,6 @@ module TK.SpaceTac { return this.simulation.total_move_ap + this.simulation.total_fire_ap; } - /** - * Guess what will be the effects applied on any ship by this maneuver - */ - guessEffects(): ManeuverEffect[] { - let result: ManeuverEffect[] = []; - - // Effects of weapon - if (this.action instanceof TriggerAction) { - this.action.getEffects(this.ship, this.target).forEach(([ship, effect, success]) => { - result.push({ ship: ship, effect: effect, success: success }); - }) - } else if (this.action instanceof DeployDroneAction) { - let ships = this.battle.collectShipsInCircle(this.target, this.action.drone_radius, true); - this.action.drone_effects.forEach(effect => { - result = result.concat(ships.map(ship => ({ ship: ship, effect: effect, success: 1 }))); - }); - } - - // Area effects on final location - let location = this.getFinalLocation(); - let effects = this.battle.drones.list().forEach(drone => { - if (Target.newFromLocation(location.x, location.y).isInRange(drone.x, drone.y, drone.radius)) { - result = result.concat(drone.effects.map(effect => ( - { ship: this.ship, effect: effect, success: 1 } - ))); - } - }); - - return result; - } - /** * Standard feedback for this maneuver. It will apply it on the battle state. */ diff --git a/src/core/ai/TacticalAIHelpers.ts b/src/core/ai/TacticalAIHelpers.ts index 2076e7b..4dc270c 100644 --- a/src/core/ai/TacticalAIHelpers.ts +++ b/src/core/ai/TacticalAIHelpers.ts @@ -29,17 +29,13 @@ module TK.SpaceTac { let dhull = 0; let dshield = 0; - maneuver.effects.forEach(result => { - if (result.ship === ship) { - if (result.effect instanceof DamageEffect) { - let damage = result.effect.getEffectiveDamage(ship, result.success); - dhull -= damage.hull; - dshield -= damage.shield; - } else if (result.effect instanceof ValueEffect) { - if (result.effect.valuetype == "hull") { - dhull = clamp(hull + result.effect.value_on, 0, chull) - hull; - } else if (result.effect.valuetype == "shield") { - dshield += clamp(shield + result.effect.value_on, 0, cshield) - shield; + maneuver.effects.forEach(diff => { + if (diff instanceof ShipValueDiff) { + if (ship.is(diff.ship_id)) { + if (diff.code == "hull") { + dhull += clamp(hull + diff.diff, 0, chull) - hull; + } else if (diff.code == "shield") { + dshield += clamp(shield + diff.diff, 0, cshield) - shield; } } }