1
0
Fork 0

Fixed many AI and AIDuel asynchronous issues

This commit is contained in:
Michaël Lemaire 2017-08-23 19:59:22 +02:00
parent b8cee05a39
commit 284c0cf297
8 changed files with 178 additions and 169 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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