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
WinDrawWin
+
+ 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);