diff --git a/out/aiworker.js b/out/aiworker.js index 790244c..278b140 100644 --- a/out/aiworker.js +++ b/out/aiworker.js @@ -5,17 +5,5 @@ var handler = { } var Phaser = new Proxy(function () { }, handler); -//var debug = console.log; -var debug = function () { }; - importScripts("app.js"); - -onmessage = function (e) { - debug("[AI Worker] Received", e.data.length); - var serializer = new TK.Serializer(TK.SpaceTac); - var processing = serializer.unserialize(e.data); - processing.processHere().then(function (plan) { - debug("[AI Worker] Send", plan); - postMessage(serializer.serialize(plan)); - }).catch(postMessage); -} +TK.SpaceTac.AIRunnerInWorker.run(); diff --git a/src/core/Battle.ts b/src/core/Battle.ts index 345e287..a5cd250 100644 --- a/src/core/Battle.ts +++ b/src/core/Battle.ts @@ -154,13 +154,6 @@ module TK.SpaceTac { this.applyDiffs([new EndBattleDiff(winner, this.turncount)]); } - /** - * Get AI plan - */ - getAIPlan(player: Player, debug = false): Promise { - return AIWorker.process(this, player, debug); - } - /** * Start the battle * diff --git a/src/core/ai/AIRunner.ts b/src/core/ai/AIRunner.ts new file mode 100644 index 0000000..f104c02 --- /dev/null +++ b/src/core/ai/AIRunner.ts @@ -0,0 +1,152 @@ +module TK.SpaceTac { + /** + * Interface for AI runners + */ + export interface AbstractAIRunner { + startWork(battle: Battle, player: Player, debug: boolean): Promise + getResult(): Promise + } + + /** + * AI runner that does a single work at a time in current thread + */ + class AILocalRunner implements AbstractAIRunner { + private running?: ContinuousAI + + async startWork(battle: Battle, player: Player, debug: boolean): Promise { + if (this.running) { + await this.getResult(); + await this.startWork(battle, player, debug); + } else { + const settings = AISettingsStock.default(battle, player); // TODO settings choice ? + this.running = new ContinuousAI(settings, debug); + this.running.play(); + } + } + + async getResult(): Promise { + if (this.running) { + const ai = this.running; + this.running = undefined; + const aiplan = await ai.getPlan(); + return aiplan.plan; + } else { + throw new Error("Asked for an AI plan but AI is not processing"); + } + } + } + + /** + * AI runner that delegates work in a web worker + */ + class AIWorkerProxyRunner implements AbstractAIRunner { + private worker: Worker + private serializer = new Serializer(TK.SpaceTac) + + constructor(jsfile = 'aiworker.js') { + try { + this.worker = new Worker(jsfile); + } catch { + throw new Error("Could not initialize AI web worker"); + } + } + + private call(method: string, ...args: any[]): Promise { + const result = new Promise((resolve, reject) => { + this.worker.onerror = reject; + this.worker.onmessage = message => { + const result = this.serializer.unserialize(message.data); + resolve(result); + } + }); + this.worker.postMessage(this.serializer.serialize({ method, args })); + return result; + } + + startWork(battle: Battle, player: Player, debug: boolean): Promise { + return this.call("startWork", battle, player, debug); + } + + async getResult(): Promise { + const result = await this.call("getResult"); + if (result.hasOwnProperty("fleets")) { + return result; + } else { + throw new Error("Web worker result is not an AIPlan"); + } + } + } + + /** + * AI runner that runs inside the web worker + */ + export class AIRunnerInWorker extends AILocalRunner { + private serializer = new Serializer(TK.SpaceTac) + + static run(debug = false): void { + const manager = new AIRunnerInWorker(); + const log = debug ? console.log : nop; + + if (typeof self != "undefined" && self.hasOwnProperty("postMessage")) { + const worker = (self) as Worker; + worker.addEventListener('message', async event => { + log("[AIRunnerInWorker] Received", event.data); + const result = await manager.onCall(event.data); + worker.postMessage(result); + log("[AIRunnerInWorker] Send", result); + }); + } else { + throw new Error("Web worker tools are not available"); + } + } + + async onCall(data: any): Promise { + const { method, args } = this.serializer.unserialize(data); + const func = bound(this, method); + const result = await func(...args); + return this.serializer.serialize(result); + } + } + + /** + * AI runner that delegates to a web worker if possible, or falls back to the main thread + */ + export class AIRunner implements AbstractAIRunner { + private _delegate?: AbstractAIRunner + + constructor(private allow_webworker = true) { + } + + /** + * Get a new delegate runner to work with + */ + private static createDelegate(allow_webworker = true): AbstractAIRunner { + if (allow_webworker) { + try { + return new AIWorkerProxyRunner(); + } catch { + console.error("Web worker not available for AI, falling back to main thread"); + return new AILocalRunner(); + } + } else { + return new AILocalRunner(); + } + } + + get delegate(): AbstractAIRunner { + if (!this._delegate) { + this._delegate = AIRunner.createDelegate(this.allow_webworker); + } + + return this._delegate; + } + + startWork(battle: Battle, player: Player, debug = false): Promise { + return this.delegate.startWork(battle, player, debug); + } + + getResult(): Promise { + return this.delegate.getResult(); + } + } +} diff --git a/src/core/ai/AIWorker.ts b/src/core/ai/AIWorker.ts deleted file mode 100644 index 690d7b4..0000000 --- a/src/core/ai/AIWorker.ts +++ /dev/null @@ -1,84 +0,0 @@ -module TK.SpaceTac { - /** - * Initialize the background worker, if possible - */ - function initializeWorker(): Worker | null { - if (typeof window != "undefined" && (window).Worker) { - try { - return new Worker('aiworker.js'); // TODO not hard-coded - } catch { - console.error("Could not initialize AI web worker"); - return null; - } - } else { - return null; - } - } - - /** - * AI processing, either in the current process or in a web worker - */ - export class AIWorker { - private static worker = initializeWorker(); - - constructor(private battle: Battle, private player = battle.fleets[1].player, private debug = false) { - } - - /** - * Process the current playing ship with an AI - * - * This should be done on the real battle state - */ - static process(battle: Battle, player?: Player, debug = false): Promise { - const processing = new AIWorker(battle, player, debug); - return processing.processAuto(); - } - - /** - * Process AI in a webworker if possible, else do the work in the render thread - */ - processAuto(): Promise { - if (!this.debug && AIWorker.worker) { - try { - return this.processInWorker(AIWorker.worker); - } catch (err) { - console.error("Web worker error, falling back to main thread", err); - return this.processHere(); - } - } else { - return this.processHere(); - } - } - - /** - * Process AI in a webworker - */ - processInWorker(worker: Worker): Promise { - let serializer = new Serializer(TK.SpaceTac); - let promise: Promise = new Promise((resolve, reject) => { - worker.onerror = reject; - worker.onmessage = (message) => { - let plan = serializer.unserialize(message.data); - if (this.debug) { - console.log("Received from AI worker", plan); - } - // TODO check type - resolve(plan); - }; - }); - worker.postMessage(serializer.serialize(this)); - return promise; - } - - /** - * Process AI in current thread - */ - async processHere(): Promise { - const settings = AISettingsStock.default(this.battle, this.player); // TODO settings choice ? - let ai = new ContinuousAI(settings, this.debug); - ai.play(); - const result = await ai.getPlan(); // TODO Only when human player is done - return result.plan; - } - } -} diff --git a/src/ui/battle/ArenaShip.ts b/src/ui/battle/ArenaShip.ts index dc1a92e..8daf767 100644 --- a/src/ui/battle/ArenaShip.ts +++ b/src/ui/battle/ArenaShip.ts @@ -141,7 +141,7 @@ module TK.SpaceTac.UI { background: async (speed: number) => { if (speed) { this.displayAttributeChanged(diff, speed); - await timer.sleep(2000 / speed); + await timer.sleep(500 / speed); } } } @@ -150,7 +150,7 @@ module TK.SpaceTac.UI { background: async (speed: number) => { if (speed) { await this.displayEffect(`${diff.theoretical} damage`, false, speed); - await timer.sleep(1000 / speed); + await timer.sleep(500 / speed); } } } @@ -296,8 +296,8 @@ module TK.SpaceTac.UI { (this.ship.arena_y < arena.height * 0.9) ? 76 : (-66 - this.effects_messages.height) ); - this.effects_messages_toggle.manipulate("added")(1400 / speed); - await this.battleview.timer.sleep(1500 / speed); + this.effects_messages_toggle.manipulate("added")(750 / speed); + await this.battleview.timer.sleep(800 / speed); } /** diff --git a/src/ui/battle/BattleView.ts b/src/ui/battle/BattleView.ts index 98fc8ec..56a461c 100644 --- a/src/ui/battle/BattleView.ts +++ b/src/ui/battle/BattleView.ts @@ -21,6 +21,9 @@ module TK.SpaceTac.UI { // Interacting player player!: Player + // AI Runner + ai!: AIRunner + // Multiplayer sharing multi!: MultiBattle @@ -85,6 +88,7 @@ module TK.SpaceTac.UI { this.ship_selected = null; this.background = null; this.multi = new MultiBattle(); + this.ai = new AIRunner(); this.toggle_tactical_mode = new Toggle( () => this.arena.setTacticalMode(true), @@ -167,6 +171,7 @@ module TK.SpaceTac.UI { this.infobar.setPhase(this.battle.turncount, this.battle.ended ? BattleInfoBarPhase.END : BattleInfoBarPhase.PLANNING ); + this.startAIWork(); } }; } else { @@ -194,12 +199,14 @@ module TK.SpaceTac.UI { } this.resetPlannings(); + this.startAIWork(); } shutdown() { - super.shutdown(); - + this.ai.getResult(); this.log_processor.destroy(); + + super.shutdown(); } /** @@ -211,16 +218,45 @@ module TK.SpaceTac.UI { } this.setInteractionEnabled(false); - this.infobar.setPhase(this.battle.turncount, BattleInfoBarPhase.AI); try { - const plan = await this.actual_battle.getAIPlan(this.player, this.debug); - this.setPlayerTurnPlan(this.player, plan); + const ai = new AIRunner(); // TODO destroy on view shutdown + await ai.startWork(this.actual_battle, this.player, this.debug); + + await this.waitForAI(ai, this.player); } finally { this.setInteractionEnabled(true); this.infobar.setPhase(this.battle.turncount, BattleInfoBarPhase.PLANNING); } } + /** + * Wait for AI plan, and apply it to a player + * + * This will put the UI in thinking mode, but not restore it + */ + async waitForAI(ai: AIRunner, player: Player): Promise { + this.setInteractionEnabled(false); + this.infobar.setPhase(this.battle.turncount, BattleInfoBarPhase.AI); + + const plan = await ai.getResult(); + this.setPlayerTurnPlan(player, plan); + } + + /** + * Make the AI work on the current turn + * + * Any previous unfinished work will be interrupted. + * + * This will do nothing on headless (test) UI. + */ + startAIWork(): void { + if (this.gameui.isTesting) { + return; + } + + this.ai.startWork(this.actual_battle, this.actual_battle.fleets[1].player, this.debug); + } + /** * Set the turn plan for a specific player */ @@ -286,16 +322,18 @@ module TK.SpaceTac.UI { /** * Start the turn resolution */ - startResolution(): void { + async startResolution() { const message = "Validate your whole fleet planning, and proceed to turn resolution?"; - UIConfirmDialog.ask(this, message).then(ok => { - if (ok) { - // TODO Check we are in planning phase - // TODO Wait for AI - // TODO Merge plans - this.actual_battle.applyTurnPlan(this.turn_plannings[0].getTurnPlan()); + const ok = await UIConfirmDialog.ask(this, message); + if (ok) { + // TODO Check we are in planning phase + await this.waitForAI(this.ai, this.actual_battle.fleets[1].player); + console.log(this.turn_plannings) + const mergedplan = { + fleets: flatten(this.turn_plannings.map(planning => planning.getTurnPlan().fleets)) } - }); + this.actual_battle.applyTurnPlan(mergedplan); + } } /**