Botanica/packages/genetics/src/genetics.test.ts

186 lines
6.3 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { execFileSync } from "node:child_process";
import {
breed,
canBreed,
checkCompatibility,
classifyRarity,
createRng,
exampleGenome,
loci,
resolvePhenotype,
validateGenome,
velvetleaf,
type BreedingParent,
} from "./index.js";
const a: BreedingParent = {
id: "a",
speciesId: "velvetleaf",
genome: exampleGenome,
generation: 2,
genomeVersion: 1,
};
const b: BreedingParent = { ...a, id: "b", generation: 4 };
const off = { alleleMutationRate: 0, namedMutationRate: 0 };
describe("genetics v1", () => {
it("replays Mulberry32 bit-identically, including a separate Node process", () => {
const rng = createRng(1);
expect([rng(), rng()]).toEqual([0.6270739405881613, 0.002735721180215478]);
const first = breed(a, b, -81);
expect(breed(a, b, -81)).toEqual(first);
const script = `import {breed} from '@botanica/genetics';console.log(JSON.stringify(breed(${JSON.stringify(a)},${JSON.stringify(b)},-81)))`;
expect(
JSON.parse(
execFileSync(process.execPath, ["--input-type=module", "-e", script], {
encoding: "utf8",
}),
),
).toEqual(first);
expect(first.parents).toEqual(["a", "b"]);
expect(first.generation).toBe(5);
expect(first.engineVersion).toBe(1);
});
it("produces the 1:2:1 PG cross over 10,000 independent seeds", () => {
const counts = { PP: 0, PG: 0, GG: 0 };
for (let seed = 0; seed < 10000; seed++) {
const pair = breed(a, b, seed, 1, off).genome.pigment;
const key = pair[0] === pair[1] ? pair.join("") : "PG";
counts[key as keyof typeof counts]++;
}
console.log("M1 PG cross (n=10000):", counts);
expect(Math.abs(counts.PP / 10000 - 0.25)).toBeLessThanOrEqual(0.02);
expect(Math.abs(counts.PG / 10000 - 0.5)).toBeLessThanOrEqual(0.02);
expect(Math.abs(counts.GG / 10000 - 0.25)).toBeLessThanOrEqual(0.02);
});
it("honors dominance at every locus and rejects incomplete or untrusted genomes", () => {
expect(resolvePhenotype("velvetleaf", exampleGenome)).toMatchObject({
pigment: "purple",
leafShape: "lobed",
leafPattern: "variegated",
growthHabit: "bushy",
flowerColor: "red",
flowerDensity: "high",
});
for (const locus of loci) {
const { dominance, labels } = velvetleaf.loci[locus];
for (const left of dominance)
for (const right of dominance) {
const genome = { ...exampleGenome, [locus]: [left, right] };
const winner = dominance.find((v) => v === left || v === right)!;
expect(resolvePhenotype("velvetleaf", genome)[locus]).toBe(
(labels as Record<string, string>)[winner],
);
}
}
expect(() =>
validateGenome("velvetleaf", { ...exampleGenome, pigment: ["X", "G"] }),
).toThrow();
expect(() =>
validateGenome("velvetleaf", { pigment: ["P", "G"] }),
).toThrow();
expect(() =>
validateGenome("velvetleaf", { ...exampleGenome, rarity: "Mythic" }),
).toThrow();
expect(() =>
resolvePhenotype("velvetleaf", exampleGenome, ["unknown"]),
).toThrow();
expect(() =>
resolvePhenotype("velvetleaf", exampleGenome, [], 2),
).toThrow();
});
it("requires compatibility groups, mutual approval and an explicit hybrid rule", () => {
expect(canBreed("velvetleaf", "velvetleaf").compatible).toBe(true);
const other = { ...velvetleaf, id: "other" };
expect(checkCompatibility(velvetleaf, other)).toMatchObject({
compatible: false,
code: "INCOMPATIBLE_SPECIES",
});
expect(
checkCompatibility(
{
...velvetleaf,
allowedPartners: ["other"],
hybridOffspring: { other: "hybrid" },
},
{ ...other, allowedPartners: ["velvetleaf"] },
),
).toMatchObject({ compatible: true, offspringSpeciesId: "hybrid" });
expect(
checkCompatibility(
{
...velvetleaf,
allowedPartners: ["other"],
hybridOffspring: { other: "hybrid" },
},
{
...other,
compatibilityGroup: "alien",
allowedPartners: ["velvetleaf"],
},
),
).toMatchObject({ compatible: false });
expect(() => breed(a, { ...b, speciesId: "unknown" }, 1)).toThrow();
});
it("has exact zero/one mutation boundaries and logs only different valid alleles", () => {
const before = JSON.stringify(a);
const clean = breed(a, b, 42, 1, off);
expect(clean.mutationEvents).toEqual([]);
const changed = breed(a, b, 42, 1, {
alleleMutationRate: 1,
namedMutationRate: 1,
});
expect(changed.mutationEvents).toHaveLength(13);
expect(changed.mutations).toEqual(["golden_veins"]);
validateGenome("velvetleaf", changed.genome);
for (const event of changed.mutationEvents.filter(
(e) => e.type === "allele",
))
expect(event.from).not.toBe(event.to);
expect(changed.visualSeed).toBe(clean.visualSeed);
expect(JSON.stringify(a)).toBe(before);
expect(() => breed(a, b, 0, 1, { alleleMutationRate: 1.01 })).toThrow();
expect(() => breed(a, b, 2147483648)).toThrow();
});
it("derives rarity from explicit catalog traits with capped unusual contributions", () => {
const base = resolvePhenotype("velvetleaf", {
pigment: ["P", "P"],
leafShape: ["L", "L"],
leafPattern: ["S", "S"],
growthHabit: ["T", "T"],
flowerColor: ["B", "B"],
flowerDensity: ["H", "H"],
});
expect(classifyRarity(base)).toMatchObject({
score: 0,
tier: "Common",
rarityPolicyVersion: 1,
});
expect(classifyRarity({ ...base, leafPattern: "marbled" })).toMatchObject({
score: 20,
tier: "Uncommon",
});
expect(
classifyRarity({ ...base, mutations: ["golden_veins"] }),
).toMatchObject({ score: 25, tier: "Rare" });
expect(
classifyRarity({
...base,
pigment: "yellow",
leafShape: "oval",
growthHabit: "compact",
leafPattern: "variegated",
mutations: ["golden_veins"],
}),
).toMatchObject({ score: 100, tier: "Mythic" });
expect(
classifyRarity({
...base,
pigment: "green",
leafShape: "oval",
growthHabit: "compact",
flowerDensity: "low",
flowerColor: "white",
}).score,
).toBe(40);
});
});