Started work on TacticalAI and added AI tournament
This commit is contained in:
parent
0acfcf896e
commit
80a82664e1
20
out/ai.html
Normal file
20
out/ai.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>SpaceTac - AI Tournament</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script src="vendor/phaser/build/phaser.min.js"></script>
|
||||
<script src="build.js"></script>
|
||||
|
||||
<script>
|
||||
window.onload = function () {
|
||||
new TS.SpaceTac.AITournament();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1 +1 @@
|
|||
Subproject commit 915a2736524c6087854677311af34607bf3eef78
|
||||
Subproject commit 9a82049a4a898cfb3eb939156cb07f94e66cce2e
|
|
@ -16,6 +16,9 @@ module TS.SpaceTac {
|
|||
// List of ships, sorted by their initiative throw
|
||||
play_order: Ship[];
|
||||
|
||||
// Current turn
|
||||
turn: number;
|
||||
|
||||
// Current ship whose turn it is to play
|
||||
playing_ship_index: number;
|
||||
playing_ship: Ship;
|
||||
|
@ -181,6 +184,9 @@ module TS.SpaceTac {
|
|||
}
|
||||
|
||||
if (this.playing_ship) {
|
||||
if (this.playing_ship_index == 0) {
|
||||
this.turn += 1;
|
||||
}
|
||||
this.playing_ship.startTurn();
|
||||
}
|
||||
|
||||
|
@ -195,9 +201,9 @@ module TS.SpaceTac {
|
|||
playAI(ai: AbstractAI | null = null) {
|
||||
if (!ai) {
|
||||
// TODO Use an AI adapted to the fleet
|
||||
ai = new BullyAI(this.playing_ship.fleet);
|
||||
ai = new BullyAI(this.playing_ship, this.timer);
|
||||
}
|
||||
ai.playShip(this.playing_ship, this.timer);
|
||||
ai.play();
|
||||
}
|
||||
|
||||
// Start the battle
|
||||
|
@ -205,6 +211,7 @@ module TS.SpaceTac {
|
|||
// This will not add any event to the battle log
|
||||
start(): void {
|
||||
this.ended = false;
|
||||
this.turn = 0;
|
||||
this.placeShips();
|
||||
this.throwInitiative();
|
||||
this.play_order.forEach((ship: Ship) => {
|
||||
|
|
84
src/core/ai/AITournament.ts
Normal file
84
src/core/ai/AITournament.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +1,12 @@
|
|||
module TS.SpaceTac {
|
||||
// Base class for all Artificial Intelligence interaction
|
||||
export class AbstractAI {
|
||||
// The fleet controlled by this AI
|
||||
fleet: Fleet;
|
||||
// Name of the AI
|
||||
name: string;
|
||||
|
||||
// Current ship being played
|
||||
ship: Ship;
|
||||
|
||||
// Set this to false to force synchronous behavior (playShip will block until finished)
|
||||
async: boolean;
|
||||
|
||||
// Time at which work as started
|
||||
started: number;
|
||||
|
||||
|
@ -17,7 +14,7 @@ module TS.SpaceTac {
|
|||
random: RandomGenerator;
|
||||
|
||||
// Timer for scheduled calls
|
||||
timer = Timer.global;
|
||||
timer: Timer;
|
||||
|
||||
// Queue of work items to process
|
||||
// Work items will be called successively, leaving time for other processing between them.
|
||||
|
@ -25,45 +22,43 @@ module TS.SpaceTac {
|
|||
// When the queue is empty, the ship will end its turn.
|
||||
private workqueue: Function[];
|
||||
|
||||
constructor(fleet: Fleet) {
|
||||
this.fleet = fleet;
|
||||
this.async = true;
|
||||
constructor(ship: Ship, timer = Timer.global, name: string = null) {
|
||||
this.name = name || classname(this);
|
||||
this.ship = ship;
|
||||
this.workqueue = [];
|
||||
this.random = new RandomGenerator();
|
||||
this.timer = timer;
|
||||
}
|
||||
|
||||
// Play a ship turn
|
||||
// This will start asynchronous work. The AI will then call action methods, then advanceToNextShip to
|
||||
// indicate it has finished.
|
||||
playShip(ship: Ship, timer: Timer | null = null): void {
|
||||
this.ship = ship;
|
||||
play(): void {
|
||||
this.workqueue = [];
|
||||
this.started = (new Date()).getTime();
|
||||
if (timer) {
|
||||
this.timer = timer;
|
||||
}
|
||||
this.initWork();
|
||||
if (this.workqueue.length > 0) {
|
||||
this.processNextWorkItem();
|
||||
} else {
|
||||
this.endTurn();
|
||||
}
|
||||
}
|
||||
|
||||
// Add a work item to the work queue
|
||||
addWorkItem(item: Function, delay = 100): void {
|
||||
if (!this.async) {
|
||||
if (this.timer.isSynchronous()) {
|
||||
if (item) {
|
||||
item();
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
var wrapped = () => {
|
||||
if (item) {
|
||||
item();
|
||||
}
|
||||
this.processNextWorkItem();
|
||||
};
|
||||
this.workqueue.push(() => this.timer.schedule(delay, wrapped));
|
||||
}
|
||||
|
||||
var wrapped = () => {
|
||||
if (item) {
|
||||
item();
|
||||
}
|
||||
this.processNextWorkItem();
|
||||
};
|
||||
this.workqueue.push(() => this.timer.schedule(delay, wrapped));
|
||||
}
|
||||
|
||||
// Initially fill the work queue.
|
||||
|
@ -99,24 +94,22 @@ module TS.SpaceTac {
|
|||
* Effectively end the current ship's turn
|
||||
*/
|
||||
private effectiveEndTurn() {
|
||||
this.ship.endTurn();
|
||||
this.ship = null;
|
||||
this.fleet.battle.advanceToNextShip();
|
||||
if (this.ship.playing) {
|
||||
let battle = this.ship.getBattle();
|
||||
this.ship.endTurn();
|
||||
this.ship = null;
|
||||
if (battle) {
|
||||
battle.advanceToNextShip();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when we want the AI decides to end the ship turn
|
||||
*/
|
||||
private endTurn(): void {
|
||||
if (this.async) {
|
||||
var duration = this.getDuration();
|
||||
if (duration < 2000) {
|
||||
// Delay, as to make the AI not too fast to play
|
||||
this.timer.schedule(2000 - duration, () => this.effectiveEndTurn());
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.effectiveEndTurn();
|
||||
// Delay, as to make the AI not too fast to play
|
||||
this.timer.schedule(2000 - this.getDuration(), () => this.effectiveEndTurn());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,7 @@ module TS.SpaceTac.Specs {
|
|||
var random = new RandomGenerator(0, 0.5, 1);
|
||||
battle.throwInitiative(random);
|
||||
|
||||
var ai = new BullyAI(battle.fleets[0]);
|
||||
ai.ship = battle.fleets[0].ships[0];
|
||||
var ai = new BullyAI(battle.fleets[0].ships[0], Timer.synchronous);
|
||||
|
||||
var result = ai.listAllEnemies();
|
||||
expect(result).toEqual([battle.fleets[1].ships[1], battle.fleets[1].ships[0]]);
|
||||
|
@ -19,7 +18,7 @@ module TS.SpaceTac.Specs {
|
|||
it("lists weapons", function () {
|
||||
var ship = new Ship();
|
||||
|
||||
var ai = new BullyAI(ship.fleet);
|
||||
var ai = new BullyAI(ship, Timer.synchronous);
|
||||
ai.ship = ship;
|
||||
|
||||
var result = ai.listAllWeapons();
|
||||
|
@ -49,7 +48,7 @@ module TS.SpaceTac.Specs {
|
|||
ship.values.power.setMaximal(10);
|
||||
ship.values.power.set(8);
|
||||
var enemy = new Ship();
|
||||
var ai = new BullyAI(ship.fleet);
|
||||
var ai = new BullyAI(ship, Timer.synchronous);
|
||||
ai.ship = ship;
|
||||
ai.move_margin = 0;
|
||||
var weapon = new Equipment(SlotType.Weapon);
|
||||
|
@ -140,7 +139,7 @@ module TS.SpaceTac.Specs {
|
|||
battle.fleets[1].addShip(ship3);
|
||||
battle.throwInitiative(new RandomGenerator(1, 0.5, 0));
|
||||
|
||||
var ai = new BullyAI(ship1.fleet);
|
||||
var ai = new BullyAI(ship1, Timer.synchronous);
|
||||
ai.ship = ship1;
|
||||
|
||||
var result = ai.listAllManeuvers();
|
||||
|
@ -166,9 +165,7 @@ module TS.SpaceTac.Specs {
|
|||
|
||||
it("gets a fallback maneuver", function () {
|
||||
var battle = TestTools.createBattle(1, 3);
|
||||
var ai = new BullyAI(battle.fleets[0]);
|
||||
ai.async = false;
|
||||
ai.ship = battle.fleets[0].ships[0];
|
||||
var ai = new BullyAI(battle.fleets[0].ships[0], Timer.synchronous);
|
||||
|
||||
TestTools.setShipAP(ai.ship, 5);
|
||||
var engine = TestTools.addEngine(ai.ship, 100);
|
||||
|
@ -206,10 +203,8 @@ module TS.SpaceTac.Specs {
|
|||
ship2.setArenaPosition(8, 0);
|
||||
battle.fleets[1].addShip(ship2);
|
||||
|
||||
var ai = new BullyAI(ship1.fleet);
|
||||
var ai = new BullyAI(ship1, Timer.synchronous);
|
||||
ai.move_margin = 0;
|
||||
ai.async = false;
|
||||
ai.ship = ship1;
|
||||
|
||||
var engine = new Equipment(SlotType.Engine);
|
||||
engine.distance = 1;
|
||||
|
|
|
@ -23,13 +23,7 @@ module TS.SpaceTac {
|
|||
// Basic Artificial Intelligence, with a tendency to move forward and shoot the nearest enemy
|
||||
export class BullyAI extends AbstractAI {
|
||||
// Safety margin in moves to account for floating-point rounding errors
|
||||
move_margin: number;
|
||||
|
||||
constructor(fleet: Fleet) {
|
||||
super(fleet);
|
||||
|
||||
this.move_margin = 0.1;
|
||||
}
|
||||
move_margin = 0.1;
|
||||
|
||||
protected initWork(): void {
|
||||
this.addWorkItem(() => {
|
||||
|
@ -54,7 +48,7 @@ module TS.SpaceTac {
|
|||
listAllEnemies(): Ship[] {
|
||||
var result: Ship[] = [];
|
||||
|
||||
this.fleet.battle.play_order.forEach((ship: Ship) => {
|
||||
this.ship.getBattle().play_order.forEach((ship: Ship) => {
|
||||
if (ship.alive && ship.getPlayer() !== this.ship.getPlayer()) {
|
||||
result.push(ship);
|
||||
}
|
||||
|
@ -159,7 +153,7 @@ module TS.SpaceTac {
|
|||
if (distance > safety_distance) { // Don't move too close
|
||||
target = target.constraintInRange(this.ship.arena_x, this.ship.arena_y,
|
||||
(distance - safety_distance) * APPROACH_FACTOR);
|
||||
target = engine.action.checkLocationTarget(this.fleet.battle, this.ship, target);
|
||||
target = engine.action.checkLocationTarget(this.ship.getBattle(), this.ship, target);
|
||||
return new BullyManeuver(new Maneuver(this.ship, engine, target));
|
||||
} else {
|
||||
return null;
|
||||
|
@ -183,16 +177,18 @@ module TS.SpaceTac {
|
|||
|
||||
// Effectively apply the chosen maneuver
|
||||
applyManeuver(maneuver: BullyManeuver): void {
|
||||
if (maneuver.move) {
|
||||
this.addWorkItem(() => {
|
||||
maneuver.move.apply();
|
||||
}, 500);
|
||||
}
|
||||
if (maneuver) {
|
||||
if (maneuver.move) {
|
||||
this.addWorkItem(() => {
|
||||
maneuver.move.apply();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
if (maneuver.fire) {
|
||||
this.addWorkItem(() => {
|
||||
maneuver.fire.apply();
|
||||
}, 1500);
|
||||
if (maneuver.fire) {
|
||||
this.addWorkItem(() => {
|
||||
maneuver.fire.apply();
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
this.addWorkItem(null, 1500);
|
||||
|
|
34
src/core/ai/TacticalAI.spec.ts
Normal file
34
src/core/ai/TacticalAI.spec.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/// <reference path="Maneuver.ts" />
|
||||
|
||||
module TS.SpaceTac.Specs {
|
||||
describe("TacticalAI", function () {
|
||||
class FixedManeuver extends Maneuver {
|
||||
score: number;
|
||||
constructor(score: number) {
|
||||
super(new Ship(), new Equipment(), new Target(0, 0));
|
||||
this.score = score;
|
||||
}
|
||||
apply() {
|
||||
applied.push(this.score);
|
||||
}
|
||||
}
|
||||
|
||||
// producer of FixedManeuver from a list of scores
|
||||
let producer = (...scores: number[]) => iterator(scores.map(score => new FixedManeuver(score)));
|
||||
let applied = [];
|
||||
|
||||
beforeEach(function () {
|
||||
applied = [];
|
||||
});
|
||||
|
||||
it("applies the highest evaluated maneuver", function () {
|
||||
let ai = new TacticalAI(new Ship(), Timer.synchronous);
|
||||
ai.evaluators.push(maneuver => maneuver.score);
|
||||
ai.producers.push(producer(1, -8, 4));
|
||||
ai.producers.push(producer(3, 7, 0, 6, 1));
|
||||
|
||||
ai.play();
|
||||
expect(applied).toEqual([7]);
|
||||
});
|
||||
});
|
||||
}
|
63
src/core/ai/TacticalAI.ts
Normal file
63
src/core/ai/TacticalAI.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
/// <reference path="AbstractAI.ts"/>
|
||||
/// <reference path="Maneuver.ts"/>
|
||||
module TS.SpaceTac {
|
||||
|
||||
type TacticalProducer = () => Maneuver | null;
|
||||
type TacticalEvaluator = (Maneuver) => number;
|
||||
|
||||
/**
|
||||
* AI that applies a set of tactical rules
|
||||
*
|
||||
* It uses a set of producers (to propose new maneuvers), and evaluators (to choose the best maneuver).
|
||||
*/
|
||||
export class TacticalAI extends AbstractAI {
|
||||
producers: TacticalProducer[] = []
|
||||
evaluators: TacticalEvaluator[] = []
|
||||
|
||||
best: Maneuver | null = null;
|
||||
best_score = -Infinity;
|
||||
|
||||
protected initWork(): void {
|
||||
this.addWorkItem(() => this.unitWork());
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a single maneuver
|
||||
*/
|
||||
evaluate(maneuver: Maneuver) {
|
||||
return sum(this.evaluators.map(evaluator => evaluator(maneuver)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Single unit of work => produce a single maneuver and evaluate it
|
||||
*/
|
||||
private unitWork() {
|
||||
if (this.producers.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Produce a maneuver
|
||||
let producer = this.producers.shift();
|
||||
let maneuver = producer();
|
||||
|
||||
if (maneuver) {
|
||||
this.producers.push(producer);
|
||||
|
||||
// Evaluate the maneuver
|
||||
let score = this.evaluate(maneuver);
|
||||
if (score > this.best_score) {
|
||||
this.best = maneuver;
|
||||
this.best_score = score;
|
||||
}
|
||||
}
|
||||
|
||||
// Continue or stop ?
|
||||
if (this.producers.length > 0) {
|
||||
this.addWorkItem(() => this.unitWork());
|
||||
} else if (this.best) {
|
||||
// TODO Also apply after a certain time of not finding better
|
||||
this.best.apply();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -31,10 +31,7 @@ module TS.SpaceTac.UI {
|
|||
|
||||
init(...args: any[]) {
|
||||
this.gameui = <MainUI>this.game;
|
||||
this.timer = new Timer();
|
||||
if (this.gameui.headless) {
|
||||
this.timer.makeSynchronous();
|
||||
}
|
||||
this.timer = new Timer(this.gameui.headless);
|
||||
}
|
||||
|
||||
create() {
|
||||
|
|
|
@ -91,6 +91,12 @@ module TS.SpaceTac.UI {
|
|||
this.inputs.bindCheat(Phaser.Keyboard.W, "Win current battle", () => {
|
||||
this.battle.endBattle(this.player.fleet);
|
||||
});
|
||||
this.inputs.bindCheat(Phaser.Keyboard.A, "Use AI to play", () => {
|
||||
if (this.interacting) {
|
||||
this.setInteractionEnabled(false);
|
||||
this.battle.playAI(new TacticalAI(this.battle.playing_ship));
|
||||
}
|
||||
});
|
||||
|
||||
// Start processing the log
|
||||
this.log_processor.start();
|
||||
|
|
Loading…
Reference in a new issue