1
0
Fork 0

Process AI in a web worker if available

This commit is contained in:
Michaël Lemaire 2017-12-21 20:04:54 +01:00
parent 58334da31a
commit 7e20599fce
11 changed files with 177 additions and 66 deletions

View File

@ -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

23
out/aiworker.js Normal file
View File

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

View File

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

View File

@ -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;
}
}

View File

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

76
src/core/ai/AIWorker.ts Normal file
View File

@ -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<void> {
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<void> {
if ((<any>window).Worker) {
await this.processInWorker(feedback);
} else {
await this.processHere(feedback);
}
}
/**
* Process AI in a webworker
*/
async processInWorker(feedback: AIFeedback): Promise<void> {
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<void> {
let ai = new TacticalAI(this.ship, feedback, this.debug);
await ai.play();
}
}
}

View File

@ -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<void> {
async play(): Promise<void> {
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)));
}
}

View File

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

View File

@ -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) => (<FixedManeuver>maneuver).score
]);
check.patch(ai, "applyManeuver", (maneuver: FixedManeuver) => applied.push(maneuver.score));
ai.play();
check.equals(applied, [7]);

View File

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

View File

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