1
0
Fork 0

Started work on TacticalAI and added AI tournament

This commit is contained in:
Michaël Lemaire 2017-02-21 22:16:18 +01:00
parent 0acfcf896e
commit 80a82664e1
11 changed files with 267 additions and 72 deletions

20
out/ai.html Normal file
View 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

View file

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

View 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;
}
}
}
}

View file

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

View file

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

View file

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

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

View file

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

View file

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