import { TestCase } from "./framework/test_case.js"; import { mapToLocal, MapTopology, COL_STEP } from "../shared/hex.js"; import { DISTORTION, EDGE_SEGMENTS, CORNERS, periodUnits, vertexOffset, distortPoint, distortedCorners, distortedBoundary, edgePoint, bentPath, latticeKey, vertexKey, } from "../client/js/map_view/distortion.js"; export class DistortionTest extends TestCase { async test_is_deterministic_for_a_seed() { const first = distortedCorners(1234, { x: 40, y: 40 }, 0); const second = distortedCorners(1234, { x: 40, y: 40 }, 0); this.assertEqual(first, second, "the same seed warps the same way"); const other = distortedCorners(5678, { x: 40, y: 40 }, 0); this.assertNotEqual(first, other, "a different seed gives a different shape"); } async test_offsets_stay_within_the_amplitude() { for (let xi = -50; xi <= 50; xi += 7) { for (let yi = -50; yi <= 50; yi += 5) { const offset = vertexOffset(99, xi, yi, 0); this.assertTrue(Math.abs(offset.x) <= DISTORTION.x, `x offset ${offset.x} too large`); this.assertTrue(Math.abs(offset.y) <= DISTORTION.y, `y offset ${offset.y} too large`); } } } async test_shared_corners_move_together_so_the_mesh_stays_watertight() { // Tile (0,0)'s right corner is the same lattice vertex as tile (1,0)'s // top-left corner: (31.5, 0). If the two tiles displaced it differently the // map would tear along their shared edge. const a = distortedCorners(2024, mapToLocal(0, 0), 0); const b = distortedCorners(2024, mapToLocal(1, 0), 0); this.assertEqual(a[0], b[2], "the shared corner is displaced identically"); // Same for the lower shared vertex (15.75, 28): (0,0)'s bottom-right and // (1,0)'s left corner. this.assertEqual(a[5], b[3], "the second shared corner is displaced identically"); } async test_cylindrical_wrap_is_seamless() { const topology = new MapTopology({ x: 40, y: 30 }, true); const period = periodUnits(topology); this.assertEqual(period, topology.width * 3, "one period is 3*width lattice columns"); const x = 31.5; const wrap = COL_STEP * topology.width; const near = distortPoint(555, x, -28, period); const wrapped = distortPoint(555, x + wrap, -28, period); this.assertApprox(wrapped.x - near.x, wrap, 1e-9); this.assertApprox(wrapped.y, near.y, 1e-9); this.assertEqual(periodUnits(new MapTopology({ x: 4, y: 4 }, false)), 0, "flat maps do not fold"); } async test_negative_lattice_coordinates_are_folded_consistently() { const period = 60; const wrap = period * (63 / 4); const x = -10; const before = distortPoint(12, x, -10, period); const after = distortPoint(12, x + wrap, -10, period); this.assertApprox(after.x - before.x, wrap, 1e-9); this.assertApprox(after.y, before.y, 1e-9); } async test_wrapped_edges_are_identical() { // An edge and its copy one wrap period over are the same border. Both must // bend the same way, or the map tears along the cylindrical seam. const topology = new MapTopology({ x: 40, y: 30 }, true); const period = periodUnits(topology); const wrap = COL_STEP * topology.width; const a = { x: 31.5, y: 0 }; const b = { x: 15.75, y: -28 }; for (let j = 0; j <= 8; j++) { const t = j / 8; const point = edgePoint(555, a, b, t, period); const copy = edgePoint(555, { x: a.x + wrap, y: a.y }, { x: b.x + wrap, y: b.y }, t, period); this.assertApprox(copy.x - point.x, wrap, 1e-9, "seam edge x"); this.assertApprox(copy.y, point.y, 1e-9, "seam edge y"); } } async test_edges_are_subdivided_and_meet_their_corners() { const centre = mapToLocal(3, -2); const boundary = distortedBoundary(77, centre, 0); this.assertSize(boundary, CORNERS.length * EDGE_SEGMENTS, "every edge is split"); // Each edge's first point is its starting corner, displaced exactly like // the corner function, so adjacent edges still join. for (let i = 0; i < CORNERS.length; i++) { const point = boundary[i * EDGE_SEGMENTS]; const corner = distortPoint(77, centre.x + CORNERS[i].x, centre.y + CORNERS[i].y, 0); this.assertApprox(point.x, corner.x, 1e-9, "corner moved"); this.assertApprox(point.y, corner.y, 1e-9, "corner moved"); this.assertApprox(point.idealX, centre.x + CORNERS[i].x, 1e-9, "ideal corner"); this.assertApprox(point.idealY, centre.y + CORNERS[i].y, 1e-9, "ideal corner"); this.assertEqual(point.t, 0); } } async test_wobbled_edge_is_shared_by_both_neighbours() { // Edge (31.5, 0) -> (15.75, -28) of tile (0,0) is the same border as edge // (15.75, -28) -> (31.5, 0) of its neighbour (1,-1). Traversed in opposite // directions, both must produce the identical set of points. const a0 = { x: 31.5, y: 0 }; const a1 = { x: 15.75, y: -28 }; const fromA = []; const fromB = []; for (let j = 0; j <= EDGE_SEGMENTS; j++) { fromA.push(edgePoint(4242, a0, a1, j / EDGE_SEGMENTS, 0)); fromB.push(edgePoint(4242, a1, a0, j / EDGE_SEGMENTS, 0)); } const sort = (list) => list.slice().sort((p, q) => (p.x !== q.x ? p.x - q.x : p.y - q.y)); const left = sort(fromA); const right = sort(fromB); this.assertSize(left, right.length); for (let i = 0; i < left.length; i++) { this.assertApprox(left[i].x, right[i].x, 1e-9, "shared border point"); this.assertApprox(left[i].y, right[i].y, 1e-9, "shared border point"); } } async test_edge_wobble_vanishes_at_the_corners() { const a = { x: 31.5, y: 0 }; const b = { x: 15.75, y: -28 }; this.assertEqual(edgePoint(9, a, b, 0, 0), distortPoint(9, a.x, a.y, 0)); this.assertEqual(edgePoint(9, a, b, 1, 0), distortPoint(9, b.x, b.y, 0)); } async test_shared_corners_share_a_lattice_key() { // Tile (0,0)'s right corner and tile (1,0)'s top-left corner are the same // lattice vertex, so border chaining can stitch them with one integer key. const a = distortedCorners(1, mapToLocal(0, 0), 0); const b = distortedCorners(1, mapToLocal(1, 0), 0); this.assertEqual(latticeKey(a[0].x, a[0].y), latticeKey(b[2].x, b[2].y)); this.assertNotEqual(latticeKey(a[0].x, a[0].y), latticeKey(a[1].x, a[1].y)); } async test_bent_path_still_starts_and_ends_where_asked() { const from = { x: 0, y: 0 }; const to = { x: 30, y: 12 }; this.assertEqual(bentPath(5, from, to, 1, 0), from); this.assertEqual(bentPath(5, from, to, 1, 1), to); const middle = bentPath(5, from, to, 1, 0.5); this.assertApprox(middle.x, 15, DISTORTION.road, "the bend stays small"); } async test_a_straight_edge_is_the_line_between_its_warped_corners() { // `wavy` false drops the saw-tooth but keeps the shared corners, so open // water reads as a regular hexagon while still tiling with its neighbours. const a = { x: 31.5, y: 0 }; const b = { x: 15.75, y: -28 }; const ca = distortPoint(9, a.x, a.y, 0); const cb = distortPoint(9, b.x, b.y, 0); for (let j = 0; j <= 8; j++) { const t = j / 8; const point = edgePoint(9, a, b, t, 0, false); this.assertApprox(point.x, ca.x + (cb.x - ca.x) * t, 1e-9, "straight edge x"); this.assertApprox(point.y, ca.y + (cb.y - ca.y) * t, 1e-9, "straight edge y"); } } async test_boundary_honours_the_per_edge_waviness() { const centre = mapToLocal(2, 1); const wavy = [false, true, false, true, false, true]; const boundary = distortedBoundary(31, centre, 0, EDGE_SEGMENTS, wavy); this.assertSize(boundary, CORNERS.length * EDGE_SEGMENTS, "every edge is still split"); for (let i = 0; i < CORNERS.length; i++) { if (wavy[i]) continue; const a = { x: centre.x + CORNERS[i].x, y: centre.y + CORNERS[i].y }; const next = CORNERS[(i + 1) % CORNERS.length]; const b = { x: centre.x + next.x, y: centre.y + next.y }; const ca = distortPoint(31, a.x, a.y, 0); const cb = distortPoint(31, b.x, b.y, 0); for (let j = 0; j < EDGE_SEGMENTS; j++) { const point = boundary[i * EDGE_SEGMENTS + j]; const t = j / EDGE_SEGMENTS; this.assertApprox(point.x, ca.x + (cb.x - ca.x) * t, 1e-9, "straight edge x"); this.assertApprox(point.y, ca.y + (cb.y - ca.y) * t, 1e-9, "straight edge y"); } } } async test_an_empty_warp_set_leaves_the_ideal_hexagon() { // Open water: no vertex is warped and no edge is wavy, so the boundary is // the plain regular hexagon. const centre = mapToLocal(3, -1); const boundary = distortedBoundary(31, centre, 0, EDGE_SEGMENTS, null, new Set()); for (let i = 0; i < CORNERS.length; i++) { const ideal = { x: centre.x + CORNERS[i].x, y: centre.y + CORNERS[i].y }; const point = boundary[i * EDGE_SEGMENTS]; this.assertApprox(point.x, ideal.x, 1e-9, "the corner is unwarped"); this.assertApprox(point.y, ideal.y, 1e-9, "the corner is unwarped"); } } async test_the_warp_set_selects_which_vertices_move() { const a = { x: 31.5, y: 0 }; const b = { x: 15.75, y: -28 }; const key = vertexKey(a.x, a.y, 0); const onlyA = new Set([key]); const start = edgePoint(9, a, b, 0, 0, false, onlyA); const end = edgePoint(9, a, b, 1, 0, false, onlyA); const movedA = distortPoint(9, a.x, a.y, 0); this.assertApprox(start.x, movedA.x, 1e-9, "a listed vertex is warped"); this.assertApprox(start.y, movedA.y, 1e-9, "a listed vertex is warped"); this.assertApprox(end.x, b.x, 1e-9, "an unlisted vertex stays put"); this.assertApprox(end.y, b.y, 1e-9, "an unlisted vertex stays put"); } }