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
|
||||
* 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
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");
|
||||
});
|
||||
|
||||
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");
|
||||
|
|
|
@ -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,12 +352,15 @@ 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) {
|
||||
let action = ship.getAction(action_id);
|
||||
if (action) {
|
||||
if (!target) {
|
||||
target = action.getDefaultTarget(ship);
|
||||
}
|
||||
|
||||
if (action.apply(this, ship, target)) {
|
||||
this.performChecks();
|
||||
|
||||
|
@ -368,7 +369,7 @@ module TK.SpaceTac {
|
|||
|
||||
if (ship.playing && ship.getValue("hull") <= 0) {
|
||||
// Playing ship died during its action, force a turn end
|
||||
this.applyOneAction(EndTurnAction.SINGLETON);
|
||||
this.applyOneAction(EndTurnAction.SINGLETON.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -377,7 +378,11 @@ module TK.SpaceTac {
|
|||
return false;
|
||||
}
|
||||
} else {
|
||||
console.error("Cannot apply action - ship not playing", action, this);
|
||||
console.error("Action not found on ship", action_id, ship);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
console.error("Cannot apply action - ship not playing", action_id, this);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
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 {
|
||||
/**
|
||||
* 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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue