module TK.SpaceTac { /** * Error codes for approach simulation */ export enum ApproachSimulationError { NO_MOVE_NEEDED, NO_VECTOR_FOUND, } /** * Status for simulation */ export enum MoveFireStatus { OK, NO_ACTION, NOT_ENOUGH_POWER, INVALID_TARGET, IMPOSSIBLE_APPROACH, } /** * A single action in the sequence result from the simulator */ export type MoveFirePart = { action: BaseAction target: Target ap: number possible: boolean } /** * A simulation result */ export class MoveFireResult { // Simulation status status = MoveFireStatus.OK // Ideal successive parts to make the full move+fire parts: MoveFirePart[] = [] need_move = false can_move = false can_end_move = false total_move_ap = 0 move_location = new Target(0, 0, null) need_fire = false can_fire = false total_fire_ap = 0 fire_location = new Target(0, 0, null) }; /** * Utility to simulate a move+fire action. * * This is also a helper to bring a ship in range to fire a weapon. */ export class MoveFireSimulator { constructor( // Playing ship private ship: Ship, // Coordinates grid private grid: ArenaGrid ) { } /** * Find an engine action, to make an approach * * This will return the first available engine, in the definition order */ findEngine(): MoveAction | null { let actions = cfilter(this.ship.actions.listAll(), MoveAction); return first(actions, action => this.ship.actions.getCooldown(action).canUse()); } /** * Get an iterator for scanning a circle */ scanCircle(x: number, y: number, radius: number, nr = 6, na = 30): Iterator { // TODO If there is a grid, produce only grid coordinates let rcount = nr ? 1 / (nr - 1) : 0; return ichainit(imap(istep(0, irepeat(rcount, nr - 1)), r => { let angles = Math.max(1, Math.ceil(na * r)); return imap(istep(0, irepeat(2 * Math.PI / angles, angles - 1)), a => { return new Target(x + r * radius * Math.cos(a), y + r * radius * Math.sin(a)).snap(this.grid); }); })); } /** * Find the best approach location, to put a target in a given range. * * Return null if no approach vector was found. */ getApproach(action: MoveAction, target: Target, radius: number, margin = 0): Target | ApproachSimulationError { let dx = target.x - this.ship.arena_x; let dy = target.y - this.ship.arena_y; let distance = Math.sqrt(dx * dx + dy * dy); if (distance <= radius) { return ApproachSimulationError.NO_MOVE_NEEDED; } else { if (margin && radius > margin) { radius -= margin; } let candidates: [number, Target][] = []; iforeach(this.scanCircle(target.x, target.y, radius), candidate => { if (action.checkTarget(this.ship, candidate)) { candidates.push([candidate.getDistanceTo(this.ship.location), candidate]); } }); if (candidates.length) { return minBy(candidates, ([distance, _]) => distance)[1]; } else { return ApproachSimulationError.NO_VECTOR_FOUND; } } } /** * Simulate a given action on a given valid target. */ simulateAction(action: BaseAction, target: Target, move_margin = 0): MoveFireResult { let result = new MoveFireResult(); let ap = this.ship.getValue("power"); // Move or approach needed ? let move_target: Target | null = null; let move_action: MoveAction | null = null; result.move_location = Target.newFromShip(this.ship); if (action instanceof MoveAction) { result.need_move = true; move_target = target; move_action = action; } else { move_action = this.findEngine(); if (move_action) { let approach_radius = action.getRangeRadius(this.ship); let approach = this.getApproach(move_action, target, approach_radius, move_margin); if (approach instanceof Target) { result.need_move = true; move_target = approach; } else if (approach != ApproachSimulationError.NO_MOVE_NEEDED) { result.need_move = true; result.can_move = false; result.status = MoveFireStatus.IMPOSSIBLE_APPROACH; return result; } } } if (move_target && move_action && result.need_move) { if (arenaDistance(move_target, this.ship.location) < 1e-8) { result.need_move = false; } else { result.can_move = bool(move_action.checkTarget(this.ship, move_target)); if (!result.can_move) { result.status = MoveFireStatus.INVALID_TARGET; } } } // Check move AP if (result.need_move && move_target && move_action) { result.total_move_ap = move_action.getPowerUsage(this.ship, move_target); result.can_move = result.can_move && (ap > 0); result.can_end_move = result.can_move && (result.total_move_ap <= ap); result.move_location = move_target; // TODO Split in "this turn" part and "next turn" part if needed result.parts.push({ action: move_action, target: move_target, ap: result.total_move_ap, possible: result.can_move }); if (!result.can_end_move) { result.status = MoveFireStatus.NOT_ENOUGH_POWER; } ap -= result.total_move_ap; } // Check action AP if (!(action instanceof MoveAction)) { result.need_fire = true; result.total_fire_ap = action.getPowerUsage(this.ship, target); if (action.checkTarget(this.ship, target, result.move_location)) { if (result.total_fire_ap <= ap) { result.can_fire = true; } else { result.status = MoveFireStatus.NOT_ENOUGH_POWER; } } else { result.status = MoveFireStatus.INVALID_TARGET; } result.fire_location = target; result.parts.push({ action: action, target: target, ap: result.total_fire_ap, possible: (!result.need_move || result.can_end_move) && result.can_fire }); } if (!result.need_move && !result.need_fire) { result.status = MoveFireStatus.NO_ACTION; } return result; } /** * Apply a move-fire simulation result, and predict the diffs it will apply on a battle * * The original battle passed as parameter will be duplicated, and not altered */ getExpectedDiffs(battle: Battle, simulation: MoveFireResult): BaseBattleDiff[] { let sim_battle = duplicate(battle, TK.SpaceTac); let sim_ship = nn(sim_battle.getShip(this.ship.id)); let results: BaseBattleDiff[] = []; simulation.parts.forEach(part => { let diffs = part.action.getDiffs(sim_ship, battle, part.target); results = results.concat(diffs); sim_battle.applyDiffs(diffs); diffs = sim_battle.performChecks(); results = results.concat(diffs); }); return results; } } }