Files
MatchMaster/tools/test-laser-rules.cjs
T
2026-10-20 11:24:09 +08:00

124 lines
5.4 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const ts = require('typescript');
const exported = {};
const source = fs.readFileSync(path.join(__dirname, '../assets/Script/LaserRules.ts'), 'utf8');
vm.runInNewContext(ts.transpileModule(source, {
compilerOptions: { target: ts.ScriptTarget.ES2017, module: ts.ModuleKind.CommonJS },
}).outputText, { exports: exported });
const { nearestLaserHit } = exported;
const point = (x, y) => ({ x, y });
const rect = (minX, minY, maxX, maxY) => ({ type: 'rect', minX, minY, maxX, maxY });
const polygon = coordinates => ({ type: 'polygon', points: coordinates.map(([x, y]) => point(x, y)) });
const close = (actual, expected) => assert.ok(Math.abs(actual - expected) < 1e-6, `${actual} != ${expected}`);
for (const [direction, start, end, expected] of [
['right', point(0, 5), point(10, 5), point(4, 5)],
['left', point(10, 5), point(0, 5), point(6, 5)],
['up', point(5, 0), point(5, 10), point(5, 4)],
['down', point(5, 10), point(5, 0), point(5, 6)],
]) {
test(`${direction} beam ends at the nearest rectangle surface`, () => {
const hit = nearestLaserHit(start, end, [rect(4, 4, 6, 6)]);
close(hit.ratio, 0.4);
close(hit.distance, 4);
close(hit.point.x, expected.x);
close(hit.point.y, expected.y);
});
}
test('nearest blocker wins regardless of collider ordering', () => {
const hit = nearestLaserHit(point(0, 0), point(10, 0), [
rect(8, -1, 9, 1),
{ type: 'circle', center: point(5, 0), radius: 1 },
polygon([[2, -1], [3, -1], [3, 1], [2, 1]]),
]);
assert.equal(hit.colliderIndex, 2);
close(hit.distance, 2);
});
test('fractional dragging positions unblock immediately, without grid snapping', () => {
const start = point(0, 0), end = point(10, 0);
const moving = rect(4.125, -0.5, 5.125, 0.5);
close(nearestLaserHit(start, end, [moving]).distance, 4.125);
moving.minY += 0.501;
moving.maxY += 0.501;
assert.equal(nearestLaserHit(start, end, [moving]), null);
moving.minY -= 0.002;
moving.maxY -= 0.002;
close(nearestLaserHit(start, end, [moving]).distance, 4.125);
});
test('a beam in the empty part of a concave shape is not blocked by its bounds', () => {
const lShape = polygon([[2, 0], [6, 0], [6, 1], [3, 1], [3, 4], [2, 4]]);
assert.equal(nearestLaserHit(point(4, 3), point(7, 3), [lShape]), null);
const hit = nearestLaserHit(point(7, 3), point(0, 3), [lShape]);
close(hit.point.x, 3);
close(hit.distance, 4);
lShape.points.reverse();
close(nearestLaserHit(point(7, 3), point(0, 3), [lShape]).distance, 4);
});
test('circle surface, tangent, and a clear line are distinguished', () => {
const circle = { type: 'circle', center: point(5, 0), radius: 2 };
close(nearestLaserHit(point(0, 0), point(10, 0), [circle]).distance, 3);
close(nearestLaserHit(point(0, 2), point(10, 2), [circle]).distance, 5);
assert.equal(nearestLaserHit(point(0, 2.001), point(10, 2.001), [circle]), null);
});
test('an emitter inside any solid has a zero-distance hit', () => {
const solids = [
rect(-1, -1, 1, 1),
{ type: 'circle', center: point(0, 0), radius: 1 },
polygon([[-1, -1], [1, -1], [1, 1], [-1, 1]]),
];
for (const solid of solids) {
const hit = nearestLaserHit(point(0, 0), point(10, 0), [solid]);
assert.equal(hit.ratio, 0);
assert.equal(hit.distance, 0);
}
});
test('touching a solid boundary blocks, including a collinear polygon edge', () => {
for (const solid of [rect(3, 0, 5, 2), polygon([[3, 0], [5, 0], [5, 2], [3, 2]])]) {
close(nearestLaserHit(point(0, 0), point(10, 0), [solid]).distance, 3);
close(nearestLaserHit(point(10, 0), point(0, 0), [solid]).distance, 5);
assert.equal(nearestLaserHit(point(3, 1), point(0, 1), [solid]).ratio, 0);
}
});
test('objects behind the emitter or beyond the receiver do not block', () => {
const solids = [
rect(-3, -1, -2, 1), rect(11, -1, 12, 1),
{ type: 'circle', center: point(13, 0), radius: 2 },
polygon([[11, -1], [12, -1], [12, 1], [11, 1]]),
];
assert.equal(nearestLaserHit(point(0, 0), point(10, 0), solids), null);
close(nearestLaserHit(point(0, 0), point(10, 0), [rect(10, -1, 11, 1)]).ratio, 1);
});
test('rotated polygon surfaces use their edges, not their axis-aligned bounds', () => {
const diamond = polygon([[5, 0], [7, 2], [5, 4], [3, 2]]);
close(nearestLaserHit(point(0, 1), point(10, 1), [diamond]).distance, 4);
const diagonal = nearestLaserHit(point(0, 0), point(10, 10), [rect(3, 3, 4, 4)]);
close(diagonal.ratio, 0.3);
close(diagonal.distance, Math.sqrt(18));
});
test('crossing beams are queried independently against the same solids', () => {
const blockers = [rect(7, 4, 8, 6)];
close(nearestLaserHit(point(0, 5), point(10, 5), blockers).distance, 7);
assert.equal(nearestLaserHit(point(5, 0), point(5, 10), blockers), null);
close(nearestLaserHit(point(0, 5), point(10, 5), blockers).distance, 7);
});
test('empty colliders and zero-length lines have defined results', () => {
assert.equal(nearestLaserHit(point(0, 0), point(10, 0), []), null);
assert.equal(nearestLaserHit(point(0, 0), point(0, 0), [rect(1, 1, 2, 2)]), null);
assert.equal(nearestLaserHit(point(0, 0), point(0, 0), [rect(-1, -1, 1, 1)]).distance, 0);
});