diff --git a/src/common b/src/common index 1425cb0..8a11307 160000 --- a/src/common +++ b/src/common @@ -1 +1 @@ -Subproject commit 1425cb08935dd996a4c7a644ab793ff3b8355c9b +Subproject commit 8a11307e3fb1b2c562674a475fa50b8511592bfe diff --git a/src/core/ArenaGrid.spec.ts b/src/core/ArenaGrid.spec.ts index c27ccc8..1cf68af 100644 --- a/src/core/ArenaGrid.spec.ts +++ b/src/core/ArenaGrid.spec.ts @@ -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); + }); }); } diff --git a/src/core/ArenaGrid.ts b/src/core/ArenaGrid.ts index 98606e8..115fb5b 100644 --- a/src/core/ArenaGrid.ts +++ b/src/core/ArenaGrid.ts @@ -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 { + from = this.snap(from); + + let row = (rfrom: IArenaLocation): Iterator => { + 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; } diff --git a/src/core/ArenaLocation.ts b/src/core/ArenaLocation.ts index 59eed53..dc00f86 100644 --- a/src/core/ArenaLocation.ts +++ b/src/core/ArenaLocation.ts @@ -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 */ diff --git a/src/core/Battle.spec.ts b/src/core/Battle.spec.ts index b014bac..ffc4d8a 100644 --- a/src/core/Battle.spec.ts +++ b/src/core/Battle.spec.ts @@ -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); diff --git a/src/core/Battle.ts b/src/core/Battle.ts index 5cca012..d1e78c9 100644 --- a/src/core/Battle.ts +++ b/src/core/Battle.ts @@ -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)); diff --git a/src/core/BattleChecks.spec.ts b/src/core/BattleChecks.spec.ts index f6c640d..c651981 100644 --- a/src/core/BattleChecks.spec.ts +++ b/src/core/BattleChecks.spec.ts @@ -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); diff --git a/src/core/MoveFireSimulator.spec.ts b/src/core/MoveFireSimulator.spec.ts index 9d70d09..16fee3e 100644 --- a/src/core/MoveFireSimulator.spec.ts +++ b/src/core/MoveFireSimulator.spec.ts @@ -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, [ diff --git a/src/core/Target.spec.ts b/src/core/Target.spec.ts index da9f997..1bd3e9b 100644 --- a/src/core/Target.spec.ts +++ b/src/core/Target.spec.ts @@ -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(); diff --git a/src/core/TestTools.ts b/src/core/TestTools.ts index e26cb09..7a4f529 100644 --- a/src/core/TestTools.ts +++ b/src/core/TestTools.ts @@ -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); diff --git a/src/core/actions/MoveAction.spec.ts b/src/core/actions/MoveAction.spec.ts index f434eec..59c22c1 100644 --- a/src/core/actions/MoveAction.spec.ts +++ b/src/core/actions/MoveAction.spec.ts @@ -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 }); diff --git a/src/core/actions/ToggleAction.spec.ts b/src/core/actions/ToggleAction.spec.ts index 7714c96..8fe6090 100644 --- a/src/core/actions/ToggleAction.spec.ts +++ b/src/core/actions/ToggleAction.spec.ts @@ -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(); diff --git a/src/core/actions/TriggerAction.spec.ts b/src/core/actions/TriggerAction.spec.ts index 96ba7d0..0ebb6e8 100644 --- a/src/core/actions/TriggerAction.spec.ts +++ b/src/core/actions/TriggerAction.spec.ts @@ -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); diff --git a/src/core/actions/VigilanceAction.spec.ts b/src/core/actions/VigilanceAction.spec.ts index 3b2ab25..eadbea3 100644 --- a/src/core/actions/VigilanceAction.spec.ts +++ b/src/core/actions/VigilanceAction.spec.ts @@ -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); diff --git a/src/core/ai/Maneuver.spec.ts b/src/core/ai/Maneuver.spec.ts index 584531b..3983552 100644 --- a/src/core/ai/Maneuver.spec.ts +++ b/src/core/ai/Maneuver.spec.ts @@ -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(); diff --git a/src/core/ai/TacticalAIHelpers.spec.ts b/src/core/ai/TacticalAIHelpers.spec.ts index c201ab8..01d6752 100644 --- a/src/core/ai/TacticalAIHelpers.spec.ts +++ b/src/core/ai/TacticalAIHelpers.spec.ts @@ -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); diff --git a/src/ui/TestGame.ts b/src/ui/TestGame.ts index f6f5cab..3eb438c 100644 --- a/src/ui/TestGame.ts +++ b/src/ui/TestGame.ts @@ -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); diff --git a/src/ui/battle/Targetting.ts b/src/ui/battle/Targetting.ts index 797134e..0ae77af 100644 --- a/src/ui/battle/Targetting.ts +++ b/src/ui/battle/Targetting.ts @@ -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); + } }); } }