Fixed many AI and AIDuel asynchronous issues
This commit is contained in:
parent
b8cee05a39
commit
284c0cf297
|
@ -8,6 +8,12 @@
|
|||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
font-family: 'SpaceTac';
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'SpaceTac';
|
||||
src: url('assets/fonts/daggersquare.regular.otf');
|
||||
}
|
||||
|
||||
body {
|
||||
|
@ -42,6 +48,7 @@
|
|||
width: 16vw;
|
||||
text-align: center;
|
||||
font-size: 26px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
td:nth-child(1) {
|
||||
|
|
|
@ -266,14 +266,14 @@ module TS.SpaceTac {
|
|||
/**
|
||||
* Make an AI play the current ship
|
||||
*/
|
||||
playAI(ai: AbstractAI | null = null): boolean {
|
||||
playAI(ai: AbstractAI | null = null, 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();
|
||||
ai.play(debug);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
|
|
@ -66,40 +66,38 @@ module TS.SpaceTac {
|
|||
/**
|
||||
* Perform the next battle
|
||||
*/
|
||||
next() {
|
||||
async next() {
|
||||
console.log(`${this.ai1.name} vs ${this.ai2.name} ...`);
|
||||
|
||||
// Prepare battle
|
||||
let battle = Battle.newQuickRandom();
|
||||
battle.fleets.forEach((fleet, findex) => {
|
||||
fleet.ships.forEach((ship, sindex) => {
|
||||
ship.name = `F${findex + 1}S${sindex + 1} (${ship.model.name})`;
|
||||
});
|
||||
});
|
||||
|
||||
// Run battle
|
||||
while (!battle.ended && battle.turn < 100) {
|
||||
let playing = battle.playing_ship;
|
||||
|
||||
// console.debug(`Turn ${battle.turn} - Ship ${battle.play_order.indexOf(playing)} - Player ${battle.fleets.indexOf(playing.fleet)}`);
|
||||
|
||||
if (playing) {
|
||||
let ai = (playing.fleet == battle.fleets[0]) ? this.ai1 : this.ai2;
|
||||
ai.timer = Timer.synchronous;
|
||||
ai.ship = playing;
|
||||
ai.play();
|
||||
} else {
|
||||
console.error("No ship playing");
|
||||
break;
|
||||
if (this.stopped) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!battle.ended && battle.playing_ship == playing) {
|
||||
console.error("AI did not end its turn !");
|
||||
battle.advanceToNextShip();
|
||||
let playing = battle.playing_ship;
|
||||
if (playing) {
|
||||
let ai = (playing.fleet == battle.fleets[0]) ? this.ai1 : this.ai2;
|
||||
ai.ship = playing;
|
||||
await ai.play();
|
||||
}
|
||||
}
|
||||
|
||||
if (battle.ended && !battle.outcome.draw && battle.outcome.winner) {
|
||||
// Update results, and go on to next battle
|
||||
if (!battle.outcome.draw && battle.outcome.winner) {
|
||||
this.update(battle.fleets.indexOf(battle.outcome.winner));
|
||||
} else {
|
||||
this.update(-1);
|
||||
}
|
||||
if (!this.stopped) {
|
||||
this.scheduled = Timer.global.schedule(100, () => this.next());
|
||||
}
|
||||
this.scheduled = Timer.global.schedule(100, () => this.next());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -116,6 +114,9 @@ module TS.SpaceTac {
|
|||
option.textContent = ai.name;
|
||||
selects[i].appendChild(option);
|
||||
}
|
||||
ai.name += `${idx + 1}`;
|
||||
ai.timer = new Timer();
|
||||
ai.timer.schedule = (delay, callback) => Timer.global.schedule(1, callback);
|
||||
});
|
||||
|
||||
let button = element.getElementsByTagName("button").item(0);
|
||||
|
|
|
@ -1,127 +1,118 @@
|
|||
module TS.SpaceTac {
|
||||
// Base class for all Artificial Intelligence interaction
|
||||
/**
|
||||
* Base class for all Artificial Intelligence interaction
|
||||
*/
|
||||
export class AbstractAI {
|
||||
// Name of the AI
|
||||
name: string;
|
||||
name: string
|
||||
|
||||
// Current ship being played
|
||||
ship: Ship;
|
||||
|
||||
// Time at which work as started
|
||||
started: number;
|
||||
ship: Ship
|
||||
|
||||
// Random generator, if needed
|
||||
random: RandomGenerator;
|
||||
random = RandomGenerator.global
|
||||
|
||||
// Timer for scheduled calls
|
||||
timer: Timer;
|
||||
timer: Timer
|
||||
|
||||
// Queue of work items to process
|
||||
// Work items will be called successively, leaving time for other processing between them.
|
||||
// So work items should always be as short as possible.
|
||||
// When the queue is empty, the ship will end its turn.
|
||||
private workqueue: Function[];
|
||||
// Debug mode
|
||||
debug = false
|
||||
|
||||
// Time at which work as started
|
||||
private started: number
|
||||
|
||||
constructor(ship: Ship, timer = Timer.global, name?: string) {
|
||||
this.name = name || classname(this);
|
||||
this.ship = ship;
|
||||
this.workqueue = [];
|
||||
this.random = RandomGenerator.global;
|
||||
this.timer = timer;
|
||||
this.name = name || classname(this);
|
||||
}
|
||||
|
||||
toString = () => this.name;
|
||||
|
||||
// Play a ship turn
|
||||
// This will start asynchronous work. The AI will then call action methods, then advanceToNextShip to
|
||||
// indicate it has finished.
|
||||
play(): void {
|
||||
this.workqueue = [];
|
||||
/**
|
||||
* Start playing current ship's turn.
|
||||
*/
|
||||
async play(debug = false): 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`);
|
||||
} else {
|
||||
this.initWork();
|
||||
if (this.workqueue.length > 0) {
|
||||
this.processNextWorkItem();
|
||||
} else {
|
||||
this.endTurn();
|
||||
return;
|
||||
}
|
||||
|
||||
// Work loop
|
||||
this.initWork();
|
||||
let last = new Date().getTime();
|
||||
let ship = this.ship;
|
||||
while (this.doWorkUnit()) {
|
||||
if (!this.ship.playing || this.ship != ship) {
|
||||
console.error(`${this.name} switched to another ship in unit work`);
|
||||
break;
|
||||
}
|
||||
if (this.getDuration() >= 10000) {
|
||||
console.warn(`${this.name} takes too long to play, forcing turn end`);
|
||||
break;
|
||||
}
|
||||
|
||||
let t = new Date().getTime();
|
||||
if (t - last > 50) {
|
||||
await this.timer.sleep(10);
|
||||
last = t + 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a work item to the work queue
|
||||
addWorkItem(item: Function | null, delay = 100): void {
|
||||
if (this.timer.isSynchronous()) {
|
||||
if (item) {
|
||||
item();
|
||||
}
|
||||
} else {
|
||||
var wrapped = () => {
|
||||
if (item) {
|
||||
item();
|
||||
}
|
||||
this.processNextWorkItem();
|
||||
};
|
||||
this.workqueue.push(() => this.timer.schedule(delay, wrapped));
|
||||
}
|
||||
}
|
||||
|
||||
// Initially fill the work queue.
|
||||
// Subclasses MUST reimplement this and call addWorkItem to add work to do.
|
||||
protected initWork(): void {
|
||||
// Abstract method
|
||||
// End the ship turn
|
||||
this.applyAction(new EndTurnAction(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time spent thinking by the AI.
|
||||
* Make the AI play an action
|
||||
*
|
||||
* This should be the only real interaction point with battle state
|
||||
*/
|
||||
private applyAction(action: BaseAction, target: Target | null): boolean {
|
||||
return action.apply(this.ship, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the groundwork for future doWorkUnit calls
|
||||
*/
|
||||
protected initWork(): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a single unit of synchronous work
|
||||
*
|
||||
* Returns true if something was done, false if the AI should end the ship turn and stop.
|
||||
*/
|
||||
protected doWorkUnit(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time spent thinking on this turn
|
||||
*/
|
||||
protected getDuration() {
|
||||
return (new Date()).getTime() - this.started;
|
||||
}
|
||||
|
||||
// Process the next work item
|
||||
private processNextWorkItem(): void {
|
||||
if (this.workqueue.length > 0) {
|
||||
if (this.getDuration() >= 10000) {
|
||||
console.warn("AI take too long to play, forcing turn end");
|
||||
this.effectiveEndTurn();
|
||||
} else {
|
||||
// Take the first item
|
||||
var item = this.workqueue.shift();
|
||||
if (item) {
|
||||
item();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.endTurn();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Effectively end the current ship's turn
|
||||
*/
|
||||
private effectiveEndTurn() {
|
||||
if (this.workqueue.length > 0) {
|
||||
console.error(`${this.name} ends turn, but there is pending work`);
|
||||
}
|
||||
|
||||
if (this.ship.playing) {
|
||||
let battle = this.ship.getBattle();
|
||||
this.ship.endTurn();
|
||||
if (battle) {
|
||||
battle.advanceToNextShip();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when we want the AI decides to end the ship turn
|
||||
*/
|
||||
private endTurn(): void {
|
||||
// Delay, as to make the AI not too fast to play
|
||||
this.timer.schedule(2000 - this.getDuration(), () => this.effectiveEndTurn());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,16 +47,10 @@ module TS.SpaceTac {
|
|||
}
|
||||
|
||||
/**
|
||||
* Apply the maneuver in current battle
|
||||
* Returns true if another maneuver could be done next on the same ship
|
||||
*/
|
||||
apply(): void {
|
||||
if (this.simulation.success) {
|
||||
this.simulation.parts.filter(part => part.possible).forEach(part => {
|
||||
if (!part.action.apply(this.ship, part.target)) {
|
||||
console.error("AI cannot apply maneuver", this, part);
|
||||
}
|
||||
});
|
||||
}
|
||||
mayContinue(): boolean {
|
||||
return this.ship.playing && !this.isIncomplete() && !(this.action instanceof EndTurnAction);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -10,9 +10,6 @@ module TS.SpaceTac.Specs {
|
|||
super(ship, new BaseAction("nothing", "Do nothing", true), new Target(0, 0));
|
||||
this.score = score;
|
||||
}
|
||||
apply() {
|
||||
applied.push(this.score);
|
||||
}
|
||||
}
|
||||
|
||||
// producer of FixedManeuver from a list of scores
|
||||
|
@ -26,7 +23,10 @@ module TS.SpaceTac.Specs {
|
|||
|
||||
it("applies the highest evaluated maneuver", function () {
|
||||
let battle = new Battle();
|
||||
let ai = new TacticalAI(battle.fleets[0].addShip(), Timer.synchronous);
|
||||
let ship = battle.fleets[0].addShip();
|
||||
battle.playing_ship = ship;
|
||||
ship.playing = true;
|
||||
let ai = new TacticalAI(ship, Timer.synchronous);
|
||||
|
||||
spyOn(ai, "getDefaultProducers").and.returnValue([
|
||||
producer(1, -8, 4),
|
||||
|
@ -35,8 +35,8 @@ module TS.SpaceTac.Specs {
|
|||
spyOn(ai, "getDefaultEvaluators").and.returnValue([
|
||||
(maneuver: Maneuver) => (<FixedManeuver>maneuver).score
|
||||
]);
|
||||
spyOn(ai, "applyManeuver").and.callFake((maneuver: FixedManeuver) => applied.push(maneuver.score));
|
||||
|
||||
ai.ship.playing = true;
|
||||
ai.play();
|
||||
expect(applied).toEqual([7]);
|
||||
});
|
||||
|
|
|
@ -19,8 +19,6 @@ module TS.SpaceTac {
|
|||
private best: Maneuver | null
|
||||
private best_score: number
|
||||
|
||||
private last_action = new Date().getTime()
|
||||
|
||||
protected initWork(): void {
|
||||
this.best = null;
|
||||
this.best_score = -Infinity;
|
||||
|
@ -28,27 +26,13 @@ module TS.SpaceTac {
|
|||
this.producers = this.getDefaultProducers();
|
||||
this.evaluators = this.getDefaultEvaluators();
|
||||
|
||||
this.addWorkItem(() => this.unitWork());
|
||||
if (this.debug) {
|
||||
console.log("AI started", this.name, this.ship.name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a single maneuver
|
||||
*/
|
||||
evaluate(maneuver: Maneuver) {
|
||||
return sum(this.evaluators.map(evaluator => evaluator(maneuver)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Single unit of work => produce a batch of maneuvers and evaluate them
|
||||
*
|
||||
* The best produced maneuver (highest evaluation score) is kept to be played.
|
||||
* If two maneuvers have nearly the same score, the best one is randomly chosen.
|
||||
*/
|
||||
private unitWork() {
|
||||
let done = 0;
|
||||
let started = new Date().getTime();
|
||||
|
||||
while (this.producers.length > 0 && (new Date().getTime() - started < 50)) {
|
||||
protected doWorkUnit(): boolean {
|
||||
if (this.producers.length > 0 && this.getDuration() < 8000) {
|
||||
// Produce a maneuver
|
||||
let maneuver: Maneuver | null = null;
|
||||
let producer = this.producers.shift();
|
||||
|
@ -70,25 +54,39 @@ module TS.SpaceTac {
|
|||
}
|
||||
}
|
||||
|
||||
done += 1;
|
||||
}
|
||||
|
||||
// Continue or stop
|
||||
if (this.producers.length > 0 && this.getDuration() < 8000) {
|
||||
this.addWorkItem(() => this.unitWork(), 10);
|
||||
return true;
|
||||
} else if (this.best) {
|
||||
// Choose the best maneuver so far
|
||||
let best_maneuver = this.best;
|
||||
console.log("AI maneuver", best_maneuver, this.best_score);
|
||||
this.addWorkItem(() => {
|
||||
this.last_action = new Date().getTime();
|
||||
best_maneuver.apply();
|
||||
if (this.ship.playing && !best_maneuver.isIncomplete()) {
|
||||
this.initWork();
|
||||
}
|
||||
}, Math.max(0, 2000 - (new Date().getTime() - this.last_action)));
|
||||
if (this.debug) {
|
||||
console.log("AI maneuver", this.name, this.ship.name, best_maneuver, this.best_score);
|
||||
}
|
||||
|
||||
if (this.best.action instanceof EndTurnAction) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let success = this.applyManeuver(best_maneuver);
|
||||
if (success && best_maneuver.mayContinue()) {
|
||||
// Try to play another maneuver
|
||||
this.initWork();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// No maneuver produced
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a single maneuver
|
||||
*/
|
||||
evaluate(maneuver: Maneuver) {
|
||||
return sum(this.evaluators.map(evaluator => evaluator(maneuver)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default set of maneuver producers
|
||||
*/
|
||||
|
@ -107,8 +105,13 @@ module TS.SpaceTac {
|
|||
* Get the default set of maneuver evaluators
|
||||
*/
|
||||
getDefaultEvaluators() {
|
||||
let scaled = (evaluator: (...args: any[]) => number, factor: number) => (...args: any[]) => factor * evaluator(...args);
|
||||
let evaluators = [
|
||||
type EvaluatorHelper = (ship: Ship, battle: Battle, maneuver: Maneuver) => number;
|
||||
|
||||
function scaled(evaluator: EvaluatorHelper, factor: number): EvaluatorHelper {
|
||||
return (ship: Ship, battle: Battle, maneuver: Maneuver) => factor * evaluator(ship, battle, maneuver);
|
||||
}
|
||||
|
||||
let evaluators: EvaluatorHelper[] = [
|
||||
scaled(TacticalAIHelpers.evaluateTurnCost, 1),
|
||||
scaled(TacticalAIHelpers.evaluateOverheat, 10),
|
||||
scaled(TacticalAIHelpers.evaluateEnemyHealth, 500),
|
||||
|
@ -118,8 +121,8 @@ module TS.SpaceTac {
|
|||
scaled(TacticalAIHelpers.evaluateIdling, 1),
|
||||
]
|
||||
|
||||
// TODO evaluator typing is lost
|
||||
return evaluators.map(evaluator => ((maneuver: Maneuver) => evaluator(this.ship, this.ship.getBattle(), maneuver)));
|
||||
let battle = nn(this.ship.getBattle());
|
||||
return evaluators.map(evaluator => ((maneuver: Maneuver) => evaluator(this.ship, battle, maneuver)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,9 @@ module TS.SpaceTac.UI {
|
|||
// Indicator that the log is being played continuously
|
||||
private playing = false
|
||||
|
||||
// Time at which the last action was applied
|
||||
private last_action: number
|
||||
|
||||
constructor(view: BattleView) {
|
||||
this.view = view;
|
||||
this.battle = view.battle;
|
||||
|
@ -184,8 +187,18 @@ module TS.SpaceTac.UI {
|
|||
console.log("Battle event", event);
|
||||
|
||||
let durations = this.forwarding.map(subscriber => subscriber(event));
|
||||
let t = (new Date()).getTime()
|
||||
|
||||
if (event instanceof ShipChangeEvent) {
|
||||
if (event instanceof ActionAppliedEvent) {
|
||||
if (event.ship.getPlayer() != this.view.player) {
|
||||
// AI is playing, do not make it play too fast
|
||||
let since_last = t - this.last_action;
|
||||
if (since_last < 2000) {
|
||||
durations.push(2000 - since_last);
|
||||
}
|
||||
}
|
||||
this.last_action = t;
|
||||
} else if (event instanceof ShipChangeEvent) {
|
||||
durations.push(this.processShipChangeEvent(event));
|
||||
} else if (event instanceof DeathEvent) {
|
||||
durations.push(this.processDeathEvent(event));
|
||||
|
|
Loading…
Reference in a new issue