1
0
Fork 0
This commit is contained in:
Michaël Lemaire 2018-07-17 16:17:46 +02:00
parent 416f17d7ea
commit 9b051c3ef8
18 changed files with 157 additions and 48 deletions

@ -1 +1 @@
Subproject commit 1425cb08935dd996a4c7a644ab793ff3b8355c9b
Subproject commit 8a11307e3fb1b2c562674a475fa50b8511592bfe

View File

@ -4,7 +4,7 @@ module TK.SpaceTac.Specs {
check.equals(got.y, expected_y, `y differs (${got.x},${got.y}) (${expected_x},${expected_y})`);
}
testing("ArenaGrid", test =>  {
testing("ArenaGrid", test => {
test.case("checks if a location is in range of another", check => {
let grid = new ArenaGrid();
@ -21,12 +21,41 @@ module TK.SpaceTac.Specs {
check.equals(grid.inRange({ x: 0, y: 0 }, { x: 0.99999999999999, y: 0 }, 1), true, "0.99999999999999");
check.equals(grid.inRange({ x: 0, y: 0 }, { x: 1.00000000000001, y: 0 }, 1), true, "1.00000000000001");
check.equals(grid.inRange({ x: 0, y: 0 }, { x: 1.000001, y: 0 }, 1), false, "1.000001");
})
});
});
testing("PixelArenaGrid", test => {
test.case("moves to cardinal points", check => {
let grid = new PixelArenaGrid(undefined, 3);
checkLocation(check, grid.down({ x: 0, y: 0 }), 0, 3);
checkLocation(check, grid.down({ x: 7, y: 5 }), 7, 8);
checkLocation(check, grid.up({ x: 7, y: 5 }), 7, 2);
checkLocation(check, grid.left({ x: 6, y: 2 }), 3, 2);
checkLocation(check, grid.right({ x: 3, y: 1 }), 6, 1);
});
test.case("iterates around a location", check => {
let grid = new PixelArenaGrid(undefined, 5);
let result = imaterialize(grid.iterate({ x: 5, y: 5 }, { xmin: 0, xmax: 15, ymin: 0, ymax: 10 }));
check.equals(result.length, 12);
checkLocation(check, result[0], 5, 5);
checkLocation(check, result[1], 10, 5);
checkLocation(check, result[2], 15, 5);
checkLocation(check, result[3], 0, 5);
checkLocation(check, result[4], 5, 10);
checkLocation(check, result[5], 10, 10);
checkLocation(check, result[6], 15, 10);
checkLocation(check, result[7], 0, 10);
checkLocation(check, result[8], 5, 0);
checkLocation(check, result[9], 10, 0);
checkLocation(check, result[10], 15, 0);
checkLocation(check, result[11], 0, 0);
});
});
testing("HexagonalArenaGrid", test => {
test.case("checks coordinates", check => {
let grid = new HexagonalArenaGrid(5, 1);
let grid = new HexagonalArenaGrid(undefined, 5, 1);
check.equals(grid.check({ x: 0, y: 0 }), true, "0,0");
check.equals(grid.check({ x: 1, y: 0 }), false, "1,0");
check.equals(grid.check({ x: 5, y: 0 }), true, "5,0");
@ -38,7 +67,7 @@ module TK.SpaceTac.Specs {
});
test.case("snaps coordinates to the nearest grid point, on a biased grid", check => {
let grid = new HexagonalArenaGrid(4, 0.75);
let grid = new HexagonalArenaGrid(undefined, 4, 0.75);
checkLocation(check, grid.snap({ x: 0, y: 0 }), 0, 0);
checkLocation(check, grid.snap({ x: 1, y: 0 }), 0, 0);
checkLocation(check, grid.snap({ x: 1.9, y: 0 }), 0, 0);
@ -52,10 +81,23 @@ module TK.SpaceTac.Specs {
});
test.case("snaps coordinates to the nearest grid point, on a regular grid", check => {
let grid = new HexagonalArenaGrid(10);
let grid = new HexagonalArenaGrid(undefined, 10);
checkLocation(check, grid.snap({ x: 0, y: 0 }), 0, 0);
checkLocation(check, grid.snap({ x: 8, y: 0 }), 10, 0);
checkLocation(check, grid.snap({ x: 1, y: 6 }), 5, 10 * Math.sqrt(0.75));
});
test.case("iterates around a location", check => {
let grid = new HexagonalArenaGrid({ xmin: -10, xmax: 10, ymin: -10, ymax: 10 }, 8, 1);
let result = imaterialize(grid.iterate({ x: 0, y: 0 }));
check.equals(result.length, 7);
checkLocation(check, result[0], 0, 0);
checkLocation(check, result[1], 8, 0);
checkLocation(check, result[2], -8, 0);
checkLocation(check, result[3], 4, 8);
checkLocation(check, result[4], -4, 8);
checkLocation(check, result[5], 4, -8);
checkLocation(check, result[6], -4, -8);
});
});
}

View File

@ -5,8 +5,12 @@ module TK.SpaceTac {
* The grid is used to snap arena coordinates on grid vertices, for ships and targets
*
* The default implementation does not enforce any grid or unit (leaves coordinates as they are)
* It only applies optional boundaries (which does not impact *snap*, but *check* and *iterate*)
*/
export class ArenaGrid {
constructor(private bounds?: ArenaBounds) {
}
/**
* Get the base unit of measurement between two points
*/
@ -15,10 +19,16 @@ module TK.SpaceTac {
}
/**
* Check that an arena location is on a grid vertex
* Check that an arena location is on a grid vertex, and inside bounds
*/
check(loc: IArenaLocation): boolean {
return arenaDistance(loc, this.snap(loc)) < 1e-8;
check(loc: IArenaLocation, bounds = this.bounds): boolean {
if (arenaDistance(loc, this.snap(loc)) > 1e-8) {
return false;
} else if (bounds) {
return (loc.x - bounds.xmin) > -1e-8 && (loc.x - bounds.xmax) < 1e-8 && (loc.y - bounds.ymin) > -1e-8 && (loc.y - bounds.ymax) < 1e-8;
} else {
return true;
}
}
/**
@ -43,16 +53,65 @@ module TK.SpaceTac {
inRange(loc1: IArenaLocation, loc2: IArenaLocation, range: number): boolean {
return this.measure(loc1, loc2) - range < 1e-8;
}
/**
* Get the coordinate "up" from a given one (at a unit distance)
*/
up(loc: IArenaLocation): IArenaLocation {
return this.snap({ x: loc.x, y: loc.y - this.getUnit() });
}
/**
* Get the coordinate "down" from a given one (at a unit distance)
*/
down(loc: IArenaLocation): IArenaLocation {
return this.snap({ x: loc.x, y: loc.y + this.getUnit() });
}
/**
* Get the coordinate "left" of a given one (at a unit distance)
*/
left(loc: IArenaLocation): IArenaLocation {
return this.snap({ x: loc.x - this.getUnit(), y: loc.y });
}
/**
* Get the coordinate "right" of a given one (at a unit distance)
*/
right(loc: IArenaLocation): IArenaLocation {
return this.snap({ x: loc.x + this.getUnit(), y: loc.y });
}
/**
* Produce all valid coordinates on the grid inside a rectangular area, starting from a given point
*/
iterate(from: IArenaLocation, bounds = nn(this.bounds)): Iterator<IArenaLocation> {
from = this.snap(from);
let row = (rfrom: IArenaLocation): Iterator<IArenaLocation> => {
return ichain(
irecur(rfrom, loc => loc.x <= bounds.xmax ? this.right(loc) : null),
irecur(this.left(rfrom), loc => loc.x >= bounds.xmin ? this.left(loc) : null),
);
};
let result = ichain(
ichainit(imap(irecur(from, loc => loc.y <= bounds.ymax ? this.down(loc) : null), row)),
ichainit(imap(irecur(this.up(from), loc => loc.y >= bounds.ymin ? this.up(loc) : null), row)),
);
return ifilter(result, loc => this.check(loc, this.bounds));
}
}
/**
* Pixel unbounded grid
* Pixel grid
*
* This will only round the coordinates to the pixels, with an optional unit for distance measurements
*/
export class PixelGrid extends ArenaGrid {
constructor(protected readonly unit = 1) {
super();
export class PixelArenaGrid extends ArenaGrid {
constructor(bounds?: ArenaBounds, protected readonly unit = 1) {
super(bounds);
}
getUnit(): number {
@ -78,15 +137,15 @@ module TK.SpaceTac {
}
/**
* Hexagonal unbounded arena grid
* Hexagonal arena grid
*
* This grid is composed of regular hexagons where all vertices are at a same distance "unit" of the hexagon center
*/
export class HexagonalArenaGrid extends PixelGrid {
export class HexagonalArenaGrid extends PixelArenaGrid {
private readonly yunit: number;
constructor(unit: number, yfactor = Math.sqrt(0.75)) {
super(unit);
constructor(bounds?: ArenaBounds, unit = 1, yfactor = Math.sqrt(0.75)) {
super(bounds, unit);
this.yunit = unit * yfactor;
}

View File

@ -51,6 +51,16 @@ module TK.SpaceTac {
}
}
/**
* Boundaries for an arena area
*/
export type ArenaBounds = {
xmin: number,
xmax: number,
ymin: number,
ymax: number,
}
/**
* Get the normalized angle (in radians) between two locations
*/

View File

@ -36,7 +36,7 @@ module TK.SpaceTac {
var ship5 = new Ship(fleet2, "F2S2");
var battle = new Battle(fleet1, fleet2, 1000, 500);
battle.grid = new PixelGrid();
battle.grid = new PixelArenaGrid();
battle.placeShips();
check.nears(ship1.arena_x, 250);
@ -280,7 +280,7 @@ module TK.SpaceTac {
test.case("lists area effects", check => {
let battle = new Battle();
battle.grid = new PixelGrid();
battle.grid = new PixelArenaGrid();
let ship = battle.fleets[0].addShip();
let peer = battle.fleets[1].addShip();
peer.setArenaPosition(100, 50);

View File

@ -39,7 +39,7 @@ module TK.SpaceTac {
ai_playing = false
constructor(fleet1 = new Fleet(new Player("Attacker")), fleet2 = new Fleet(new Player("Defender")), width = 1808, height = 948) {
this.grid = new HexagonalArenaGrid(50);
this.grid = new HexagonalArenaGrid({ xmin: 50, xmax: width - 50, ymin: 50, ymax: height - 50 }, 50);
this.fleets = [fleet1, fleet2];
this.ships = new RObjectContainer(fleet1.ships.concat(fleet2.ships));

View File

@ -72,7 +72,7 @@ module TK.SpaceTac.Specs {
test.case("applies vigilance actions", check => {
let battle = new Battle();
battle.grid = new PixelGrid();
battle.grid = new PixelArenaGrid();
let ship1 = battle.fleets[0].addShip();
ship1.setArenaPosition(100, 100);
TestTools.setShipModel(ship1, 10, 0, 5);

View File

@ -6,14 +6,14 @@ module TK.SpaceTac.Specs {
TestTools.setShipModel(ship, 100, 0, ship_ap);
TestTools.addEngine(ship, engine_distance);
let action = new TriggerAction("weapon", { power: weapon_ap, range: distance, blast: 0.1 });
let simulator = new MoveFireSimulator(ship, new PixelGrid());
let simulator = new MoveFireSimulator(ship, new PixelArenaGrid());
ship.actions.addCustom(action);
return [ship, simulator, action];
}
test.case("finds a suitable engine to make an approach", check => {
let ship = new Ship();
let simulator = new MoveFireSimulator(ship, new PixelGrid());
let simulator = new MoveFireSimulator(ship, new PixelArenaGrid());
check.equals(simulator.findEngine(), null, "no engine");
let engine1 = TestTools.addEngine(ship, 100);
engine1.configureCooldown(1, 1);
@ -92,7 +92,7 @@ module TK.SpaceTac.Specs {
});
test.case("scans a circle for move targets", check => {
let simulator = new MoveFireSimulator(new Ship(), new PixelGrid());
let simulator = new MoveFireSimulator(new Ship(), new PixelArenaGrid());
let result = simulator.scanCircle(50, 30, 10, 1, 1);
check.equals(imaterialize(result), [
@ -198,7 +198,7 @@ module TK.SpaceTac.Specs {
TestTools.setShipModel(enemy, 2, 1);
let engine = TestTools.addEngine(ship, 80);
let weapon = TestTools.addWeapon(ship, 5, 1, 150);
let simulator = new MoveFireSimulator(ship, new PixelGrid());
let simulator = new MoveFireSimulator(ship, new PixelArenaGrid());
let result = simulator.simulateAction(weapon, Target.newFromShip(enemy), 5);
let diffs = simulator.getExpectedDiffs(nn(ship.getBattle()), result);
check.equals(diffs, [

View File

@ -30,7 +30,7 @@ module TK.SpaceTac.Specs {
});
test.case("snaps to a grid", check => {
let grid = new HexagonalArenaGrid(5, 1);
let grid = new HexagonalArenaGrid(undefined, 5, 1);
check.equals(Target.newFromLocation(1, 1).snap(grid), Target.newFromLocation(0, 0), "1,1");
check.equals(Target.newFromLocation(6, 4).snap(grid), Target.newFromLocation(7.5, 5), "6,4");
let ship = new Ship();

View File

@ -16,7 +16,7 @@ module TK.SpaceTac {
var battle = new Battle(fleet1, fleet2);
if (!hexgrid) {
battle.grid = new PixelGrid();
battle.grid = new PixelArenaGrid();
}
battle.ships.list().forEach(ship => TestTools.setShipModel(ship, 1, 0));
battle.play_order = fleet1.ships.concat(fleet2.ships);

View File

@ -3,7 +3,7 @@ module TK.SpaceTac.Specs {
test.case("checks target is on the grid", check => {
let ship = new Ship();
let battle = new Battle(ship.fleet);
battle.grid = new PixelGrid();
battle.grid = new PixelArenaGrid();
TestTools.setShipPlaying(battle, ship);
TestTools.setShipModel(ship, 1, 0, 1);
let action = new MoveAction("Engine", { distance_per_power: 1 });

View File

@ -21,7 +21,7 @@ module TK.SpaceTac.Specs {
test.case("collects impacted ships", check => {
let action = new ToggleAction("testtoggle", { radius: 50 });
let battle = new Battle();
battle.grid = new PixelGrid();
battle.grid = new PixelArenaGrid();
let ship1 = battle.fleets[0].addShip();
ship1.setArenaPosition(0, 0);
let ship2 = battle.fleets[0].addShip();

View File

@ -28,7 +28,7 @@ module TK.SpaceTac.Specs {
ship3.alive = false;
let battle = new Battle(fleet);
battle.grid = new PixelGrid();
battle.grid = new PixelArenaGrid();
battle.play_order = [ship, ship1, ship2, ship3];
TestTools.setShipPlaying(battle, ship);
fleet.setBattle(battle);

View File

@ -47,7 +47,7 @@ module TK.SpaceTac.Specs {
test.case("handles the vigilance effect to know who to target", check => {
let battle = new Battle();
battle.grid = new PixelGrid();
battle.grid = new PixelArenaGrid();
let ship1a = battle.fleets[0].addShip();
ship1a.setArenaPosition(0, 0);
TestTools.setShipModel(ship1a, 10, 0, 5);

View File

@ -2,7 +2,7 @@ module TK.SpaceTac.Specs {
testing("Maneuver", test => {
test.case("uses move-fire simulation to build a list of battle diffs", check => {
let battle = new Battle();
battle.grid = new PixelGrid();
battle.grid = new PixelArenaGrid();
let ship1 = battle.fleets[0].addShip();
let ship2 = battle.fleets[1].addShip();
let ship3 = battle.fleets[1].addShip();

View File

@ -108,7 +108,7 @@ module TK.SpaceTac.Specs {
test.case("evaluates turn cost", check => {
let battle = new Battle();
battle.grid = new PixelGrid();
battle.grid = new PixelArenaGrid();
let ship = battle.fleets[0].addShip();
let weapon = TestTools.addWeapon(ship, 50, 5, 100);
let action = weapon;
@ -173,7 +173,7 @@ module TK.SpaceTac.Specs {
test.case("evaluates damage to enemies", check => {
let battle = new Battle();
battle.grid = new PixelGrid();
battle.grid = new PixelArenaGrid();
let ship = battle.fleets[0].addShip();
let action = TestTools.addWeapon(ship, 50, 5, 500, 100);
@ -199,7 +199,7 @@ module TK.SpaceTac.Specs {
test.case("evaluates ship clustering", check => {
let battle = new Battle();
battle.grid = new PixelGrid();
battle.grid = new PixelArenaGrid();
let ship = battle.fleets[0].addShip();
TestTools.setShipModel(ship, 100, 0, 10);
TestTools.addEngine(ship, 100);

View File

@ -82,7 +82,7 @@ module TK.SpaceTac.UI.Specs {
view.splash = false;
let battle = Battle.newQuickRandom();
battle.grid = new ArenaGrid();
battle.grid = new ArenaGrid({ xmin: 0, xmax: 10, ymin: 0, ymax: 10 });
battle.placeShips();
let player = new Player();
nn(battle.playing_ship).fleet.setPlayer(player);

View File

@ -242,18 +242,16 @@ module TK.SpaceTac.UI {
let action = this.action;
let simulator = new MoveFireSimulator(this.ship, grid);
range(35).forEach(x => {
range(20).forEach(y => {
let target = Target.newFromLocation(x * 50, y * 50).snap(grid);
let result = simulator.simulateAction(action, target);
if (result.status == MoveFireStatus.OK) {
this.grid_ok.add(target.x, target.y);
} else if (result.status == MoveFireStatus.NOT_ENOUGH_POWER) {
this.grid_power.add(target.x, target.y);
} else {
this.grid_invalid.add(target.x, target.y);
}
});
iforeach(grid.iterate(this.ship.location), loc => {
let target = Target.newFromLocation(loc.x, loc.y);
let result = simulator.simulateAction(action, target);
if (result.status == MoveFireStatus.OK) {
this.grid_ok.add(target.x, target.y);
} else if (result.status == MoveFireStatus.NOT_ENOUGH_POWER) {
this.grid_power.add(target.x, target.y);
} else {
this.grid_invalid.add(target.x, target.y);
}
});
}
}