Process AI in a web worker if available
This commit is contained in:
parent
58334da31a
commit
7e20599fce
2
TODO.md
2
TODO.md
|
@ -86,7 +86,6 @@ Artificial Intelligence
|
||||||
* Abandon fight if the AI judges there is no hope of victory
|
* Abandon fight if the AI judges there is no hope of victory
|
||||||
* Add combination of random small move and actual maneuver, as producer
|
* Add combination of random small move and actual maneuver, as producer
|
||||||
* New duel page with producers/evaluators tweaking
|
* New duel page with producers/evaluators tweaking
|
||||||
* Work in a dedicated process (webworker)
|
|
||||||
|
|
||||||
Common UI
|
Common UI
|
||||||
---------
|
---------
|
||||||
|
@ -101,6 +100,7 @@ Common UI
|
||||||
Technical
|
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 all images in atlases, and split them by stage
|
||||||
* Pack sounds
|
* Pack sounds
|
||||||
* Add toggles for shaders, automatically disable them if too slow, and initially disable them on mobile
|
* Add toggles for shaders, automatically disable them if too slow, and initially disable them on mobile
|
||||||
|
|
23
out/aiworker.js
Normal file
23
out/aiworker.js
Normal 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);
|
||||||
|
}
|
|
@ -118,7 +118,7 @@ module TK.SpaceTac {
|
||||||
check.equals(battle.ships.list().filter(ship => ship.alive), [ship1, ship2, ship3, ship4], "alive ships");
|
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.equals(result, true, "action applied successfully");
|
||||||
check.in("after weapon", check => {
|
check.in("after weapon", check => {
|
||||||
check.same(battle.playing_ship, ship3, "playing ship");
|
check.same(battle.playing_ship, ship3, "playing ship");
|
||||||
|
|
|
@ -251,15 +251,13 @@ module TK.SpaceTac {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make an AI play the current ship
|
* 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) {
|
if (this.playing_ship && !this.ai_playing) {
|
||||||
this.ai_playing = true;
|
this.ai_playing = true;
|
||||||
if (!ai) {
|
AIWorker.process(this, debug);
|
||||||
// TODO Use an AI adapted to the fleet
|
|
||||||
ai = new TacticalAI(this.playing_ship, this.timer);
|
|
||||||
}
|
|
||||||
ai.play(debug);
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
|
@ -288,7 +286,7 @@ module TK.SpaceTac {
|
||||||
*/
|
*/
|
||||||
advanceToNextShip(): void {
|
advanceToNextShip(): void {
|
||||||
if (this.playing_ship) {
|
if (this.playing_ship) {
|
||||||
this.applyOneAction(EndTurnAction.SINGLETON);
|
this.applyOneAction(EndTurnAction.SINGLETON.id);
|
||||||
} else if (this.play_order.length) {
|
} else if (this.play_order.length) {
|
||||||
this.setPlayingShip(this.play_order[0]);
|
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
|
* 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;
|
let ship = this.playing_ship;
|
||||||
if (ship) {
|
if (ship) {
|
||||||
if (!target) {
|
let action = ship.getAction(action_id);
|
||||||
target = action.getDefaultTarget(ship);
|
if (action) {
|
||||||
}
|
if (!target) {
|
||||||
if (action.apply(this, ship, target)) {
|
target = action.getDefaultTarget(ship);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
} else {
|
||||||
|
console.error("Action not found on ship", action_id, ship);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error("Cannot apply action - ship not playing", action, this);
|
console.error("Cannot apply action - ship not playing", action_id, this);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,7 +146,7 @@ module TK.SpaceTac {
|
||||||
for (let i = 0; i < actions.length; i++) {
|
for (let i = 0; i < actions.length; i++) {
|
||||||
let [ship, action, target] = actions[i];
|
let [ship, action, target] = actions[i];
|
||||||
battle.setPlayingShip(ship);
|
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`);
|
check.equals(result, true, `action ${i + 1} successfully applied`);
|
||||||
checks[i + 1](check.sub(`after action ${i + 1} applied`));
|
checks[i + 1](check.sub(`after action ${i + 1} applied`));
|
||||||
}
|
}
|
||||||
|
|
76
src/core/ai/AIWorker.ts
Normal file
76
src/core/ai/AIWorker.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,9 @@
|
||||||
module TK.SpaceTac {
|
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
|
* Base class for all Artificial Intelligence interaction
|
||||||
*/
|
*/
|
||||||
|
@ -18,11 +23,16 @@ module TK.SpaceTac {
|
||||||
// Debug mode
|
// Debug mode
|
||||||
debug = false
|
debug = false
|
||||||
|
|
||||||
|
// Feedback to send maneuvers to
|
||||||
|
feedback: AIFeedback
|
||||||
|
|
||||||
// Time at which work as started
|
// Time at which work as started
|
||||||
private started: number
|
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.ship = ship;
|
||||||
|
this.feedback = feedback ? feedback : ((maneuver: Maneuver) => maneuver.apply(nn(this.ship.getBattle())));
|
||||||
|
this.debug = debug;
|
||||||
this.timer = timer;
|
this.timer = timer;
|
||||||
this.name = name || classname(this);
|
this.name = name || classname(this);
|
||||||
}
|
}
|
||||||
|
@ -32,9 +42,8 @@ module TK.SpaceTac {
|
||||||
/**
|
/**
|
||||||
* Start playing current ship's turn.
|
* Start playing current ship's turn.
|
||||||
*/
|
*/
|
||||||
async play(debug = false): Promise<void> {
|
async play(): Promise<void> {
|
||||||
this.started = (new Date()).getTime();
|
this.started = (new Date()).getTime();
|
||||||
this.debug = debug;
|
|
||||||
|
|
||||||
if (!this.ship.playing) {
|
if (!this.ship.playing) {
|
||||||
console.error(`${this.name} tries to play a ship out of turn`);
|
console.error(`${this.name} tries to play a ship out of turn`);
|
||||||
|
@ -63,38 +72,8 @@ module TK.SpaceTac {
|
||||||
}
|
}
|
||||||
|
|
||||||
// End the ship turn
|
// End the ship turn
|
||||||
this.applyAction(EndTurnAction.SINGLETON, Target.newFromShip(ship));
|
if (this.ship.playing) {
|
||||||
}
|
this.feedback(new Maneuver(this.ship, 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -108,5 +108,28 @@ module TK.SpaceTac {
|
||||||
|
|
||||||
return result;
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,13 @@ module TK.SpaceTac.Specs {
|
||||||
let ship = battle.fleets[0].addShip();
|
let ship = battle.fleets[0].addShip();
|
||||||
TestTools.setShipPlaying(battle, ship);
|
TestTools.setShipPlaying(battle, ship);
|
||||||
ship.playing = true;
|
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", () => [
|
check.patch(ai, "getDefaultProducers", () => [
|
||||||
producer(1, -8, 4),
|
producer(1, -8, 4),
|
||||||
|
@ -35,7 +41,6 @@ module TK.SpaceTac.Specs {
|
||||||
check.patch(ai, "getDefaultEvaluators", () => [
|
check.patch(ai, "getDefaultEvaluators", () => [
|
||||||
(maneuver: Maneuver) => (<FixedManeuver>maneuver).score
|
(maneuver: Maneuver) => (<FixedManeuver>maneuver).score
|
||||||
]);
|
]);
|
||||||
check.patch(ai, "applyManeuver", (maneuver: FixedManeuver) => applied.push(maneuver.score));
|
|
||||||
|
|
||||||
ai.play();
|
ai.play();
|
||||||
check.equals(applied, [7]);
|
check.equals(applied, [7]);
|
||||||
|
|
|
@ -66,8 +66,8 @@ module TK.SpaceTac {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let success = this.applyManeuver(best_maneuver);
|
let success = this.feedback(best_maneuver);
|
||||||
if (success && best_maneuver.mayContinue()) {
|
if (success) {
|
||||||
// Try to play another maneuver
|
// Try to play another maneuver
|
||||||
this.initWork();
|
this.initWork();
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -194,7 +194,7 @@ module TK.SpaceTac.UI {
|
||||||
if (ship) {
|
if (ship) {
|
||||||
let ship_action = first(ship.getAvailableActions(), ac => ac.is(action));
|
let ship_action = first(ship.getAvailableActions(), ac => ac.is(action));
|
||||||
if (ship_action) {
|
if (ship_action) {
|
||||||
let result = this.actual_battle.applyOneAction(action, target);
|
let result = this.actual_battle.applyOneAction(action.id, target);
|
||||||
if (result) {
|
if (result) {
|
||||||
this.setInteractionEnabled(false);
|
this.setInteractionEnabled(false);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue