1
0
Fork 0

Restored and improved AI runner

This commit is contained in:
Michaël Lemaire 2019-05-26 23:41:12 +02:00
parent 65f2008fd7
commit aa640e23f2
6 changed files with 208 additions and 121 deletions

View file

@ -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();

View file

@ -154,13 +154,6 @@ module TK.SpaceTac {
this.applyDiffs([new EndBattleDiff(winner, this.turncount)]);
}
/**
* Get AI plan
*/
getAIPlan(player: Player, debug = false): Promise<TurnPlan> {
return AIWorker.process(this, player, debug);
}
/**
* Start the battle
*

152
src/core/ai/AIRunner.ts Normal file
View file

@ -0,0 +1,152 @@
module TK.SpaceTac {
/**
* Interface for AI runners
*/
export interface AbstractAIRunner {
startWork(battle: Battle, player: Player, debug: boolean): Promise<void>
getResult(): Promise<TurnPlan>
}
/**
* 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<void> {
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<TurnPlan> {
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<any> {
const result = new Promise<TurnPlan>((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<void> {
return this.call("startWork", battle, player, debug);
}
async getResult(): Promise<TurnPlan> {
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 = (<unknown>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<any> {
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<void> {
return this.delegate.startWork(battle, player, debug);
}
getResult(): Promise<TurnPlan> {
return this.delegate.getResult();
}
}
}

View file

@ -1,84 +0,0 @@
module TK.SpaceTac {
/**
* Initialize the background worker, if possible
*/
function initializeWorker(): Worker | null {
if (typeof window != "undefined" && (<any>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<TurnPlan> {
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<TurnPlan> {
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<TurnPlan> {
let serializer = new Serializer(TK.SpaceTac);
let promise: Promise<TurnPlan> = 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<TurnPlan> {
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;
}
}
}

View file

@ -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);
}
/**

View file

@ -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<void> {
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);
}
}
/**