Restored and improved AI runner
This commit is contained in:
parent
65f2008fd7
commit
aa640e23f2
|
@ -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();
|
||||
|
|
|
@ -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
152
src/core/ai/AIRunner.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 => {
|
||||
const ok = await UIConfirmDialog.ask(this, message);
|
||||
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());
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in a new issue