From 7e20599fcef7e36b82098030aeab58e6e68ac0f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Lemaire?= Date: Thu, 21 Dec 2017 20:04:54 +0100 Subject: [PATCH] Process AI in a web worker if available --- TODO.md | 2 +- out/aiworker.js | 23 ++++++++++ src/core/Battle.spec.ts | 2 +- src/core/Battle.ts | 51 +++++++++++++---------- src/core/TestTools.ts | 2 +- src/core/ai/AIWorker.ts | 76 ++++++++++++++++++++++++++++++++++ src/core/ai/AbstractAI.ts | 49 +++++++--------------- src/core/ai/Maneuver.ts | 23 ++++++++++ src/core/ai/TacticalAI.spec.ts | 9 +++- src/core/ai/TacticalAI.ts | 4 +- src/ui/battle/BattleView.ts | 2 +- 11 files changed, 177 insertions(+), 66 deletions(-) create mode 100644 out/aiworker.js create mode 100644 src/core/ai/AIWorker.ts diff --git a/TODO.md b/TODO.md index 876d7ad..299dd80 100644 --- a/TODO.md +++ b/TODO.md @@ -86,7 +86,6 @@ Artificial Intelligence * 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 -* Work in a dedicated process (webworker) Common UI --------- @@ -101,6 +100,7 @@ Common UI Technical --------- +* Remove references from battle internals (ships, fleets...) to universe (it causes large serialized battles in campaign mode) * Pack all images in atlases, and split them by stage * Pack sounds * Add toggles for shaders, automatically disable them if too slow, and initially disable them on mobile diff --git a/out/aiworker.js b/out/aiworker.js new file mode 100644 index 0000000..84d9c64 --- /dev/null +++ b/out/aiworker.js @@ -0,0 +1,23 @@ +var handler = { + get(target, name) { + return function () { } + } +} +var Phaser = new Proxy({}, handler); + +//var debug = console.log; +var debug = function () { }; + +importScripts("build.js"); + +onmessage = function (e) { + debug("[AI Worker] Received", e.data.length); + var serializer = new TK.Serializer(TK.SpaceTac); + var battle = serializer.unserialize(e.data); + var processing = new TK.SpaceTac.AIWorker(battle); + processing.processHere(function (maneuver) { + debug("[AI Worker] Send", maneuver); + postMessage(serializer.serialize(maneuver)); + return maneuver.apply(battle); + }).catch(postMessage).then(close); +} diff --git a/src/core/Battle.spec.ts b/src/core/Battle.spec.ts index 77a52dd..ee04ad7 100644 --- a/src/core/Battle.spec.ts +++ b/src/core/Battle.spec.ts @@ -118,7 +118,7 @@ module TK.SpaceTac { check.equals(battle.ships.list().filter(ship => ship.alive), [ship1, ship2, ship3, ship4], "alive ships"); }); - let result = battle.applyOneAction(nn(weapon.action), Target.newFromLocation(0, 0)); + let result = battle.applyOneAction(nn(weapon.action).id, Target.newFromLocation(0, 0)); check.equals(result, true, "action applied successfully"); check.in("after weapon", check => { check.same(battle.playing_ship, ship3, "playing ship"); diff --git a/src/core/Battle.ts b/src/core/Battle.ts index 9fd9d72..7b7f8c8 100644 --- a/src/core/Battle.ts +++ b/src/core/Battle.ts @@ -251,15 +251,13 @@ module TK.SpaceTac { /** * Make an AI play the current ship + * + * This will run asynchronous work in background, until the playing ship is changed */ - playAI(ai: AbstractAI | null = null, debug = false): boolean { + playAI(debug = false): boolean { if (this.playing_ship && !this.ai_playing) { this.ai_playing = true; - if (!ai) { - // TODO Use an AI adapted to the fleet - ai = new TacticalAI(this.playing_ship, this.timer); - } - ai.play(debug); + AIWorker.process(this, debug); return true; } else { return false; @@ -288,7 +286,7 @@ module TK.SpaceTac { */ advanceToNextShip(): void { if (this.playing_ship) { - this.applyOneAction(EndTurnAction.SINGLETON); + this.applyOneAction(EndTurnAction.SINGLETON.id); } else if (this.play_order.length) { this.setPlayingShip(this.play_order[0]); } @@ -354,30 +352,37 @@ module TK.SpaceTac { * * At the end of the action, some checks will be applied to ensure the battle state is consistent */ - applyOneAction(action: BaseAction, target?: Target): boolean { + applyOneAction(action_id: RObjectId, target?: Target): boolean { let ship = this.playing_ship; if (ship) { - if (!target) { - target = action.getDefaultTarget(ship); - } - if (action.apply(this, ship, target)) { - this.performChecks(); - - if (!this.ended) { - this.applyDiffs([new ShipActionEndedDiff(ship, action, target)]); - - if (ship.playing && ship.getValue("hull") <= 0) { - // Playing ship died during its action, force a turn end - this.applyOneAction(EndTurnAction.SINGLETON); - } + let action = ship.getAction(action_id); + if (action) { + if (!target) { + target = action.getDefaultTarget(ship); } - return true; + if (action.apply(this, ship, target)) { + this.performChecks(); + + if (!this.ended) { + this.applyDiffs([new ShipActionEndedDiff(ship, action, target)]); + + if (ship.playing && ship.getValue("hull") <= 0) { + // Playing ship died during its action, force a turn end + this.applyOneAction(EndTurnAction.SINGLETON.id); + } + } + + return true; + } else { + return false; + } } else { + console.error("Action not found on ship", action_id, ship); return false; } } else { - console.error("Cannot apply action - ship not playing", action, this); + console.error("Cannot apply action - ship not playing", action_id, this); return false; } } diff --git a/src/core/TestTools.ts b/src/core/TestTools.ts index d5323e3..7c4419c 100644 --- a/src/core/TestTools.ts +++ b/src/core/TestTools.ts @@ -146,7 +146,7 @@ module TK.SpaceTac { for (let i = 0; i < actions.length; i++) { let [ship, action, target] = actions[i]; battle.setPlayingShip(ship); - let result = battle.applyOneAction(action, target); + let result = battle.applyOneAction(action.id, target); check.equals(result, true, `action ${i + 1} successfully applied`); checks[i + 1](check.sub(`after action ${i + 1} applied`)); } diff --git a/src/core/ai/AIWorker.ts b/src/core/ai/AIWorker.ts new file mode 100644 index 0000000..d7d78ae --- /dev/null +++ b/src/core/ai/AIWorker.ts @@ -0,0 +1,76 @@ +module TK.SpaceTac { + /** + * AI processing, either in the current process or in a web worker + */ + export class AIWorker { + private battle: Battle; + private ship: Ship; + private debug: boolean; + + constructor(battle: Battle, debug = false) { + this.battle = battle; + this.ship = nn(battle.playing_ship); + this.debug = debug; + } + + /** + * Process the current playing ship with an AI + * + * This should be done on the real battle state + */ + static async process(battle: Battle, debug = false): Promise { + let processing = new AIWorker(battle, debug); + await processing.processAuto(maneuver => maneuver.apply(battle)); + } + + /** + * Process AI in a webworker if possible, else do the work in the render thread + */ + async processAuto(feedback: AIFeedback): Promise { + if ((window).Worker) { + await this.processInWorker(feedback); + } else { + await this.processHere(feedback); + } + } + + /** + * Process AI in a webworker + */ + async processInWorker(feedback: AIFeedback): Promise { + let worker = new Worker('aiworker.js'); // TODO not hard-coded + let serializer = new Serializer(TK.SpaceTac); + let promise = new Promise((resolve, reject) => { + worker.onerror = (error) => { + worker.terminate(); + reject(error); + }; + worker.onmessage = (message) => { + let maneuver = serializer.unserialize(message.data); + if (maneuver instanceof Maneuver) { + if (this.debug) { + console.log("Received from AI worker", maneuver); + } + let result = maneuver.apply(this.battle); + if (!result) { + resolve(); + } + } else { + worker.terminate(); + reject("Received something that is not a Maneuver"); + } + }; + }); + worker.postMessage(serializer.serialize(this.battle)); + await promise; + } + + /** + * Process AI in current thread + */ + async processHere(feedback: AIFeedback): Promise { + let ai = new TacticalAI(this.ship, feedback, this.debug); + await ai.play(); + } + } +} diff --git a/src/core/ai/AbstractAI.ts b/src/core/ai/AbstractAI.ts index 6c5d388..a2771df 100644 --- a/src/core/ai/AbstractAI.ts +++ b/src/core/ai/AbstractAI.ts @@ -1,4 +1,9 @@ module TK.SpaceTac { + /** + * Feeback that will be called with each proposed maneuver, and should return true if the AI is to continue playing + */ + export type AIFeedback = (maneuver: Maneuver) => boolean; + /** * Base class for all Artificial Intelligence interaction */ @@ -18,11 +23,16 @@ module TK.SpaceTac { // Debug mode debug = false + // Feedback to send maneuvers to + feedback: AIFeedback + // Time at which work as started private started: number - constructor(ship: Ship, timer = Timer.global, name?: string) { + constructor(ship: Ship, feedback?: AIFeedback, debug = false, timer = Timer.global, name?: string) { this.ship = ship; + this.feedback = feedback ? feedback : ((maneuver: Maneuver) => maneuver.apply(nn(this.ship.getBattle()))); + this.debug = debug; this.timer = timer; this.name = name || classname(this); } @@ -32,9 +42,8 @@ module TK.SpaceTac { /** * Start playing current ship's turn. */ - async play(debug = false): Promise { + async play(): Promise { this.started = (new Date()).getTime(); - this.debug = debug; if (!this.ship.playing) { console.error(`${this.name} tries to play a ship out of turn`); @@ -63,38 +72,8 @@ module TK.SpaceTac { } // End the ship turn - this.applyAction(EndTurnAction.SINGLETON, Target.newFromShip(ship)); - } - - /** - * Make the AI play an action - * - * This should be the only real interaction point with battle state - */ - private applyAction(action: BaseAction, target: Target): boolean { - let battle = this.ship.getBattle(); - if (battle) { - return battle.applyOneAction(action, target); - } else { - return false; - } - } - - /** - * Make the AI play a full maneuver (sequence of actions) - */ - applyManeuver(maneuver: Maneuver): boolean { - if (maneuver.simulation.success) { - let parts = maneuver.simulation.parts; - for (let i = 0; i < parts.length; i++) { - let part = parts[i]; - if (part.action instanceof EndTurnAction || !part.possible || !this.applyAction(part.action, part.target)) { - return false; - } - } - return true; - } else { - return false; + if (this.ship.playing) { + this.feedback(new Maneuver(this.ship, EndTurnAction.SINGLETON, Target.newFromShip(ship))); } } diff --git a/src/core/ai/Maneuver.ts b/src/core/ai/Maneuver.ts index e0c6738..2370219 100644 --- a/src/core/ai/Maneuver.ts +++ b/src/core/ai/Maneuver.ts @@ -108,5 +108,28 @@ module TK.SpaceTac { return result; } + + /** + * Standard feedback for this maneuver. It will apply it on the battle state. + */ + apply(battle: Battle): boolean { + if (!this.ship.is(battle.playing_ship)) { + console.error("Maneuver was not produced for the playing ship", this, battle); + return false; + } else if (!this.simulation.success) { + return false; + } else { + let parts = this.simulation.parts; + for (let i = 0; i < parts.length; i++) { + let part = parts[i]; + if (part.action instanceof EndTurnAction || part.possible) { + return battle.applyOneAction(part.action.id, part.target); + } else { + return false; + } + } + return this.mayContinue(); + } + } } } diff --git a/src/core/ai/TacticalAI.spec.ts b/src/core/ai/TacticalAI.spec.ts index b32876a..817936f 100644 --- a/src/core/ai/TacticalAI.spec.ts +++ b/src/core/ai/TacticalAI.spec.ts @@ -26,7 +26,13 @@ module TK.SpaceTac.Specs { let ship = battle.fleets[0].addShip(); TestTools.setShipPlaying(battle, ship); ship.playing = true; - let ai = new TacticalAI(ship, Timer.synchronous); + + let ai = new TacticalAI(ship, maneuver => { + if (maneuver instanceof FixedManeuver) { + applied.push(maneuver.score); + } + return false; + }, false, Timer.synchronous); check.patch(ai, "getDefaultProducers", () => [ producer(1, -8, 4), @@ -35,7 +41,6 @@ module TK.SpaceTac.Specs { check.patch(ai, "getDefaultEvaluators", () => [ (maneuver: Maneuver) => (maneuver).score ]); - check.patch(ai, "applyManeuver", (maneuver: FixedManeuver) => applied.push(maneuver.score)); ai.play(); check.equals(applied, [7]); diff --git a/src/core/ai/TacticalAI.ts b/src/core/ai/TacticalAI.ts index 7313ccd..9eefc7c 100644 --- a/src/core/ai/TacticalAI.ts +++ b/src/core/ai/TacticalAI.ts @@ -66,8 +66,8 @@ module TK.SpaceTac { return false; } - let success = this.applyManeuver(best_maneuver); - if (success && best_maneuver.mayContinue()) { + let success = this.feedback(best_maneuver); + if (success) { // Try to play another maneuver this.initWork(); return true; diff --git a/src/ui/battle/BattleView.ts b/src/ui/battle/BattleView.ts index 2603497..49091cd 100644 --- a/src/ui/battle/BattleView.ts +++ b/src/ui/battle/BattleView.ts @@ -194,7 +194,7 @@ module TK.SpaceTac.UI { if (ship) { let ship_action = first(ship.getAvailableActions(), ac => ac.is(action)); if (ship_action) { - let result = this.actual_battle.applyOneAction(action, target); + let result = this.actual_battle.applyOneAction(action.id, target); if (result) { this.setInteractionEnabled(false); }