diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 129ccdd..69b06ae 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -18,7 +18,7 @@ "taskName": "test", "isBuildCommand": false, "isTestCommand": true, - "showOutput": "silent", + "showOutput": "always", "problemMatcher": "$tsc" } ] diff --git a/src/core/Battle.ts b/src/core/Battle.ts index 1d66e39..bee2c88 100644 --- a/src/core/Battle.ts +++ b/src/core/Battle.ts @@ -68,6 +68,13 @@ module TS.SpaceTac { return ichainit(imap(iarray(this.fleets), fleet => iarray(fleet.ships))); } + /** + * Return an iterator over ships enemy of a player + */ + ienemies(player: Player, alive_only = false): Iterator { + return ifilter(this.iships(), ship => ship.getPlayer() != player && (ship.alive || !alive_only)); + } + // Check if a player is able to play // This can be used by the UI to determine if player interaction is allowed canPlay(player: Player): boolean { @@ -118,13 +125,7 @@ module TS.SpaceTac { // Collect all ships within a given radius of a target collectShipsInCircle(center: Target, radius: number, alive_only = false): Ship[] { - var result: Ship[] = []; - this.play_order.forEach(ship => { - if ((ship.alive || !alive_only) && Target.newFromShip(ship).getDistanceTo(center) <= radius) { - result.push(ship); - } - }); - return result; + return imaterialize(ifilter(this.iships(), ship => (ship.alive || !alive_only) && Target.newFromShip(ship).getDistanceTo(center) <= radius)); } // Ends a battle and sets the outcome @@ -221,9 +222,7 @@ module TS.SpaceTac { this.turn = 0; this.placeShips(); this.throwInitiative(); - this.play_order.forEach((ship: Ship) => { - ship.startBattle(); - }); + iforeach(this.iships(), ship => ship.startBattle()); this.advanceToNextShip(); } diff --git a/src/core/Equipment.ts b/src/core/Equipment.ts index 116b040..4e95839 100644 --- a/src/core/Equipment.ts +++ b/src/core/Equipment.ts @@ -48,6 +48,7 @@ module TS.SpaceTac { this.requirements = {}; this.permanent_effects = []; this.target_effects = []; + this.action = new BaseAction("nothing", "Do nothing", false); } // Returns true if the equipment can be equipped on a ship diff --git a/src/core/Fleet.ts b/src/core/Fleet.ts index 82a018f..7abeb62 100644 --- a/src/core/Fleet.ts +++ b/src/core/Fleet.ts @@ -46,7 +46,7 @@ module TS.SpaceTac { } // Add a ship in this fleet - addShip(ship: Ship): Ship { + addShip(ship = new Ship()): Ship { if (ship.fleet && ship.fleet != this) { remove(ship.fleet.ships, ship); } diff --git a/src/core/MoveFireSimulator.ts b/src/core/MoveFireSimulator.ts index a955aa5..4c1c9bb 100644 --- a/src/core/MoveFireSimulator.ts +++ b/src/core/MoveFireSimulator.ts @@ -3,7 +3,7 @@ module TS.SpaceTac { /** * A single action in the sequence result from the simulator */ - type MoveFirePart = { + export type MoveFirePart = { action: BaseAction target: Target ap: number @@ -12,7 +12,7 @@ module TS.SpaceTac { /** * A simulation result */ - class MoveFireResult { + export class MoveFireResult { // Simulation success, false only if no route can be found success = false // Ideal successive parts to make the full move+fire @@ -64,10 +64,11 @@ module TS.SpaceTac { let distance = Math.sqrt(dx * dx + dy * dy); let result = new MoveFireResult(); let ap = this.ship.values.power.get(); + let action_radius = action.getRangeRadius(this.ship); - if (distance > action.getRangeRadius(this.ship)) { + if (action instanceof MoveAction || distance > action_radius) { result.need_move = true; - let move_distance = distance - action.getRangeRadius(this.ship); + let move_distance = action instanceof MoveAction ? distance : distance - action_radius; let move_target = new Target(this.ship.arena_x + dx * move_distance / distance, this.ship.arena_y + dy * move_distance / distance, null); let engine = this.findBestEngine(); if (engine) { @@ -82,7 +83,7 @@ module TS.SpaceTac { } } - if (distance <= action.getRangeRadius(this.ship)) { + if (distance <= action_radius) { result.success = true; if (!(action instanceof MoveAction)) { result.need_fire = true; diff --git a/src/core/TestTools.ts b/src/core/TestTools.ts index 48e49b3..44ff014 100644 --- a/src/core/TestTools.ts +++ b/src/core/TestTools.ts @@ -42,6 +42,19 @@ module TS.SpaceTac { return equipment; } + /** + * Add a weapon to a ship + */ + static addWeapon(ship: Ship, damage = 100, power_usage = 1, max_distance = 100, blast = 0): Equipment { + var equipment = ship.addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon)); + equipment.action = new FireWeaponAction(equipment, blast != 0, "Test Weapon"); + equipment.ap_usage = power_usage; + equipment.blast = blast; + equipment.distance = max_distance; + equipment.target_effects.push(new DamageEffect(damage)); + return equipment; + } + // Set a ship action points, adding/updating an equipment if needed static setShipAP(ship: Ship, points: number, recovery: number = 0): void { var equipment = this.getOrGenEquipment(ship, SlotType.Power, new Equipments.BasicPowerCore()); diff --git a/src/core/actions/FireWeaponAction.ts b/src/core/actions/FireWeaponAction.ts index 60b36ce..19e26f5 100644 --- a/src/core/actions/FireWeaponAction.ts +++ b/src/core/actions/FireWeaponAction.ts @@ -35,29 +35,29 @@ module TS.SpaceTac { } } - protected customApply(battle: Battle, ship: Ship, target: Target) { - var affected: Ship[] = []; - var blast = this.getBlastRadius(ship); + /** + * Collect the effects applied by this action + */ + getEffects(battle: Battle, ship: Ship, target: Target): [Ship, BaseEffect][] { + let result: [Ship, BaseEffect][] = []; + let blast = this.getBlastRadius(ship); + let ships = blast ? battle.collectShipsInCircle(target, blast, true) : ((target.ship && target.ship.alive) ? [target.ship] : []); + ships.forEach(ship => { + this.equipment.target_effects.forEach(effect => result.push([ship, effect])); + }); + return result; + } + protected customApply(battle: Battle, ship: Ship, target: Target) { // Face the target ship.rotate(Target.newFromShip(ship).getAngleTo(target)); - // Collect affected ships - if (blast) { - affected = affected.concat(battle.collectShipsInCircle(target, blast, true)); - } else if (target.ship && target.ship.alive) { - affected.push(target.ship); - } - // Fire event ship.addBattleEvent(new FireEvent(ship, this.equipment, target)); - // Apply all target effects - affected.forEach((affship: Ship) => { - this.equipment.target_effects.forEach((effect: BaseEffect) => { - effect.applyOnShip(affship); - }); - }); + // Apply effects + let effects = this.getEffects(battle, ship, target); + effects.forEach(([ship, effect]) => effect.applyOnShip(ship)); } } } diff --git a/src/core/ai/AIDuel.ts b/src/core/ai/AIDuel.ts index 4046ecb..dfc324c 100644 --- a/src/core/ai/AIDuel.ts +++ b/src/core/ai/AIDuel.ts @@ -69,10 +69,11 @@ module TS.SpaceTac { console.log(`${this.ai1.name} vs ${this.ai2.name} ...`); let battle = Battle.newQuickRandom(); - let playing = battle.playing_ship; while (!battle.ended && battle.turn < 100) { //console.debug(`Turn ${battle.turn} - Ship ${battle.play_order.indexOf(playing)}`); + + let playing = battle.playing_ship; let ai = (playing.fleet == battle.fleets[0]) ? this.ai1 : this.ai2; ai.timer = Timer.synchronous; ai.ship = playing; @@ -82,7 +83,6 @@ module TS.SpaceTac { console.error(`${ai.name} did not end its turn !`); battle.advanceToNextShip(); } - playing = battle.playing_ship; } if (battle.ended && !battle.outcome.draw) { diff --git a/src/core/ai/AbstractAI.ts b/src/core/ai/AbstractAI.ts index 4a5ffc1..97c7dfd 100644 --- a/src/core/ai/AbstractAI.ts +++ b/src/core/ai/AbstractAI.ts @@ -26,7 +26,7 @@ module TS.SpaceTac { this.name = name || classname(this); this.ship = ship; this.workqueue = []; - this.random = new RandomGenerator(); + this.random = RandomGenerator.global; this.timer = timer; } @@ -77,7 +77,7 @@ module TS.SpaceTac { /** * Get the time spent thinking by the AI. */ - private getDuration() { + protected getDuration() { return (new Date()).getTime() - this.started; } diff --git a/src/core/ai/BullyAI.spec.ts b/src/core/ai/BullyAI.spec.ts index 37fab9a..68d65e1 100644 --- a/src/core/ai/BullyAI.spec.ts +++ b/src/core/ai/BullyAI.spec.ts @@ -45,8 +45,7 @@ module TS.SpaceTac.Specs { engine.ap_usage = 3; engine.distance = 1; ship.addSlot(SlotType.Engine).attach(engine); - ship.values.power.setMaximal(10); - ship.values.power.set(8); + TestTools.setShipAP(ship, 10); var enemy = new Ship(); var ai = new BullyAI(ship, Timer.synchronous); ai.ship = ship; @@ -54,6 +53,7 @@ module TS.SpaceTac.Specs { var weapon = new Equipment(SlotType.Weapon); weapon.ap_usage = 2; weapon.distance = 3; + ship.addSlot(SlotType.Weapon).attach(weapon); // enemy in range, the ship can fire without moving ship.values.power.set(8); diff --git a/src/core/ai/Maneuver.ts b/src/core/ai/Maneuver.ts index cd57cf5..0a1d8e7 100644 --- a/src/core/ai/Maneuver.ts +++ b/src/core/ai/Maneuver.ts @@ -1,6 +1,9 @@ module TS.SpaceTac { - // Ship maneuver for an artifical intelligence - // A maneuver is like a human player action, choosing an equipment and using it + /** + * Ship maneuver for an artifical intelligence + * + * A maneuver is like a human player action, choosing an equipment and using it + */ export class Maneuver { // Concerned ship ship: Ship; @@ -11,17 +14,28 @@ module TS.SpaceTac { // Target for the action; target: Target; + // Result of move-fire simulation + simulation: MoveFireResult; + constructor(ship: Ship, equipment: Equipment, target: Target) { this.ship = ship; this.equipment = equipment; this.target = target; + + let simulator = new MoveFireSimulator(this.ship); + this.simulation = simulator.simulateAction(this.equipment.action, this.target); } - // Apply the maneuver in current battle + /** + * Apply the maneuver in current battle + */ apply(): void { - let result = this.equipment.action.apply(this.ship.getBattle(), this.ship, this.target); - if (!result) { - console.warn("AI could not apply maneuver", this); + if (this.simulation.success) { + this.simulation.parts.forEach(part => { + if (!part.action.apply(this.ship.getBattle(), this.ship, part.target)) { + console.error("AI cannot apply maneuver", this); + } + }); } } } diff --git a/src/core/ai/TacticalAI.spec.ts b/src/core/ai/TacticalAI.spec.ts index 7c281a7..8a49ff4 100644 --- a/src/core/ai/TacticalAI.spec.ts +++ b/src/core/ai/TacticalAI.spec.ts @@ -31,25 +31,5 @@ module TS.SpaceTac.Specs { ai.play(); expect(applied).toEqual([7]); }); - - it("produces direct weapon shots", function () { - let battle = new Battle(); - let ship0a = battle.fleets[0].addShip(new Ship(null, "0A")); - let ship0b = battle.fleets[0].addShip(new Ship(null, "0B")); - let ship1a = battle.fleets[1].addShip(new Ship(null, "1A")); - let ship1b = battle.fleets[1].addShip(new Ship(null, "1B")); - - let result = imaterialize(produceDirectWeapon(ship0a, battle)); - expect(result.length).toBe(0); - - let weapon1 = ship0a.addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon)); - let weapon2 = ship0a.addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon)); - result = imaterialize(produceDirectWeapon(ship0a, battle)); - expect(result.length).toBe(4); - expect(result).toContain(new Maneuver(ship0a, weapon1, Target.newFromShip(ship1a))); - expect(result).toContain(new Maneuver(ship0a, weapon1, Target.newFromShip(ship1b))); - expect(result).toContain(new Maneuver(ship0a, weapon2, Target.newFromShip(ship1a))); - expect(result).toContain(new Maneuver(ship0a, weapon2, Target.newFromShip(ship1b))); - }); }); } diff --git a/src/core/ai/TacticalAI.ts b/src/core/ai/TacticalAI.ts index b8778c4..b0f1132 100644 --- a/src/core/ai/TacticalAI.ts +++ b/src/core/ai/TacticalAI.ts @@ -2,8 +2,8 @@ /// module TS.SpaceTac { - type TacticalProducer = Iterator; - type TacticalEvaluator = (Maneuver) => number; + export type TacticalProducer = Iterator; + export type TacticalEvaluator = (Maneuver) => number; /** * AI that applies a set of tactical rules @@ -39,35 +39,39 @@ module TS.SpaceTac { } /** - * Single unit of work => produce a single maneuver and evaluate it + * Single unit of work => produce a batch of maneuvers and evaluate them + * + * The best produced maneuver (highest evaluation score) is kept to be played. + * If two maneuvers have nearly the same score, the best one is randomly chosen. */ private unitWork() { - if (this.producers.length == 0) { - return; - } + let done = 0; - // Produce a maneuver - let maneuver: Maneuver; - let producer = this.producers.shift(); - [maneuver, producer] = producer(); + while (done < 1000 && this.producers.length > 0) { + // Produce a maneuver + let maneuver: Maneuver; + let producer = this.producers.shift(); + [maneuver, producer] = producer(); - if (maneuver) { - this.producers.push(producer); + if (maneuver) { + this.producers.push(producer); - // Evaluate the maneuver - let score = this.evaluate(maneuver); - if (score > this.best_score) { - this.best = maneuver; - this.best_score = score; + // Evaluate the maneuver + let score = this.evaluate(maneuver); + //console.log(maneuver, score); + if ((Math.abs(score - this.best_score) < 0.0001 && this.random.bool()) || score > this.best_score) { + this.best = maneuver; + this.best_score = score; + } } + + done += 1; } - // Continue or stop ? - if (this.producers.length > 0) { + // Continue or stop + if (this.producers.length > 0 && this.getDuration() < 3000) { this.addWorkItem(() => this.unitWork()); } else if (this.best) { - // TODO Also apply after a certain time of not finding better - // TODO If not in range for action, make an approach move this.best.apply(); } } @@ -76,22 +80,25 @@ module TS.SpaceTac { * Setup the default set of maneuver producers */ private setupDefaultProducers() { - this.producers.push(produceDirectWeapon(this.ship, this.ship.getBattle())); + let producers = [ + TacticalAIHelpers.produceDirectShots, + TacticalAIHelpers.produceBlastShots, + TacticalAIHelpers.produceRandomMoves, + ] + producers.forEach(producer => this.producers.push(producer(this.ship, this.ship.getBattle()))); } /** * Setup the default set of maneuver evaluators */ private setupDefaultEvaluators() { + let scaled = (evaluator: (...args) => number, factor: number) => (...args) => factor * evaluator(...args); + let evaluators = [ + scaled(TacticalAIHelpers.evaluateTurnCost, 1), + scaled(TacticalAIHelpers.evaluateDamageToEnemy, 30), + scaled(TacticalAIHelpers.evaluateClustering, 3), + ] + evaluators.forEach(evaluator => this.evaluators.push((maneuver: Maneuver) => evaluator(this.ship, this.ship.getBattle(), maneuver))); } } - - /** - * Produce all "direct hit" weapon shots. - */ - export function produceDirectWeapon(ship: Ship, battle: Battle): TacticalProducer { - let enemies = ifilter(battle.iships(), iship => iship.getPlayer() != ship.getPlayer()); - let weapons = iarray(ship.listEquipment(SlotType.Weapon)); - return imap(icombine(enemies, weapons), ([enemy, weapon]) => new Maneuver(ship, weapon, Target.newFromShip(enemy))); - } } diff --git a/src/core/ai/TacticalAIHelpers.spec.ts b/src/core/ai/TacticalAIHelpers.spec.ts new file mode 100644 index 0000000..d393efc --- /dev/null +++ b/src/core/ai/TacticalAIHelpers.spec.ts @@ -0,0 +1,145 @@ +module TS.SpaceTac.Specs { + describe("TacticalAIHelpers", function () { + it("produces direct weapon shots", function () { + let battle = new Battle(); + let ship0a = battle.fleets[0].addShip(new Ship(null, "0A")); + let ship0b = battle.fleets[0].addShip(new Ship(null, "0B")); + let ship1a = battle.fleets[1].addShip(new Ship(null, "1A")); + let ship1b = battle.fleets[1].addShip(new Ship(null, "1B")); + + let result = imaterialize(TacticalAIHelpers.produceDirectShots(ship0a, battle)); + expect(result.length).toBe(0); + + let weapon1 = ship0a.addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon)); + let weapon2 = ship0a.addSlot(SlotType.Weapon).attach(new Equipment(SlotType.Weapon)); + result = imaterialize(TacticalAIHelpers.produceDirectShots(ship0a, battle)); + expect(result.length).toBe(4); + expect(result).toContain(new Maneuver(ship0a, weapon1, Target.newFromShip(ship1a))); + expect(result).toContain(new Maneuver(ship0a, weapon1, Target.newFromShip(ship1b))); + expect(result).toContain(new Maneuver(ship0a, weapon2, Target.newFromShip(ship1a))); + expect(result).toContain(new Maneuver(ship0a, weapon2, Target.newFromShip(ship1b))); + }); + + it("produces random moves inside a grid", function () { + let battle = new Battle(); + battle.width = 100; + battle.height = 100; + let ship = battle.fleets[0].addShip(); + + let result = imaterialize(TacticalAIHelpers.produceRandomMoves(ship, battle, 2, 1)); + expect(result.length).toBe(0); + + let engine = ship.addSlot(SlotType.Engine).attach(new Equipment(SlotType.Engine)); + + result = imaterialize(TacticalAIHelpers.produceRandomMoves(ship, battle, 2, 1, new SkewedRandomGenerator([0.5], true))); + expect(result).toEqual([ + new Maneuver(ship, engine, Target.newFromLocation(25, 25)), + new Maneuver(ship, engine, Target.newFromLocation(75, 25)), + new Maneuver(ship, engine, Target.newFromLocation(25, 75)), + new Maneuver(ship, engine, Target.newFromLocation(75, 75)), + ]); + }); + + it("produces blast shots", function () { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); + let weapon = TestTools.addWeapon(ship, 50, 1, 1000, 105); + + let result = imaterialize(TacticalAIHelpers.produceBlastShots(ship, battle)); + expect(result.length).toBe(0); + + let enemy1 = battle.fleets[1].addShip(); + enemy1.setArenaPosition(500, 0); + + result = imaterialize(TacticalAIHelpers.produceBlastShots(ship, battle)); + expect(result.length).toBe(0); + + let enemy2 = battle.fleets[1].addShip(); + enemy2.setArenaPosition(700, 0); + + result = imaterialize(TacticalAIHelpers.produceBlastShots(ship, battle)); + expect(result).toEqual([ + new Maneuver(ship, weapon, Target.newFromLocation(600, 0)), + ]); + + let enemy3 = battle.fleets[1].addShip(); + enemy3.setArenaPosition(700, 300); + + result = imaterialize(TacticalAIHelpers.produceBlastShots(ship, battle)); + expect(result).toEqual([ + new Maneuver(ship, weapon, Target.newFromLocation(600, 0)), + ]); + }); + + it("evaluates turn cost", function () { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); + let weapon = TestTools.addWeapon(ship, 50, 5, 100); + let engine = TestTools.addEngine(ship, 25); + + let maneuver = new Maneuver(ship, weapon, Target.newFromLocation(100, 0)); + expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(-Infinity); + + TestTools.setShipAP(ship, 10); + expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(0.5); // 5 power remaining on 10 + + maneuver = new Maneuver(ship, weapon, Target.newFromLocation(110, 0)); + expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(0.4); // 4 power remaining on 10 + + maneuver = new Maneuver(ship, weapon, Target.newFromLocation(140, 0)); + expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(0.3); // 3 power remaining on 10 + + maneuver = new Maneuver(ship, weapon, Target.newFromLocation(310, 0)); + expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(-1); // can't do in one turn + }); + + it("evaluates damage to enemies", function () { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); + let weapon = TestTools.addWeapon(ship, 50, 5, 500, 100); + + let enemy1 = battle.fleets[1].addShip(); + enemy1.setArenaPosition(250, 0); + TestTools.setShipHP(enemy1, 50, 25); + let enemy2 = battle.fleets[1].addShip(); + enemy2.setArenaPosition(300, 0); + TestTools.setShipHP(enemy2, 25, 0); + + // no enemies hurt + let maneuver = new Maneuver(ship, weapon, Target.newFromLocation(100, 0)); + expect(TacticalAIHelpers.evaluateDamageToEnemy(ship, battle, maneuver)).toEqual(0); + + // one enemy loses half-life + maneuver = new Maneuver(ship, weapon, Target.newFromLocation(180, 0)); + expect(TacticalAIHelpers.evaluateDamageToEnemy(ship, battle, maneuver)).toEqual(0.25); + + // one enemy loses half-life, the other one is dead + maneuver = new Maneuver(ship, weapon, Target.newFromLocation(280, 0)); + expect(TacticalAIHelpers.evaluateDamageToEnemy(ship, battle, maneuver)).toEqual(0.625); + }); + + it("evaluates ship clustering", function () { + let battle = new Battle(); + let ship = battle.fleets[0].addShip(); + TestTools.addEngine(ship, 100); + TestTools.setShipAP(ship, 10); + let weapon = TestTools.addWeapon(ship, 100, 1, 100, 10); + + let maneuver = new Maneuver(ship, weapon, Target.newFromLocation(200, 0)); + expect(maneuver.simulation.move_location).toEqual(Target.newFromLocation(100, 0)); + expect(TacticalAIHelpers.evaluateClustering(ship, battle, maneuver)).toEqual(0); + + battle.fleets[1].addShip().setArenaPosition(battle.width, battle.height); + expect(TacticalAIHelpers.evaluateClustering(ship, battle, maneuver)).toBeCloseTo(-0.01, 2); + + battle.fleets[1].addShip().setArenaPosition(120, 40); + expect(TacticalAIHelpers.evaluateClustering(ship, battle, maneuver)).toBeCloseTo(-0.4, 1); + + battle.fleets[0].addShip().setArenaPosition(80, 60); + expect(TacticalAIHelpers.evaluateClustering(ship, battle, maneuver)).toBeCloseTo(-0.7, 1); + + battle.fleets[0].addShip().setArenaPosition(110, 20); + expect(TacticalAIHelpers.evaluateClustering(ship, battle, maneuver)).toEqual(-1); + }); + }); +} diff --git a/src/core/ai/TacticalAIHelpers.ts b/src/core/ai/TacticalAIHelpers.ts new file mode 100644 index 0000000..835f190 --- /dev/null +++ b/src/core/ai/TacticalAIHelpers.ts @@ -0,0 +1,110 @@ +module TS.SpaceTac { + /** + * Standard producers and evaluators for TacticalAI + * + * These are static methods that may be used as base for TacticalAI ruleset. + */ + export class TacticalAIHelpers { + /** + * Produce all "direct hit" weapon shots. + */ + static produceDirectShots(ship: Ship, battle: Battle): TacticalProducer { + let enemies = ifilter(battle.iships(), iship => iship.getPlayer() != ship.getPlayer()); + let weapons = iarray(ship.listEquipment(SlotType.Weapon)); + return imap(icombine(enemies, weapons), ([enemy, weapon]) => new Maneuver(ship, weapon, Target.newFromShip(enemy))); + } + + /** + * Produce random moves inside arena cell + */ + static produceRandomMoves(ship: Ship, battle: Battle, cells = 10, iterations = 1, random = RandomGenerator.global): TacticalProducer { + let engines = ship.listEquipment(SlotType.Engine); + if (engines.length == 0) { + return IEMPTY; + } + + return ichainit(imap(irange(iterations), iteration => + imap(irange(cells * cells), cellpos => { + let y = Math.floor(cellpos / cells); + let x = cellpos - y * cells; + let target = Target.newFromLocation((x + random.random()) * battle.width / cells, (y + random.random()) * battle.height / cells); + return new Maneuver(ship, engines[0], target); + }) + )); + } + + /** + * Produce blast weapon shots, with multiple targets. + */ + static produceBlastShots(ship: Ship, battle: Battle): TacticalProducer { + // TODO Work with groups of 3, 4 ... + let weapons = ifilter(iarray(ship.listEquipment(SlotType.Weapon)), weapon => weapon.blast > 0); + let enemies = battle.ienemies(ship.getPlayer()); + let couples = ifilter(icombine(enemies, enemies), ([e1, e2]) => e1 != e2); + let candidates = ifilter(icombine(weapons, couples), ([weapon, [e1, e2]]) => Target.newFromShip(e1).getDistanceTo(Target.newFromShip(e2)) < weapon.blast * 2); + let result = imap(candidates, ([weapon, [e1, e2]]) => new Maneuver(ship, weapon, Target.newFromLocation((e1.arena_x + e2.arena_x) / 2, (e1.arena_y + e2.arena_y) / 2))); + return result; + } + + /** + * Evaluate the number of turns necessary for the maneuver, between -1 and 1 + */ + static evaluateTurnCost(ship: Ship, battle: Battle, maneuver: Maneuver): number { + let powerusage = maneuver.simulation.total_move_ap + maneuver.simulation.total_fire_ap; + if (maneuver.simulation.total_fire_ap > ship.getAttribute("power_capacity")) { + return -Infinity; + } else if (powerusage > ship.getValue("power")) { + return -1; + } else { + return (ship.getValue("power") - powerusage) / ship.getAttribute("power_capacity"); + } + } + + /** + * Evaluate the damage done to the enemy, between -1 and 1 + */ + static evaluateDamageToEnemy(ship: Ship, battle: Battle, maneuver: Maneuver): number { + let action = maneuver.equipment.action; + if (action instanceof FireWeaponAction) { + let enemies = imaterialize(battle.ienemies(ship.getPlayer(), true)); + if (enemies.length == 0) { + return 0; + } + let damage = 0; + let dead = 0; + let effects = action.getEffects(battle, ship, maneuver.target); + effects.forEach(([ship, effect]) => { + if (effect instanceof DamageEffect && contains(enemies, ship)) { + let [shield, hull] = effect.getEffectiveDamage(ship); + damage += shield + hull; + if (hull == ship.getValue("hull")) { + dead += 1 + } + } + }); + let hp = sum(enemies.map(enemy => enemy.getValue("hull") + enemy.getValue("shield"))); + let result = 0.5 * (damage / hp) + 0.5 * (dead / enemies.length); + return result; + } else { + return 0; + } + } + + /** + * Evaluate the clustering of ships, between -1 and 1 + */ + static evaluateClustering(ship: Ship, battle: Battle, maneuver: Maneuver): number { + // TODO Take into account blast radius of in-play weapons + let move_location = maneuver.simulation.move_location || Target.newFromShip(ship); + 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); + //console.log(distances, result); + return result; + } + } + } +} \ No newline at end of file diff --git a/src/core/effects/DamageEffect.ts b/src/core/effects/DamageEffect.ts index 2f513bf..9525e56 100644 --- a/src/core/effects/DamageEffect.ts +++ b/src/core/effects/DamageEffect.ts @@ -16,7 +16,10 @@ module TS.SpaceTac { this.value = value; } - applyOnShip(ship: Ship): boolean { + /** + * Get the effective damage done to both shield and hull (in this order) + */ + getEffectiveDamage(ship: Ship): [number, number] { var damage = this.value; var hull: number; var shield: number; @@ -36,7 +39,11 @@ module TS.SpaceTac { hull = damage; } - // Effective damages on ship + return [shield, hull]; + } + + applyOnShip(ship: Ship): boolean { + let [shield, hull] = this.getEffectiveDamage(ship); ship.addDamage(hull, shield); return true;