Files
Battle-for-Tismo/tests/distortion_test.js
T

237 lines
10 KiB
JavaScript

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,
cubicBezier,
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_cubic_bezier_hits_the_endpoints_and_the_midpoint_formula() {
const p0 = { x: 0, y: 0 };
const p1 = { x: 10, y: 20 };
const p2 = { x: 30, y: 20 };
const p3 = { x: 40, y: 0 };
const points = cubicBezier(p0, p1, p2, p3, 16);
this.assertEqual(points.length, 17, "the curve is tessellated end to end");
this.assertApprox(points[0].x, p0.x, 1e-9);
this.assertApprox(points[0].y, p0.y, 1e-9);
this.assertApprox(points[16].x, p3.x, 1e-9);
this.assertApprox(points[16].y, p3.y, 1e-9);
// The cubic midpoint is (P0 + 3P1 + 3P2 + P3) / 8.
this.assertApprox(points[8].x, (p0.x + 3 * p1.x + 3 * p2.x + p3.x) / 8, 1e-9);
this.assertApprox(points[8].y, (p0.y + 3 * p1.y + 3 * p2.y + p3.y) / 8, 1e-9);
// It bows away from the straight chord.
this.assertApprox(points[8].y, 15, 1e-9, "the curve winds");
}
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");
}
}