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 Phaser = new Proxy(function () { }, handler);
|
||||||
|
|
||||||
//var debug = console.log;
|
|
||||||
var debug = function () { };
|
|
||||||
|
|
||||||
importScripts("app.js");
|
importScripts("app.js");
|
||||||
|
TK.SpaceTac.AIRunnerInWorker.run();
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
|
@ -154,13 +154,6 @@ module TK.SpaceTac {
|
||||||
this.applyDiffs([new EndBattleDiff(winner, this.turncount)]);
|
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
|
* 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) => {
|
background: async (speed: number) => {
|
||||||
if (speed) {
|
if (speed) {
|
||||||
this.displayAttributeChanged(diff, 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) => {
|
background: async (speed: number) => {
|
||||||
if (speed) {
|
if (speed) {
|
||||||
await this.displayEffect(`${diff.theoretical} damage`, false, 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.ship.arena_y < arena.height * 0.9) ? 76 : (-66 - this.effects_messages.height)
|
||||||
);
|
);
|
||||||
|
|
||||||
this.effects_messages_toggle.manipulate("added")(1400 / speed);
|
this.effects_messages_toggle.manipulate("added")(750 / speed);
|
||||||
await this.battleview.timer.sleep(1500 / speed);
|
await this.battleview.timer.sleep(800 / speed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -21,6 +21,9 @@ module TK.SpaceTac.UI {
|
||||||
// Interacting player
|
// Interacting player
|
||||||
player!: Player
|
player!: Player
|
||||||
|
|
||||||
|
// AI Runner
|
||||||
|
ai!: AIRunner
|
||||||
|
|
||||||
// Multiplayer sharing
|
// Multiplayer sharing
|
||||||
multi!: MultiBattle
|
multi!: MultiBattle
|
||||||
|
|
||||||
|
@ -85,6 +88,7 @@ module TK.SpaceTac.UI {
|
||||||
this.ship_selected = null;
|
this.ship_selected = null;
|
||||||
this.background = null;
|
this.background = null;
|
||||||
this.multi = new MultiBattle();
|
this.multi = new MultiBattle();
|
||||||
|
this.ai = new AIRunner();
|
||||||
|
|
||||||
this.toggle_tactical_mode = new Toggle(
|
this.toggle_tactical_mode = new Toggle(
|
||||||
() => this.arena.setTacticalMode(true),
|
() => this.arena.setTacticalMode(true),
|
||||||
|
@ -167,6 +171,7 @@ module TK.SpaceTac.UI {
|
||||||
this.infobar.setPhase(this.battle.turncount,
|
this.infobar.setPhase(this.battle.turncount,
|
||||||
this.battle.ended ? BattleInfoBarPhase.END : BattleInfoBarPhase.PLANNING
|
this.battle.ended ? BattleInfoBarPhase.END : BattleInfoBarPhase.PLANNING
|
||||||
);
|
);
|
||||||
|
this.startAIWork();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
@ -194,12 +199,14 @@ module TK.SpaceTac.UI {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.resetPlannings();
|
this.resetPlannings();
|
||||||
|
this.startAIWork();
|
||||||
}
|
}
|
||||||
|
|
||||||
shutdown() {
|
shutdown() {
|
||||||
super.shutdown();
|
this.ai.getResult();
|
||||||
|
|
||||||
this.log_processor.destroy();
|
this.log_processor.destroy();
|
||||||
|
|
||||||
|
super.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -211,16 +218,45 @@ module TK.SpaceTac.UI {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setInteractionEnabled(false);
|
this.setInteractionEnabled(false);
|
||||||
this.infobar.setPhase(this.battle.turncount, BattleInfoBarPhase.AI);
|
|
||||||
try {
|
try {
|
||||||
const plan = await this.actual_battle.getAIPlan(this.player, this.debug);
|
const ai = new AIRunner(); // TODO destroy on view shutdown
|
||||||
this.setPlayerTurnPlan(this.player, plan);
|
await ai.startWork(this.actual_battle, this.player, this.debug);
|
||||||
|
|
||||||
|
await this.waitForAI(ai, this.player);
|
||||||
} finally {
|
} finally {
|
||||||
this.setInteractionEnabled(true);
|
this.setInteractionEnabled(true);
|
||||||
this.infobar.setPhase(this.battle.turncount, BattleInfoBarPhase.PLANNING);
|
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
|
* Set the turn plan for a specific player
|
||||||
*/
|
*/
|
||||||
|
@ -286,16 +322,18 @@ module TK.SpaceTac.UI {
|
||||||
/**
|
/**
|
||||||
* Start the turn resolution
|
* Start the turn resolution
|
||||||
*/
|
*/
|
||||||
startResolution(): void {
|
async startResolution() {
|
||||||
const message = "Validate your whole fleet planning, and proceed to turn resolution?";
|
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) {
|
if (ok) {
|
||||||
// TODO Check we are in planning phase
|
// TODO Check we are in planning phase
|
||||||
// TODO Wait for AI
|
await this.waitForAI(this.ai, this.actual_battle.fleets[1].player);
|
||||||
// TODO Merge plans
|
console.log(this.turn_plannings)
|
||||||
this.actual_battle.applyTurnPlan(this.turn_plannings[0].getTurnPlan());
|
const mergedplan = {
|
||||||
|
fleets: flatten(this.turn_plannings.map(planning => planning.getTurnPlan().fleets))
|
||||||
}
|
}
|
||||||
});
|
this.actual_battle.applyTurnPlan(mergedplan);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in a new issue