From 9d5608f46e5746e5223b3ee931c99c8423385b8c Mon Sep 17 00:00:00 2001 From: Gulum Date: Fri, 5 Jun 2026 23:38:18 +0200 Subject: [PATCH] GEN-1: add locus/allele model (A C D E G P Sp Re, multi-allele C/E) Co-Authored-By: Claude Opus 4.8 (1M context) --- gerbil-manager-web/src/genetics/loci.ts | 76 +++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 gerbil-manager-web/src/genetics/loci.ts diff --git a/gerbil-manager-web/src/genetics/loci.ts b/gerbil-manager-web/src/genetics/loci.ts new file mode 100644 index 0000000..f983ed2 --- /dev/null +++ b/gerbil-manager-web/src/genetics/loci.ts @@ -0,0 +1,76 @@ +/** + * Locus / allele model for the German gerbil colour-genetics system. + * + * Identifiers are English/canonical; all user-facing texts live in de.ts. + * + * Each locus owns an ordered list of alleles, most-dominant first. The + * dominance order drives phenotype resolution and lets us pick the + * "expressed" allele of a genotype pair. + * + * Sources (German gerbil-breeder community): + * - rennmaus-info.jimdoweb.com/zucht/farb-genloci-bei-rennmäusen + * - de.wikibooks.org/wiki/Die_Rennmaus/_Farbvarianten_und_Farbgenetik + */ + +/** Canonical locus keys, in conventional display order. */ +export const LOCUS_ORDER = ['A', 'C', 'D', 'E', 'G', 'P', 'Sp', 'Re'] as const +export type LocusKey = (typeof LOCUS_ORDER)[number] + +export interface LocusDef { + readonly key: LocusKey + /** German locus name (for tooltips/UI; kept here as reference, mirror in de.ts). */ + readonly nameDe: string + /** Allele symbols, most-dominant first. Symbols are the canonical serialised form. */ + readonly alleles: readonly string[] +} + +/** + * The eight loci. Multi-allele C series modelled explicitly: + * C = full colour (dominant) + * cchm = marked colourpoint ("chinchilla-medium") — more residual pigment + * ch = himalayan colourpoint — least pigment + * E series: + * E = full extension + * ef = Schimmel/roan (progressive whitening) + * e = Fox (suppresses eumelanin) + * Sp/Re are dominant markers, lethal/semi-lethal when homozygous (see lethality.ts): + * Sp = Schecke (checkered); checkered animals are always Spsp, SpSp dies in utero. + * Re = Rex (curly coat); rex animals are Re-, ReRe is semi-lethal. + */ +export const LOCI: Readonly> = { + A: { key: 'A', nameDe: 'Agouti', alleles: ['A', 'a'] }, + C: { key: 'C', nameDe: 'Farbe (Albino-Serie)', alleles: ['C', 'cchm', 'ch'] }, + D: { key: 'D', nameDe: 'Verdünnung (Dilute)', alleles: ['D', 'd'] }, + E: { key: 'E', nameDe: 'Extension (Fox/Schimmel)', alleles: ['E', 'ef', 'e'] }, + G: { key: 'G', nameDe: 'Grau (Underwhite)', alleles: ['G', 'g'] }, + P: { key: 'P', nameDe: 'Rotaugenaufhellung (Pink-Eye)', alleles: ['P', 'p'] }, + Sp: { key: 'Sp', nameDe: 'Schecke', alleles: ['Sp', 'sp'] }, + Re: { key: 'Re', nameDe: 'Rex', alleles: ['Re', 're'] }, +} + +/** Set of all valid allele symbols, longest-first (for maximal-munch parsing). */ +export const ALLELE_SYMBOLS: readonly string[] = Array.from( + new Set(LOCUS_ORDER.flatMap((k) => LOCI[k].alleles)), +).sort((a, b) => b.length - a.length) + +/** Map every allele symbol to the locus it belongs to. */ +export const ALLELE_TO_LOCUS: Readonly> = Object.fromEntries( + LOCUS_ORDER.flatMap((k) => LOCI[k].alleles.map((al) => [al, k] as const)), +) + +/** Dominance rank within a locus (0 = most dominant). */ +export function dominanceRank(locus: LocusKey, allele: string): number { + const idx = LOCI[locus].alleles.indexOf(allele) + if (idx < 0) throw new Error(`Unknown allele "${allele}" for locus ${locus}`) + return idx +} + +/** The dominant (expressed) allele of a pair at a locus. */ +export function dominantAllele(locus: LocusKey, a: string, b: string): string { + return dominanceRank(locus, a) <= dominanceRank(locus, b) ? a : b +} + +/** Wild-type homozygous allele (the most dominant) for a locus. */ +export function wildTypeAllele(locus: LocusKey): string { + return LOCI[locus].alleles[0] +}