/// /// module TS.SpaceTac { export type TacticalProducer = Iterator; export 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). * * As much work as possible is done using iterators, without materializing every possibilities. */ export class TacticalAI extends AbstractAI { producers: TacticalProducer[] = [] evaluators: TacticalEvaluator[] = [] best: Maneuver | null = null best_score = -Infinity protected initWork(): void { if (this.producers.length == 0) { this.setupDefaultProducers(); } if (this.evaluators.length == 0) { this.setupDefaultEvaluators(); } 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 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; while (done < 1000 && this.producers.length > 0) { // Produce a maneuver let maneuver: Maneuver; let producer = this.producers.shift(); [maneuver, producer] = producer(); if (maneuver) { this.producers.push(producer); // Evaluate the maneuver let score = this.evaluate(maneuver); //console.log(maneuver, score); if ((Math.abs(score - this.best_score) < 0.0001 && this.random.bool()) || score > this.best_score) { this.best = maneuver; this.best_score = score; } } done += 1; } // Continue or stop if (this.producers.length > 0 && this.getDuration() < 3000) { this.addWorkItem(() => this.unitWork()); } else if (this.best) { this.best.apply(); } } /** * Setup the default set of maneuver producers */ private setupDefaultProducers() { let producers = [ TacticalAIHelpers.produceDirectShots, TacticalAIHelpers.produceBlastShots, TacticalAIHelpers.produceRandomMoves, ] producers.forEach(producer => this.producers.push(producer(this.ship, this.ship.getBattle()))); } /** * Setup the default set of maneuver evaluators */ private setupDefaultEvaluators() { let scaled = (evaluator: (...args) => number, factor: number) => (...args) => factor * evaluator(...args); let evaluators = [ scaled(TacticalAIHelpers.evaluateTurnCost, 1), scaled(TacticalAIHelpers.evaluateDamageToEnemy, 30), scaled(TacticalAIHelpers.evaluateClustering, 3), ] evaluators.forEach(evaluator => this.evaluators.push((maneuver: Maneuver) => evaluator(this.ship, this.ship.getBattle(), maneuver))); } } }