Something went wrong. Try again.
A functional toolkit for authoring resource-leveling algorithms — compose constraint and scoring blocks, enumerate feasible schedules
Something went wrong. Try again.
4.6 kB · 150 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151import { test, expect, describe } from "bun:test";
import { canFollow, cardinalityMessage, FAMILY_CARDINALITY, FAMILY_FOLLOWS, FAMILY_ORDER, sortIntoBandOrder,} from "../../src/level-blocks/composition.ts";import type { BlockFamily } from "../../src/level-blocks/registry.ts";
const FAMILIES: readonly BlockFamily[] = [ "constraint", "scorer", "search", "collector",];
describe("canFollow — the family grammar", () => { // The full truth table, mirroring connections.ts NEXT_ALLOWED. When a // vendor sync deletes its copy, this table is the contract it inherits. const EXPECTED: Record<BlockFamily, Record<BlockFamily, boolean>> = { constraint: { constraint: true, scorer: true, search: true, collector: false, }, scorer: { constraint: false, scorer: true, search: true, collector: false }, search: { constraint: false, scorer: false, search: false, collector: true, }, collector: { constraint: false, scorer: false, search: false, collector: true, }, };
for (const above of FAMILIES) { for (const below of FAMILIES) { test(`${below} may ${EXPECTED[above][below] ? "" : "not "}follow ${above}`, () => { expect(canFollow(above, below)).toBe(EXPECTED[above][below]); }); } }
test("FAMILY_FOLLOWS agrees with canFollow", () => { for (const above of FAMILIES) { for (const below of FAMILIES) { expect(FAMILY_FOLLOWS[above].includes(below)).toBe( canFollow(above, below), ); } } });
test("a scorer physically cannot sit above a constraint", () => { expect(canFollow("scorer", "constraint")).toBe(false); });
test("only collectors may follow the search block", () => { expect(FAMILY_FOLLOWS.search).toEqual(["collector"]); });});
describe("FAMILY_CARDINALITY", () => { test("exactly one search, everything else open", () => { expect(FAMILY_CARDINALITY.search).toEqual({ min: 1, max: 1 }); expect(FAMILY_CARDINALITY.constraint).toEqual({ min: 0, max: null }); expect(FAMILY_CARDINALITY.scorer).toEqual({ min: 0, max: null }); expect(FAMILY_CARDINALITY.collector).toEqual({ min: 0, max: null }); });
test("cardinalityMessage preserves the validateGraph search wording", () => { expect(cardinalityMessage("search", FAMILY_CARDINALITY.search, 0)).toBe( "Expected exactly one search-family block, found 0", ); expect(cardinalityMessage("search", FAMILY_CARDINALITY.search, 2)).toBe( "Expected exactly one search-family block, found 2", ); });
test("cardinalityMessage phrasing generalizes to other bounds", () => { expect(cardinalityMessage("scorer", { min: 1, max: null }, 0)).toBe( "Expected at least 1 scorer-family blocks, found 0", ); expect(cardinalityMessage("collector", { min: 0, max: 2 }, 3)).toBe( "Expected between 0 and 2 collector-family blocks, found 3", ); });});
describe("sortIntoBandOrder", () => { const familyOf = (item: { family: BlockFamily | undefined; }): BlockFamily | undefined => item.family;
test("sorts a shuffled set into canonical band order", () => { const items = [ { id: "collector", family: "collector" as const }, { id: "search", family: "search" as const }, { id: "scorer", family: "scorer" as const }, { id: "constraint", family: "constraint" as const }, ]; expect(sortIntoBandOrder(items, familyOf).map((i) => i.id)).toEqual([ "constraint", "scorer", "search", "collector", ]); });
test("is stable within a band", () => { const items = [ { id: "s2", family: "scorer" as const }, { id: "c1", family: "constraint" as const }, { id: "s1", family: "scorer" as const }, ]; expect(sortIntoBandOrder(items, familyOf).map((i) => i.id)).toEqual([ "c1", "s2", "s1", ]); });
test("unknown families sink to the end", () => { const items = [ { id: "unknown", family: undefined }, { id: "constraint", family: "constraint" as const }, ]; expect(sortIntoBandOrder(items, familyOf).map((i) => i.id)).toEqual([ "constraint", "unknown", ]); });
test("every adjacent pair of a sorted single-search set satisfies canFollow", () => { const items = FAMILY_ORDER.map((family) => ({ family })); const sorted = sortIntoBandOrder(items, (i) => i.family); for (let i = 1; i < sorted.length; i++) { expect(canFollow(sorted[i - 1]!.family, sorted[i]!.family)).toBe(true); } });});