188 lines
7.9 KiB
JavaScript
188 lines
7.9 KiB
JavaScript
import { TestCase } from "./framework/test_case.js";
|
|
import { TECHNOLOGIES } from "../shared/data.js";
|
|
import {
|
|
romanNumeral,
|
|
layoutTechnologies,
|
|
chamferPath,
|
|
unmetPrerequisites,
|
|
TECH_LAYOUT,
|
|
} from "../client/js/ui/tech_tree.js";
|
|
|
|
const indexOf = (id) => TECHNOLOGIES.findIndex((t) => t.id === id);
|
|
|
|
// Every axis-aligned run of every link, as {x1,y1,x2,y2,label,port}. The first
|
|
// and last run are the port stubs, which several links sharing a card port are
|
|
// allowed to overlap (they fan out of, or converge into, one point).
|
|
function segments(layout) {
|
|
const runs = [];
|
|
for (const e of layout.edges) {
|
|
const label = `${TECHNOLOGIES[e.from].id}->${TECHNOLOGIES[e.to].id}`;
|
|
for (let i = 1; i < e.points.length; i++) {
|
|
const [x1, y1] = e.points[i - 1];
|
|
const [x2, y2] = e.points[i];
|
|
if (Math.hypot(x2 - x1, y2 - y1) < 0.5) continue;
|
|
const port = i === 1 || i === e.points.length - 1;
|
|
runs.push({ x1, y1, x2, y2, label, port });
|
|
}
|
|
}
|
|
return runs;
|
|
}
|
|
|
|
const horizontal = (s) => Math.abs(s.y1 - s.y2) < 0.01;
|
|
const vertical = (s) => Math.abs(s.x1 - s.x2) < 0.01;
|
|
|
|
export class TechTreeTest extends TestCase {
|
|
test_repeatable_levels_are_roman_up_to_ten() {
|
|
this.assertEqual(romanNumeral(1), "I");
|
|
this.assertEqual(romanNumeral(4), "IV");
|
|
this.assertEqual(romanNumeral(9), "IX");
|
|
this.assertEqual(romanNumeral(10), "X");
|
|
// Beyond the catalogue's practical reach a plain number is used.
|
|
this.assertEqual(romanNumeral(11), "11");
|
|
}
|
|
|
|
test_column_is_the_distance_from_a_root() {
|
|
const layout = layoutTechnologies(TECHNOLOGIES);
|
|
this.assertEqual(layout.placed.get(indexOf("industrial_automation")).col, 0, "a root opens column 0");
|
|
this.assertEqual(layout.placed.get(indexOf("quantum_computing")).col, 0);
|
|
this.assertEqual(layout.placed.get(indexOf("space_program")).col, 1, "one step from a root");
|
|
this.assertEqual(layout.placed.get(indexOf("fusion_power")).col, 2, "two steps from a root");
|
|
// Every technology of a tier shares its column.
|
|
for (const [index, pos] of layout.placed) {
|
|
const roots = TECHNOLOGIES[index].prerequisites;
|
|
if (roots.length) {
|
|
const parents = roots.map((id) => layout.placed.get(indexOf(id)).col);
|
|
this.assertEqual(pos.col, Math.max(...parents) + 1, `${TECHNOLOGIES[index].id} sits one past its deepest parent`);
|
|
} else {
|
|
this.assertEqual(pos.col, 0, `${TECHNOLOGIES[index].id} is a root`);
|
|
}
|
|
}
|
|
}
|
|
|
|
test_every_prerequisite_sits_to_the_left() {
|
|
const layout = layoutTechnologies(TECHNOLOGIES);
|
|
for (const [index, pos] of layout.placed) {
|
|
for (const id of TECHNOLOGIES[index].prerequisites) {
|
|
const parent = layout.placed.get(indexOf(id));
|
|
this.assertTrue(parent.x < pos.x, `${TECHNOLOGIES[index].id} follows ${id}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
test_every_prerequisite_has_a_routed_link() {
|
|
const layout = layoutTechnologies(TECHNOLOGIES);
|
|
let expected = 0;
|
|
for (const tech of TECHNOLOGIES) expected += tech.prerequisites.length;
|
|
this.assertSize(layout.edges, expected, "one link per prerequisite");
|
|
for (const e of layout.edges) {
|
|
const parent = layout.placed.get(e.from);
|
|
const child = layout.placed.get(e.to);
|
|
this.assertEqual(e.points[0][0], parent.x + TECH_LAYOUT.W, "the link leaves the parent's right edge");
|
|
const last = e.points[e.points.length - 1];
|
|
this.assertEqual(last[0], child.x, "the link enters the child's left edge");
|
|
for (let i = 1; i < e.points.length; i++) {
|
|
const a = e.points[i - 1];
|
|
const b = e.points[i];
|
|
this.assertTrue(
|
|
Math.abs(a[0] - b[0]) < 0.5 || Math.abs(a[1] - b[1]) < 0.5,
|
|
"every run is axis-aligned"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
test_links_are_chamfered_without_curves() {
|
|
const layout = layoutTechnologies(TECHNOLOGIES);
|
|
const edge = layout.edges.find(
|
|
(e) => TECHNOLOGIES[e.from].id === "quantum_computing" && TECHNOLOGIES[e.to].id === "space_program"
|
|
);
|
|
const d = chamferPath(edge.points);
|
|
this.assertTrue(d.startsWith("M "), "the path starts with a move");
|
|
this.assertFalse(d.includes("NaN"), "no coordinate is missing");
|
|
this.assertFalse(d.includes("C "), "there is no bezier curve");
|
|
}
|
|
|
|
// The whole point of the lane routing: distinct dependencies must never be
|
|
// drawn on top of each other. The short stubs that fan out of a shared card
|
|
// port are the one exception, allowed to lie along the same line.
|
|
test_no_two_links_overlap() {
|
|
const layout = layoutTechnologies(TECHNOLOGIES);
|
|
const runs = segments(layout);
|
|
for (let i = 0; i < runs.length; i++) {
|
|
for (let j = i + 1; j < runs.length; j++) {
|
|
const a = runs[i];
|
|
const b = runs[j];
|
|
if (a.port || b.port) continue;
|
|
if (horizontal(a) && horizontal(b) && Math.abs(a.y1 - b.y1) < 0.01) {
|
|
const lo = Math.max(Math.min(a.x1, a.x2), Math.min(b.x1, b.x2));
|
|
const hi = Math.min(Math.max(a.x1, a.x2), Math.max(b.x1, b.x2));
|
|
this.assertTrue(hi - lo < 0.5, `${a.label} overlaps ${b.label}`);
|
|
}
|
|
if (vertical(a) && vertical(b) && Math.abs(a.x1 - b.x1) < 0.01) {
|
|
const lo = Math.max(Math.min(a.y1, a.y2), Math.min(b.y1, b.y2));
|
|
const hi = Math.min(Math.max(a.y1, a.y2), Math.max(b.y1, b.y2));
|
|
this.assertTrue(hi - lo < 0.5, `${a.label} overlaps ${b.label}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
test_every_card_has_a_single_port_each_way() {
|
|
const layout = layoutTechnologies(TECHNOLOGIES);
|
|
const { W, H } = TECH_LAYOUT;
|
|
for (const e of layout.edges) {
|
|
const parent = layout.placed.get(e.from);
|
|
const child = layout.placed.get(e.to);
|
|
const start = e.points[0];
|
|
const end = e.points[e.points.length - 1];
|
|
this.assertEqual(start[0], parent.x + W, "the link leaves the parent's right edge");
|
|
this.assertEqual(start[1], parent.y + H / 2, "every outgoing link leaves the middle of the right edge");
|
|
this.assertEqual(end[0], child.x, "the link enters the child's left edge");
|
|
this.assertEqual(end[1], child.y + H / 2, "every incoming link arrives at the middle of the left edge");
|
|
}
|
|
}
|
|
|
|
test_no_link_passes_behind_a_card() {
|
|
const layout = layoutTechnologies(TECHNOLOGIES);
|
|
const { W, H } = TECH_LAYOUT;
|
|
for (const run of segments(layout)) {
|
|
for (const [index, pos] of layout.placed) {
|
|
const left = pos.x, right = pos.x + W;
|
|
const top = pos.y, bottom = pos.y + H;
|
|
if (horizontal(run)) {
|
|
const xlo = Math.min(run.x1, run.x2);
|
|
const xhi = Math.max(run.x1, run.x2);
|
|
const crosses = run.y1 > top + 0.5 && run.y1 < bottom - 0.5 &&
|
|
xhi > left + 0.5 && xlo < right - 0.5;
|
|
this.assertFalse(crosses, `${run.label} crosses ${TECHNOLOGIES[index].id}`);
|
|
} else {
|
|
const ylo = Math.min(run.y1, run.y2);
|
|
const yhi = Math.max(run.y1, run.y2);
|
|
const crosses = run.x1 > left + 0.5 && run.x1 < right - 0.5 &&
|
|
yhi > top + 0.5 && ylo < bottom - 0.5;
|
|
this.assertFalse(crosses, `${run.label} crosses ${TECHNOLOGIES[index].id}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
test_unmet_prerequisites_recurse_to_the_roots() {
|
|
const hasBuilding = () => false;
|
|
const deep = unmetPrerequisites(indexOf("fusion_power"), TECHNOLOGIES, new Set(), hasBuilding);
|
|
this.assertTrue(deep.has(indexOf("space_program")));
|
|
this.assertTrue(deep.has(indexOf("quantum_computing")));
|
|
this.assertTrue(deep.has(indexOf("industrial_automation")), "the chain is followed to the roots");
|
|
|
|
// A met prerequisite stops the walk: its own requirements are satisfied.
|
|
const shallow = unmetPrerequisites(
|
|
indexOf("fusion_power"),
|
|
TECHNOLOGIES,
|
|
new Set([indexOf("industrial_automation")]),
|
|
hasBuilding
|
|
);
|
|
this.assertFalse(shallow.has(indexOf("industrial_automation")));
|
|
this.assertTrue(shallow.has(indexOf("space_program")));
|
|
this.assertTrue(shallow.has(indexOf("quantum_computing")));
|
|
}
|
|
}
|