1
0
Fork 0
spacetac/src/core/MoveFireSimulator.ts

212 lines
8.2 KiB
TypeScript

module TK.SpaceTac {
/**
* Error codes for approach simulation
*/
export enum ApproachSimulationError {
NO_MOVE_NEEDED,
NO_VECTOR_FOUND,
}
/**
* 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 success, false only if no route can be found
success = false
// Ideal successive parts to make the full move+fire
parts: MoveFirePart[] = []
// Simulation complete (both move and fire are possible)
complete = false
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 {
ship: Ship;
constructor(ship: Ship) {
this.ship = ship;
}
/**
* 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());
}
/**
* Check that a move action can reach a given destination
*/
canMoveTo(action: MoveAction, target: Target): boolean {
let checked = action.checkLocationTarget(this.ship, target);
return checked != null && checked.x == target.x && checked.y == target.y;
}
/**
* Get an iterator for scanning a circle
*/
scanCircle(x: number, y: number, radius: number, nr = 6, na = 30): Iterable<Target> {
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))
});
}));
}
/**
* 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 factor = (distance - radius) / distance;
let candidate = new Target(this.ship.arena_x + dx * factor, this.ship.arena_y + dy * factor);
if (this.canMoveTo(action, candidate)) {
return candidate;
} else {
let candidates: [number, Target][] = [];
iforeach(this.scanCircle(target.x, target.y, radius), candidate => {
if (this.canMoveTo(action, candidate)) {
candidates.push([candidate.getDistanceTo(this.ship.location), candidate]);
}
});
if (candidates.length) {
return minBy(candidates, ([distance, candidate]) => 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) {
let corrected_target = action.applyReachableRange(this.ship, target, move_margin);
corrected_target = action.applyExclusion(this.ship, corrected_target);
if (corrected_target) {
result.need_move = target.getDistanceTo(this.ship.location) > 0;
move_target = corrected_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.success = false;
return result;
}
}
}
if (move_target && arenaDistance(move_target, this.ship.location) < 0.000001) {
result.need_move = false;
}
// 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 = ap > 0;
result.can_end_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 });
ap -= result.total_move_ap;
}
// Check action AP
if (action instanceof MoveAction) {
result.success = result.need_move && result.can_move;
} else {
result.need_fire = true;
result.total_fire_ap = action.getPowerUsage(this.ship, target);
result.can_fire = result.total_fire_ap <= ap;
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 });
result.success = true;
}
result.complete = (!result.need_move || result.can_end_move) && (!result.need_fire || result.can_fire);
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;
}
}
}