149 lines
5.5 KiB
JavaScript
149 lines
5.5 KiB
JavaScript
import { TestCase } from "./framework/test_case.js";
|
|
import {
|
|
MapTopology,
|
|
mapToLocal,
|
|
localToMap,
|
|
nearestCopyPoint,
|
|
flightLine,
|
|
flightDistanceTiles,
|
|
key,
|
|
parseKey,
|
|
COL_STEP,
|
|
HEX_H,
|
|
} from "../shared/hex.js";
|
|
|
|
export class HexTest extends TestCase {
|
|
flat() {
|
|
return new MapTopology({ x: 10, y: 10 }, false);
|
|
}
|
|
|
|
cylinder() {
|
|
return new MapTopology({ x: 10, y: 10 }, true);
|
|
}
|
|
|
|
test_topology_type_width_and_period() {
|
|
this.assertFalse(this.flat().isCylindrical());
|
|
this.assertApprox(this.flat().periodPixels(), 0, 1e-9);
|
|
const cylinder = this.cylinder();
|
|
this.assertTrue(cylinder.isCylindrical());
|
|
this.assertEqual(cylinder.width, 10);
|
|
this.assertApprox(cylinder.periodPixels(), COL_STEP * 10, 1e-9);
|
|
}
|
|
|
|
test_wrap_x_folds_only_on_a_cylinder() {
|
|
const flatTopology = this.flat();
|
|
this.assertEqual(flatTopology.wrapX(25), 25);
|
|
this.assertEqual(flatTopology.wrapCoords(25, -3), { x: 25, y: -3 });
|
|
const topology = this.cylinder();
|
|
// Origin is -5, so the valid x range is -5..4.
|
|
this.assertEqual(topology.wrapX(-5), -5);
|
|
this.assertEqual(topology.wrapX(4), 4);
|
|
this.assertEqual(topology.wrapX(5), -5);
|
|
this.assertEqual(topology.wrapX(-6), 4);
|
|
}
|
|
|
|
test_wrapped_delta_takes_the_short_route() {
|
|
this.assertEqual(this.flat().wrappedDelta({ x: -5, y: 0 }, { x: 4, y: 0 }), { x: 9, y: 0 });
|
|
this.assertEqual(this.cylinder().wrappedDelta({ x: -5, y: 0 }, { x: 4, y: 0 }), { x: -1, y: 0 });
|
|
this.assertApprox(this.cylinder().tileDistance({ x: -5, y: 0 }, { x: 4, y: 0 }), 1.0, 1e-9);
|
|
|
|
const topology = this.cylinder();
|
|
const delta = topology.pixelDelta({ x: 0, y: 0 }, { x: -4, y: 0 });
|
|
const expected = mapToLocal(-4, 0);
|
|
this.assertApprox(delta.x, expected.x, 1e-9);
|
|
this.assertApprox(delta.y, expected.y, 1e-9);
|
|
}
|
|
|
|
test_neighbours_are_six_adjacent_cells_that_wrap() {
|
|
const topology = this.flat();
|
|
const neighbours = topology.neighbours(0, 0).map((c) => key(c.x, c.y)).sort();
|
|
this.assertEqual(neighbours, ["-1,-1", "-1,0", "0,-1", "0,1", "1,-1", "1,0"]);
|
|
|
|
const centre = mapToLocal(0, 0);
|
|
for (const neighbour of topology.neighbours(0, 0)) {
|
|
const delta = topology.pixelDelta({ x: 0, y: 0 }, neighbour);
|
|
const distance = Math.hypot(delta.x, delta.y);
|
|
this.assert(distance > 0 && distance <= HEX_H + 1e-9, `distance ${distance}`);
|
|
const local = mapToLocal(neighbour.x, neighbour.y);
|
|
this.assertApprox(local.x - centre.x, delta.x, 1e-9);
|
|
this.assertApprox(local.y - centre.y, delta.y, 1e-9);
|
|
}
|
|
|
|
const cylinder = this.cylinder();
|
|
for (const neighbour of cylinder.neighbours(-5, 0)) {
|
|
this.assert(neighbour.x >= -5 && neighbour.x <= 4, `x ${neighbour.x} outside map`);
|
|
}
|
|
this.assertTrue(
|
|
cylinder.neighbours(-5, 0).some((c) => c.x === 4),
|
|
"the west neighbour of the seam wraps to the east edge"
|
|
);
|
|
}
|
|
|
|
test_local_to_map_inverts_map_to_local() {
|
|
const flat = this.flat();
|
|
for (const coords of [{ x: 0, y: 0 }, { x: 3, y: -4 }, { x: -7, y: 2 }]) {
|
|
this.assertEqual(localToMap(mapToLocal(coords.x, coords.y), flat), coords);
|
|
}
|
|
// A pixel a few units off a centre still resolves to that tile.
|
|
const centre = mapToLocal(2, 3);
|
|
this.assertEqual(localToMap({ x: centre.x + 4, y: centre.y - 3 }, flat), { x: 2, y: 3 });
|
|
// On a cylinder the resolved tile folds back into range.
|
|
this.assertEqual(localToMap(mapToLocal(5, 0), this.cylinder()), { x: -5, y: 0 });
|
|
}
|
|
|
|
test_nearest_copy_point_folds_by_whole_periods() {
|
|
const point = mapToLocal(0, 0);
|
|
const period = this.cylinder().periodPixels();
|
|
const reference = { x: point.x - period * 1.2, y: 0 };
|
|
const copy = nearestCopyPoint(point, reference, this.cylinder());
|
|
this.assertApprox(copy.x, point.x - period, 1e-9);
|
|
this.assertEqual(copy.y, point.y);
|
|
// A flat map returns the point untouched.
|
|
this.assertEqual(nearestCopyPoint(point, reference, this.flat()), point);
|
|
}
|
|
|
|
test_flight_line_takes_the_shortest_wrapped_copy() {
|
|
const topology = this.cylinder();
|
|
const from = mapToLocal(-5, 0);
|
|
// -5 and 4 are neighbours across the seam, one map period apart.
|
|
const line = flightLine(from, { x: 4, y: 0 }, topology);
|
|
this.assertEqual(line.length, 2);
|
|
this.assertEqual(line[0], from);
|
|
// Adjacent columns are offset by half a hex, so a neighbour hop is a
|
|
// diagonal of hypot(COL_STEP, HEX_H/2), not a bare COL_STEP.
|
|
const hop = Math.hypot(COL_STEP, HEX_H / 2);
|
|
this.assertApprox(
|
|
Math.hypot(line[1].x - line[0].x, line[1].y - line[0].y),
|
|
hop,
|
|
1e-6,
|
|
"the flight leaves one edge and re-enters the other"
|
|
);
|
|
}
|
|
|
|
test_flight_distance_measures_tiles_along_the_line() {
|
|
const maxStep = HEX_H;
|
|
const hop = Math.hypot(COL_STEP, HEX_H / 2);
|
|
this.assertApprox(
|
|
flightDistanceTiles({ x: 0, y: 0 }, { x: 1, y: 0 }, this.flat(), maxStep),
|
|
hop / maxStep,
|
|
1e-9
|
|
);
|
|
// Across the seam on a cylinder it is the short hop, not the long way.
|
|
this.assertApprox(
|
|
flightDistanceTiles({ x: -5, y: 0 }, { x: 4, y: 0 }, this.cylinder(), maxStep),
|
|
hop / maxStep,
|
|
1e-9
|
|
);
|
|
}
|
|
|
|
test_key_and_parse_key_round_trip_and_odd_column_offset() {
|
|
for (const coords of [{ x: 0, y: 0 }, { x: 3, y: -4 }, { x: -12, y: 7 }]) {
|
|
this.assertEqual(parseKey(key(coords.x, coords.y)), coords);
|
|
}
|
|
const even = mapToLocal(0, 0);
|
|
const odd = mapToLocal(1, 0);
|
|
this.assertApprox(odd.x - even.x, COL_STEP, 1e-9);
|
|
this.assertApprox(odd.y - even.y, HEX_H / 2, 1e-9);
|
|
}
|
|
}
|