1
0
Fork 0
polaris-task-force/tests/int/land-nav.int.spec.ts

140 lines
5.4 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
GRID_COLS,
GRID_ROWS,
LAND_NAV_DIFFICULTIES,
calculateScore,
calculateXp,
createCourse,
gridRef,
isLegCorrect,
isOptionCorrect,
isLandNavDifficulty,
} from "@/lib/land-nav";
function seededRng(seed: number) {
let state = seed;
return () => {
state = (state * 1664525 + 1013904223) % 4294967296;
return state / 4294967296;
};
}
describe("land-nav lib", () => {
it("guards the difficulty keys", () => {
expect(isLandNavDifficulty("waypoint")).toBe(true);
expect(isLandNavDifficulty("route")).toBe(true);
expect(isLandNavDifficulty("expedition")).toBe(true);
expect(isLandNavDifficulty("patrol")).toBe(false);
expect(isLandNavDifficulty(42)).toBe(false);
});
it("creates a valid course for every difficulty", () => {
for (const difficulty of ["waypoint", "route", "expedition"] as const) {
const config = LAND_NAV_DIFFICULTIES[difficulty];
const course = createCourse(difficulty, seededRng(1));
expect(course.legs).toHaveLength(config.legs);
expect(course.features).toHaveLength(config.features);
expect(course.cols).toBe(GRID_COLS);
expect(course.rows).toBe(GRID_ROWS);
const featureCells = new Set(course.features.map((f) => `${f.col},${f.row}`));
expect(featureCells.size).toBe(config.features);
for (const feature of course.features) {
expect(feature.col).toBeGreaterThanOrEqual(0);
expect(feature.col).toBeLessThan(GRID_COLS);
expect(feature.row).toBeGreaterThanOrEqual(0);
expect(feature.row).toBeLessThan(GRID_ROWS);
expect(feature.name.length).toBeGreaterThan(0);
}
for (const leg of course.legs) {
expect(leg.prompt.length).toBeGreaterThan(0);
expect(leg.answerCell.col).toBeGreaterThanOrEqual(0);
expect(leg.answerCell.col).toBeLessThan(GRID_COLS);
expect(leg.answerCell.row).toBeGreaterThanOrEqual(0);
expect(leg.answerCell.row).toBeLessThan(GRID_ROWS);
if (leg.kind === "azimuth") {
expect(leg.azimuth).toBeGreaterThanOrEqual(0);
expect(leg.azimuth).toBeLessThanOrEqual(359);
expect(leg.distanceM).toBeGreaterThan(0);
expect(leg.startCell).toBeDefined();
}
if (leg.kind === "identify") {
expect(leg.options).toHaveLength(4);
expect(new Set(leg.options).size).toBe(4);
}
}
}
});
it("produces 6-figure grid references", () => {
const course = createCourse("waypoint", seededRng(7));
const ref = gridRef(course, { col: 0, row: 0 });
expect(ref).toMatch(/^\d{6}$/);
expect(ref).toBe(
`${String(course.easting0).padStart(3, "0")}${String(course.northing0).padStart(3, "0")}`,
);
});
it("scores passes higher than fails and awards XP only on a pass", () => {
const passScore = calculateScore("route", 6, 60, true);
const failScore = calculateScore("route", 2, 60, false);
expect(passScore).toBeGreaterThan(failScore);
const faster = calculateScore("route", 6, 30, true);
expect(faster).toBeGreaterThanOrEqual(passScore);
expect(calculateXp(passScore, true)).toBeGreaterThanOrEqual(1);
expect(calculateXp(passScore, false)).toBe(0);
});
it("validates leg answers", () => {
const course = createCourse("waypoint", seededRng(3));
const locateLeg = course.legs.find((leg) => leg.kind === "locate");
const identifyLeg = course.legs.find((leg) => leg.kind === "identify");
expect(locateLeg).toBeDefined();
expect(identifyLeg).toBeDefined();
if (locateLeg) {
expect(isLegCorrect(locateLeg, locateLeg.answerCell)).toBe(true);
expect(isLegCorrect(locateLeg, { col: 0, row: 0 })).toBe(
locateLeg.answerCell.col === 0 && locateLeg.answerCell.row === 0,
);
}
if (identifyLeg) {
const answerFeature = course.features.find((f) => f.id === identifyLeg.answerFeatureId);
expect(answerFeature).toBeDefined();
expect(isOptionCorrect(identifyLeg, course, answerFeature!.name)).toBe(true);
const wrongOption = (identifyLeg.options ?? []).find(
(option) => option !== answerFeature!.name,
);
expect(isOptionCorrect(identifyLeg, course, wrongOption ?? "")).toBe(false);
}
});
it("generates declination legs on harder courses with a consistent conversion", () => {
let seen = 0;
for (let seed = 1; seed <= 40 && seen < 5; seed += 1) {
const course = createCourse("expedition", seededRng(seed));
const declLegs = course.legs.filter((leg) => leg.kind === "declination");
seen += declLegs.length;
for (const leg of declLegs) {
expect(leg.declination).not.toBe(0);
expect(leg.startCell).toBeDefined();
const radians = ((leg.azimuth ?? 0) * Math.PI) / 180;
const distanceCells = (leg.distanceM ?? 0) / 100;
const dx = Math.round(Math.sin(radians) * distanceCells);
const dy = -Math.round(Math.cos(radians) * distanceCells) || 0;
expect(leg.answerCell.col - leg.startCell!.col).toBe(dx);
expect(leg.answerCell.row - leg.startCell!.row).toBe(dy);
expect(leg.prompt).toMatch(/declination/i);
}
}
expect(seen).toBeGreaterThan(0);
});
it("never generates declination legs on the waypoint course", () => {
for (let seed = 1; seed <= 20; seed += 1) {
const course = createCourse("waypoint", seededRng(seed));
expect(course.legs.every((leg) => leg.kind !== "declination")).toBe(true);
}
});
});