Get around move exlusion areas in the move-fire simulator
This commit is contained in:
parent
8c6892f5c0
commit
13291e7d5f
3
TODO
3
TODO
|
@ -20,7 +20,7 @@
|
|||
* Menu: end appear animation when a button is clicked
|
||||
* Menu: allow to delete cloud saves
|
||||
* Arena: display effects description instead of attribute changes
|
||||
* Arena: display radius for area effects (both on action hover, and while action is activated)
|
||||
* Arena: display radius for area effects (both on action hover, and while action is active)
|
||||
* Arena: add auto-move to attack
|
||||
* Arena: fix effects originating from real ship location instead of current sprite (when AI fires then moves)
|
||||
* Arena: add engine trail
|
||||
|
@ -45,7 +45,6 @@
|
|||
* Mobile: display tooltips larger and on the side of screen where the finger is not
|
||||
* Mobile: targetting in two times, using a draggable target indicator
|
||||
* AI: use a first batch of producers, and only if no "good" move has been found, go on with some infinite producers
|
||||
* AI: apply safety distances to move actions
|
||||
* AI: evaluate buffs/debuffs
|
||||
* AI: abandon fight
|
||||
* AI: add combination of random small move and actual maneuver, as producer
|
||||
|
|
|
@ -71,6 +71,64 @@ module TS.SpaceTac.Specs {
|
|||
]);
|
||||
});
|
||||
|
||||
it("scans a circle for move targets", function () {
|
||||
let simulator = new MoveFireSimulator(new Ship());
|
||||
|
||||
let result = simulator.scanCircle(50, 30, 10, 1, 1);
|
||||
expect(imaterialize(result)).toEqual([
|
||||
new Target(50, 30)
|
||||
]);
|
||||
|
||||
result = simulator.scanCircle(50, 30, 10, 2, 1);
|
||||
expect(imaterialize(result)).toEqual([
|
||||
new Target(50, 30),
|
||||
new Target(60, 30)
|
||||
]);
|
||||
|
||||
result = simulator.scanCircle(50, 30, 10, 2, 2);
|
||||
expect(imaterialize(result)).toEqual([
|
||||
new Target(50, 30),
|
||||
new Target(60, 30),
|
||||
new Target(40, 30)
|
||||
]);
|
||||
|
||||
result = simulator.scanCircle(50, 30, 10, 3, 4);
|
||||
expect(imaterialize(result)).toEqual([
|
||||
new Target(50, 30),
|
||||
new Target(55, 30),
|
||||
new Target(45, 30),
|
||||
new Target(60, 30),
|
||||
new Target(50, 40),
|
||||
new Target(40, 30),
|
||||
new Target(50, 20)
|
||||
]);
|
||||
});
|
||||
|
||||
it("accounts for exclusion areas for the approach", function () {
|
||||
let [ship, simulator, action] = simpleWeaponCase(100, 5, 1, 50);
|
||||
ship.setArenaPosition(300, 200);
|
||||
let battle = new Battle();
|
||||
battle.fleets[0].addShip(ship);
|
||||
let ship1 = battle.fleets[0].addShip();
|
||||
let moveaction = <MoveAction>nn(simulator.findBestEngine()).action;
|
||||
moveaction.safety_distance = 30;
|
||||
|
||||
expect(simulator.getApproach(moveaction, Target.newFromLocation(350, 200), 100)).toBe(ApproachSimulationError.NO_MOVE_NEEDED);
|
||||
expect(simulator.getApproach(moveaction, Target.newFromLocation(400, 200), 100)).toBe(ApproachSimulationError.NO_MOVE_NEEDED);
|
||||
expect(simulator.getApproach(moveaction, Target.newFromLocation(500, 200), 100)).toEqual(new Target(400, 200));
|
||||
|
||||
ship1.setArenaPosition(420, 200);
|
||||
|
||||
spyOn(simulator, "scanCircle").and.returnValue(iarray([
|
||||
new Target(400, 200),
|
||||
new Target(410, 200),
|
||||
new Target(410, 230),
|
||||
new Target(420, 210),
|
||||
new Target(480, 260),
|
||||
]));
|
||||
expect(simulator.getApproach(moveaction, Target.newFromLocation(500, 200), 100)).toEqual(new Target(410, 230));
|
||||
});
|
||||
|
||||
it("moves to get in range, even if not enough AP to fire", function () {
|
||||
let [ship, simulator, action] = simpleWeaponCase(8, 3, 2, 5);
|
||||
let result = simulator.simulateAction(action, new Target(ship.arena_x + 18, ship.arena_y, null));
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
module TS.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
|
||||
|
@ -57,52 +64,118 @@ module TS.SpaceTac {
|
|||
}
|
||||
|
||||
/**
|
||||
* Simulate a given action on a given valid target.
|
||||
* Check that a move action can reach a given destination
|
||||
*/
|
||||
simulateAction(action: BaseAction, target: Target, move_margin = 0): MoveFireResult {
|
||||
let result = new MoveFireResult();
|
||||
canMoveTo(action: MoveAction, target: Target): boolean {
|
||||
return action.checkLocationTarget(this.ship, target) == target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an iterator for scanning a circle
|
||||
*/
|
||||
scanCircle(x: number, y: number, radius: number, nr = 6, na = 30): Iterator<Target> {
|
||||
return ichainit(imap(istep(0, irepeat(nr ? 1 / (nr - 1) : 0, 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): 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);
|
||||
|
||||
let ap = this.ship.values.power.get();
|
||||
let action_radius = action.getRangeRadius(this.ship);
|
||||
if (distance <= radius) {
|
||||
return ApproachSimulationError.NO_MOVE_NEEDED;
|
||||
} else {
|
||||
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 (action instanceof MoveAction || distance > action_radius) {
|
||||
let move_distance = action instanceof MoveAction ? distance : (distance - action_radius + move_margin);
|
||||
if (move_distance > 0.000001) {
|
||||
result.need_move = true;
|
||||
if (candidates.length) {
|
||||
return minBy(candidates, ([distance, candidate]) => distance)[1];
|
||||
} else {
|
||||
return ApproachSimulationError.NO_VECTOR_FOUND;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let move_target = new Target(this.ship.arena_x + dx * move_distance / distance, this.ship.arena_y + dy * move_distance / distance, null);
|
||||
let engine = this.findBestEngine();
|
||||
if (engine) {
|
||||
result.total_move_ap = engine.action.getActionPointsUsage(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: engine.action, target: move_target, ap: result.total_move_ap, possible: result.can_move });
|
||||
/**
|
||||
* 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");
|
||||
|
||||
ap -= result.total_move_ap;
|
||||
distance -= move_distance;
|
||||
// Move or approach needed ?
|
||||
let move_target: Target | null = null;
|
||||
if (action instanceof MoveAction) {
|
||||
let corrected_target = action.applyExclusion(this.ship, target);
|
||||
if (corrected_target) {
|
||||
result.need_move = target.getDistanceTo(this.ship.location) > 0;
|
||||
move_target = corrected_target;
|
||||
}
|
||||
} else {
|
||||
let engine = this.findBestEngine();
|
||||
if (engine && engine.action instanceof MoveAction) {
|
||||
let approach_radius = action.getRangeRadius(this.ship);
|
||||
if (move_margin && approach_radius > move_margin) {
|
||||
approach_radius -= move_margin;
|
||||
}
|
||||
let approach = this.getApproach(engine.action, target, approach_radius);
|
||||
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 (distance <= action_radius) {
|
||||
result.success = true;
|
||||
if (!(action instanceof MoveAction)) {
|
||||
result.need_fire = true;
|
||||
result.total_fire_ap = action.getActionPointsUsage(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 });
|
||||
// Check move AP
|
||||
if (result.need_move && move_target) {
|
||||
let engine = this.findBestEngine();
|
||||
if (engine) {
|
||||
result.total_move_ap = engine.action.getActionPointsUsage(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: engine.action, target: move_target, ap: result.total_move_ap, possible: result.can_move });
|
||||
|
||||
ap -= result.total_move_ap;
|
||||
}
|
||||
} else {
|
||||
result.success = false;
|
||||
}
|
||||
|
||||
// Check action AP
|
||||
if (!(action instanceof MoveAction)) {
|
||||
result.need_fire = true;
|
||||
result.total_fire_ap = action.getActionPointsUsage(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;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,11 +54,10 @@ module TS.SpaceTac {
|
|||
return this.distance_per_power;
|
||||
}
|
||||
|
||||
checkLocationTarget(ship: Ship, target: Target): Target {
|
||||
// Apply maximal distance
|
||||
var max_distance = this.getRangeRadius(ship);
|
||||
target = target.constraintInRange(ship.arena_x, ship.arena_y, max_distance);
|
||||
|
||||
/**
|
||||
* Apply exclusion areas (neer arena borders, or other ships)
|
||||
*/
|
||||
applyExclusion(ship: Ship, target: Target): Target {
|
||||
let battle = ship.getBattle();
|
||||
if (battle) {
|
||||
// Keep out of arena borders
|
||||
|
@ -74,6 +73,16 @@ module TS.SpaceTac {
|
|||
target = target.moveOutOfCircle(s.arena_x, s.arena_y, this.safety_distance, ship.arena_x, ship.arena_y);
|
||||
});
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
checkLocationTarget(ship: Ship, target: Target): Target {
|
||||
// Apply maximal distance
|
||||
var max_distance = this.getRangeRadius(ship);
|
||||
target = target.constraintInRange(ship.arena_x, ship.arena_y, max_distance);
|
||||
|
||||
// Apply exclusion areas
|
||||
target = this.applyExclusion(ship, target);
|
||||
|
||||
return target;
|
||||
}
|
||||
|
|
|
@ -88,19 +88,27 @@ module TS.SpaceTac.Specs {
|
|||
let weapon = TestTools.addWeapon(ship, 50, 5, 100);
|
||||
let engine = TestTools.addEngine(ship, 25);
|
||||
|
||||
let maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(100, 0));
|
||||
let maneuver = new Maneuver(ship, new BaseAction("fake", "Nothing", false), new Target(0, 0), 0);
|
||||
expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(-1);
|
||||
|
||||
maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(100, 0), 0);
|
||||
expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(-Infinity);
|
||||
|
||||
TestTools.setShipAP(ship, 4);
|
||||
maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(100, 0), 0);
|
||||
expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(-Infinity);
|
||||
|
||||
TestTools.setShipAP(ship, 10);
|
||||
maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(100, 0), 0);
|
||||
expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(0.5); // 5 power remaining on 10
|
||||
|
||||
maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(110, 0));
|
||||
maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(110, 0), 0);
|
||||
expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(0.4); // 4 power remaining on 10
|
||||
|
||||
maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(140, 0));
|
||||
maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(140, 0), 0);
|
||||
expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(0.3); // 3 power remaining on 10
|
||||
|
||||
maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(310, 0));
|
||||
maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(310, 0), 0);
|
||||
expect(TacticalAIHelpers.evaluateTurnCost(ship, battle, maneuver)).toBe(-1); // can't do in one turn
|
||||
});
|
||||
|
||||
|
@ -159,7 +167,8 @@ module TS.SpaceTac.Specs {
|
|||
let weapon = TestTools.addWeapon(ship, 100, 1, 100, 10);
|
||||
|
||||
let maneuver = new Maneuver(ship, weapon.action, Target.newFromLocation(200, 0), 0.5);
|
||||
expect(maneuver.simulation.move_location).toEqual(Target.newFromLocation(100.5, 0));
|
||||
expect(maneuver.simulation.move_location.x).toBeCloseTo(100.5, 1);
|
||||
expect(maneuver.simulation.move_location.y).toBe(0);
|
||||
expect(TacticalAIHelpers.evaluateClustering(ship, battle, maneuver)).toEqual(0);
|
||||
|
||||
battle.fleets[1].addShip().setArenaPosition(battle.width, battle.height);
|
||||
|
|
|
@ -131,7 +131,9 @@ module TS.SpaceTac {
|
|||
*/
|
||||
static evaluateTurnCost(ship: Ship, battle: Battle, maneuver: Maneuver): number {
|
||||
let powerusage = maneuver.simulation.total_move_ap + maneuver.simulation.total_fire_ap;
|
||||
if (maneuver.simulation.total_fire_ap > ship.getAttribute("power_capacity")) {
|
||||
if (powerusage == 0) {
|
||||
return -1;
|
||||
} else if (maneuver.simulation.total_fire_ap > ship.getAttribute("power_capacity")) {
|
||||
return -Infinity;
|
||||
} else if (powerusage > ship.getValue("power")) {
|
||||
return -1;
|
||||
|
|
Loading…
Reference in a new issue