diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 072d75a..129ccdd 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -11,14 +11,14 @@
"taskName": "build",
"isBuildCommand": true,
"isTestCommand": false,
- "showOutput": "always",
+ "showOutput": "never",
"problemMatcher": "$tsc"
},
{
"taskName": "test",
"isBuildCommand": false,
"isTestCommand": true,
- "showOutput": "always",
+ "showOutput": "silent",
"problemMatcher": "$tsc"
}
]
diff --git a/out/ai.html b/out/ai.html
index 8e78a09..a503be8 100644
--- a/out/ai.html
+++ b/out/ai.html
@@ -3,16 +3,93 @@
- SpaceTac - AI Tournament
+ SpaceTac - AI Duel
+
+
+
+
SpaceTac - AI Duel
+
+
+
+ |
+ versus |
+ |
+
+
+ |
+
+
+ Win |
+ Draw |
+ Win |
+
+
+ |
+ |
+ |
+
+
+
+
diff --git a/package.json b/package.json
index 8f8a37c..f11ce9d 100644
--- a/package.json
+++ b/package.json
@@ -28,7 +28,7 @@
"karma-phantomjs-launcher": "~1.0",
"remap-istanbul": "~0.8",
"live-server": "~1.2",
- "typescript": "~2.1",
+ "typescript": "~2.2",
"typings": "~1.4"
}
}
\ No newline at end of file
diff --git a/src/common b/src/common
index d844179..ce9feb3 160000
--- a/src/common
+++ b/src/common
@@ -1 +1 @@
-Subproject commit d84417976adb78147656616fccd7cefc5eca21bb
+Subproject commit ce9feb35874b0aa2686c80f1b7f56447044b9d74
diff --git a/src/core/ai/AIDuel.ts b/src/core/ai/AIDuel.ts
new file mode 100644
index 0000000..4046ecb
--- /dev/null
+++ b/src/core/ai/AIDuel.ts
@@ -0,0 +1,133 @@
+module TS.SpaceTac {
+ /**
+ * Duel between two AIs, over multiple battles
+ */
+ export class AIDuel {
+ static current: AIDuel | null = null
+
+ ai1: AbstractAI
+ ai2: AbstractAI
+ win1 = 0
+ win2 = 0
+ draw = 0
+ scheduled = null
+ stopped = false
+ onupdate: Function | null = null
+
+ constructor(ai1: AbstractAI, ai2: AbstractAI) {
+ this.ai1 = ai1;
+ this.ai2 = ai2;
+ }
+
+ /**
+ * Start the duel
+ */
+ start(onupdate: Function | null = null) {
+ if (!this.scheduled) {
+ this.stopped = false;
+ this.scheduled = Timer.global.schedule(100, () => this.next());
+ this.onupdate = onupdate;
+ }
+ }
+
+ /**
+ * Stop the duel
+ */
+ stop() {
+ this.stopped = true;
+ if (this.scheduled) {
+ Timer.global.cancel(this.scheduled);
+ this.scheduled = null;
+ }
+ }
+
+ /**
+ * Update the result of a single battle
+ */
+ update(winner: AbstractAI | null) {
+ if (winner) {
+ if (winner == this.ai1) {
+ this.win1 += 1;
+ } else {
+ this.win2 += 1;
+ }
+ console.log(` => ${winner.name} wins`);
+ } else {
+ this.draw += 1;
+ console.log(" => draw");
+ }
+
+ if (this.onupdate) {
+ this.onupdate();
+ }
+ }
+
+ /**
+ * Perform the next battle
+ */
+ next() {
+ console.log(`${this.ai1.name} vs ${this.ai2.name} ...`);
+
+ let battle = Battle.newQuickRandom();
+ let playing = battle.playing_ship;
+
+ while (!battle.ended && battle.turn < 100) {
+ //console.debug(`Turn ${battle.turn} - Ship ${battle.play_order.indexOf(playing)}`);
+ let ai = (playing.fleet == battle.fleets[0]) ? this.ai1 : this.ai2;
+ ai.timer = Timer.synchronous;
+ ai.ship = playing;
+ ai.play();
+
+ if (!battle.ended && battle.playing_ship == playing) {
+ console.error(`${ai.name} did not end its turn !`);
+ battle.advanceToNextShip();
+ }
+ playing = battle.playing_ship;
+ }
+
+ if (battle.ended && !battle.outcome.draw) {
+ this.update(battle.outcome.winner == battle.fleets[0] ? this.ai1 : this.ai2);
+ } else {
+ this.update(null);
+ }
+ if (!this.stopped) {
+ this.scheduled = Timer.global.schedule(100, () => this.next());
+ }
+ }
+
+ /**
+ * Setup the duel HTML page
+ */
+ static setup(element: HTMLElement) {
+ let ais = [new BullyAI(null), new TacticalAI(null), new AbstractAI(null)];
+ ais.forEach((ai, idx) => {
+ let selects = element.getElementsByTagName("select");
+ for (let i = 0; i < selects.length; i++) {
+ let option = document.createElement("option");
+ option.setAttribute("value", idx.toString());
+ option.textContent = ai.name;
+ selects[i].appendChild(option);
+ }
+ });
+
+ let button = element.getElementsByTagName("button").item(0);
+ button.onclick = () => {
+ if (AIDuel.current) {
+ AIDuel.current.stop();
+ AIDuel.current = null;
+ button.textContent = "Start !";
+ } else {
+ let ai1 = parseInt(element.getElementsByTagName("select").item(0).value);
+ let ai2 = parseInt(element.getElementsByTagName("select").item(1).value);
+ AIDuel.current = new AIDuel(ais[ai1], ais[ai2]);
+ AIDuel.current.start(() => {
+ element.getElementsByClassName("win1").item(0).textContent = AIDuel.current.win1.toString();
+ element.getElementsByClassName("win2").item(0).textContent = AIDuel.current.win2.toString();
+ element.getElementsByClassName("draw").item(0).textContent = AIDuel.current.draw.toString();
+ });
+ button.textContent = "Stop !";
+ }
+ }
+ }
+ }
+}
diff --git a/src/core/ai/AITournament.ts b/src/core/ai/AITournament.ts
deleted file mode 100644
index 4bc2419..0000000
--- a/src/core/ai/AITournament.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-module TS.SpaceTac {
- /**
- * Tournament to test AIs against each other, over a lot of battles
- */
- export class AITournament {
- duels: [AbstractAI, number, AbstractAI, number][] = [];
-
- constructor() {
- this.addDuel(new AbstractAI(null), new BullyAI(null));
- this.addDuel(new AbstractAI(null), new TacticalAI(null));
- this.addDuel(new BullyAI(null), new TacticalAI(null));
-
- this.start();
- }
-
- addDuel(ai1: AbstractAI, ai2: AbstractAI) {
- ai1.timer = Timer.synchronous;
- ai2.timer = Timer.synchronous;
- this.duels.push([ai1, 0, ai2, 0]);
- }
-
- start(rounds = 100) {
- if (this.duels.length == 0) {
- console.error("No duel to perform");
- return;
- }
-
- while (rounds--) {
- this.duels.forEach(duel => {
- console.log(`${duel[0].name} vs ${duel[2].name}`);
-
- let winner = this.doOneBattle(duel[0], duel[2]);
-
- if (winner) {
- if (winner == duel[0]) {
- duel[1] += 1;
- } else {
- duel[3] += 1;
- }
- console.log(` => ${winner.name} wins`);
- } else {
- console.log(" => draw");
- }
- });
- }
-
- console.log("--------------------------------------------------------");
- console.log("Final result :");
- this.duels.forEach(duel => {
- let message = `${duel[0].name} ${duel[1]} - ${duel[2].name} ${duel[3]}`
- console.log(message);
- if (typeof document != "undefined") {
- let line = document.createElement("div");
- line.textContent = message;
- document.body.appendChild(line);
- }
- });
- }
-
- doOneBattle(ai1: AbstractAI, ai2: AbstractAI): AbstractAI | null {
- let battle = Battle.newQuickRandom();
- let playing = battle.playing_ship;
- while (!battle.ended && battle.turn < 100) {
- //console.debug(`Turn ${battle.turn} - Ship ${battle.play_order.indexOf(playing)}`);
- let ai = (playing.fleet == battle.fleets[0]) ? ai1 : ai2;
-
- ai.ship = playing;
- ai.play();
-
- if (!battle.ended && battle.playing_ship == playing) {
- console.error(`${ai.name} did not end its turn !`);
- battle.advanceToNextShip();
- }
- playing = battle.playing_ship;
- }
-
- if (battle.ended && !battle.outcome.draw) {
- return (battle.outcome.winner == battle.fleets[0]) ? ai1 : ai2;
- } else {
- return null;
- }
- }
- }
-}
diff --git a/src/core/ai/AbstractAI.ts b/src/core/ai/AbstractAI.ts
index 14a3c75..4a5ffc1 100644
--- a/src/core/ai/AbstractAI.ts
+++ b/src/core/ai/AbstractAI.ts
@@ -30,17 +30,24 @@ module TS.SpaceTac {
this.timer = timer;
}
+ 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 = [];
this.started = (new Date()).getTime();
- this.initWork();
- if (this.workqueue.length > 0) {
- this.processNextWorkItem();
+
+ if (!this.ship.playing) {
+ console.error(`${this.name} tries to play a ship out of turn`);
} else {
- this.endTurn();
+ this.initWork();
+ if (this.workqueue.length > 0) {
+ this.processNextWorkItem();
+ } else {
+ this.endTurn();
+ }
}
}
diff --git a/src/core/ai/TacticalAI.spec.ts b/src/core/ai/TacticalAI.spec.ts
index 8cfcfbc..7c281a7 100644
--- a/src/core/ai/TacticalAI.spec.ts
+++ b/src/core/ai/TacticalAI.spec.ts
@@ -14,7 +14,7 @@ module TS.SpaceTac.Specs {
}
// producer of FixedManeuver from a list of scores
- let producer = (...scores: number[]) => iarray(scores.map(score => new FixedManeuver(score)));
+ let producer = (...scores: number[]) => imap(iarray(scores), score => new FixedManeuver(score));
let applied = [];
beforeEach(function () {
@@ -27,11 +27,12 @@ module TS.SpaceTac.Specs {
ai.producers.push(producer(1, -8, 4));
ai.producers.push(producer(3, 7, 0, 6, 1));
+ ai.ship.playing = true;
ai.play();
expect(applied).toEqual([7]);
});
- it("produces direct weapon hits", function () {
+ it("produces direct weapon shots", function () {
let battle = new Battle();
let ship0a = battle.fleets[0].addShip(new Ship(null, "0A"));
let ship0b = battle.fleets[0].addShip(new Ship(null, "0B"));
diff --git a/src/core/ai/TacticalAI.ts b/src/core/ai/TacticalAI.ts
index 2e2e251..b8778c4 100644
--- a/src/core/ai/TacticalAI.ts
+++ b/src/core/ai/TacticalAI.ts
@@ -2,7 +2,7 @@
///
module TS.SpaceTac {
- type TacticalProducer = () => Maneuver | null;
+ type TacticalProducer = Iterator;
type TacticalEvaluator = (Maneuver) => number;
/**
@@ -47,8 +47,9 @@ module TS.SpaceTac {
}
// Produce a maneuver
+ let maneuver: Maneuver;
let producer = this.producers.shift();
- let maneuver = producer();
+ [maneuver, producer] = producer();
if (maneuver) {
this.producers.push(producer);