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

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")));
}
}