/** * Farbschlag catalog: genotype -> German variety name. * * A catalog entry matches when every locus it SPECIFIES equals the genotype's * phenotype token (see phenotype.ts). Unspecified loci are wildcards. The first * matching base-colour entry wins; Schecke/Rex are appended as modifiers. * * Coverage is partial by design — extend BASE_COLORS as more genotype tables * are confirmed. Unknown genotypes fall back to "Unbekannter Farbschlag" plus * the raw genotype string (handled by the caller / index.ts). * * Sourced from the German gerbil-breeder community: * - de.wikibooks.org/wiki/Die_Rennmaus/_Farbvarianten_und_Farbgenetik (black/red-eyed tables) * - rennmaus-info.jimdoweb.com (loci & colourpoint series) * - clan-of-topolino.ch, rennmauswelten.jimdofree.com (Zobel, Schimmel) */ import { LOCUS_ORDER, type LocusKey } from './loci' import { makeGenotype, toDisplayString, wildType, type AllelePair, type Genotype } from './genotype' import { phenotypeTokens, type PhenotypeTokens } from './phenotype' export interface FarbschlagEntry { /** German variety name. */ readonly name: string /** Common English name (reference only; UI is German). */ readonly english?: string /** Required phenotype tokens; omitted loci are wildcards. */ readonly tokens: Partial> } /** * Base colours (the A/C/D/E/G/P phenotype). Ordered most-specific first so the * earliest match wins. Colourpoint (C != "C") entries are naturally disjoint * from full-colour ones because the C token differs. */ export const BASE_COLORS: readonly FarbschlagEntry[] = [ // ── Colourpoint / C-series (black & red eyed) ── { name: 'Pink Eyed White (PEW)', english: 'Pink Eyed White', tokens: { C: 'ch', P: 'p' } }, { name: 'Hermelin', english: 'Dark Tailed White', tokens: { A: 'a', C: 'ch', D: 'D', P: 'P' } }, { name: 'Himalaya', english: 'Himalayan', tokens: { A: 'A', C: 'ch', D: 'D', P: 'P' } }, { name: 'Zobel', english: 'Sable', tokens: { A: 'a', C: 'cchm', D: 'D', E: 'E', G: 'g', P: 'P' } }, // ── Schimmel / roan (ef ef) ── { name: 'Schwarzschimmel', english: 'Black Roan', tokens: { C: 'C', D: 'D', E: 'ef', G: 'G', P: 'P' } }, { name: 'Rotaugenschimmel', english: 'Red-Eyed Roan', tokens: { C: 'C', D: 'D', E: 'ef', G: 'G', P: 'p' } }, // ── Black-eyed full colour (P) ── { name: 'Agouti', english: 'Golden Agouti', tokens: { A: 'A', C: 'C', D: 'D', E: 'E', G: 'G', P: 'P' } }, { name: 'Schwarz', english: 'Black', tokens: { A: 'a', C: 'C', D: 'D', E: 'E', G: 'G', P: 'P' } }, { name: 'Silberagouti', english: 'Grey Agouti', tokens: { A: 'A', C: 'C', D: 'D', E: 'E', G: 'g', P: 'P' } }, { name: 'Anthrazit', english: 'Slate', tokens: { A: 'a', C: 'C', D: 'D', E: 'E', G: 'g', P: 'P' } }, { name: 'Algierfuchs', english: 'Dark-Eyed Honey', tokens: { A: 'A', C: 'C', D: 'D', E: 'e', G: 'G', P: 'P' } }, { name: 'Blau', english: 'Blue', tokens: { A: 'a', C: 'C', D: 'd', E: 'E', G: 'G', P: 'P' } }, // ── Red-eyed full colour (p) ── { name: 'Gold', english: 'Argente Golden', tokens: { A: 'A', C: 'C', D: 'D', E: 'E', G: 'G', P: 'p' } }, { name: 'Platin', english: 'Lilac', tokens: { A: 'a', C: 'C', D: 'D', E: 'E', G: 'G', P: 'p' } }, { name: 'Goldfuchs', english: 'Yellow Fox', tokens: { A: 'A', C: 'C', D: 'D', E: 'e', G: 'G', P: 'p' } }, { name: 'Rotfuchs', english: 'Argente Nutmeg', tokens: { A: 'a', C: 'C', D: 'D', E: 'e', G: 'G', P: 'p' } }, { name: 'dd Gold', english: 'dd Argente Golden', tokens: { A: 'A', C: 'C', D: 'd', E: 'E', G: 'G', P: 'p' } }, { name: 'dd Platin', english: 'dd Lilac', tokens: { A: 'a', C: 'C', D: 'd', E: 'E', G: 'G', P: 'p' } }, ] export const UNKNOWN_FARBSCHLAG = 'Unbekannter Farbschlag' export interface FarbschlagMatch { /** German variety name (with Schecke/Rex modifiers appended), or the fallback. */ readonly name: string /** The matched base entry, if any. */ readonly base: FarbschlagEntry | null /** True when no base colour matched (name is the fallback). */ readonly unknown: boolean } function matches(tokens: PhenotypeTokens, entry: FarbschlagEntry): boolean { return (Object.keys(entry.tokens) as LocusKey[]).every( (locus) => tokens[locus] === entry.tokens[locus], ) } /** Resolve a genotype to its German Farbschlag (with Schecke/Rex modifiers). */ export function farbschlagFor(g: Genotype): FarbschlagMatch { const tokens = phenotypeTokens(g) const base = BASE_COLORS.find((e) => matches(tokens, e)) ?? null const modifiers: string[] = [] if (tokens.Sp === 'Sp') modifiers.push('Schecke') if (tokens.Re === 'Re') modifiers.push('Rex') if (!base) { return { name: UNKNOWN_FARBSCHLAG, base: null, unknown: true } } const name = [base.name, ...modifiers].join(' ') return { name, base, unknown: false } } /** * Convenience: genotype -> German Farbschlag name (with Schecke/Rex modifiers), * or "Unbekannter Farbschlag". This is the DENORMALIZATION contract for DATA-1: * the backend mirrors this mapping into a stored Farbschlag column (recomputed * whenever the genotype changes) so Gridify list filtering/sorting never has to * parse genotype strings in SQL. The catalog (BASE_COLORS) is self-contained in * this file so it can be ported/kept in sync with the backend. */ export function genotypeToFarbschlag(g: Genotype): string { return farbschlagFor(g).name } /** * A representative full genotype for a catalog entry: each specified locus is * homozygous for its token allele; unspecified loci take the wild-type allele. * This is the entry's CanonicalGenotype for DB seeding. */ export function representativeGenotype(entry: FarbschlagEntry): Genotype { const base = wildType() const out = {} as Record for (const locus of LOCUS_ORDER) { const token = entry.tokens[locus] out[locus] = token ? [token, token] : base[locus] } return makeGenotype(out) } /** * DB seed view for DATA-2's ColorVariety table. Each row mirrors the table * shape (Name, CanonicalGenotype, SortOrder). SortOrder = catalog position. * * IMPORTANT: `name` values become DB keys the UI filters on — renames are * BREAKING changes and must be routed through god. Adding new varieties is safe. */ export interface ColorVarietySeed { readonly name: string readonly english?: string readonly canonicalGenotype: string readonly sortOrder: number } export const CATALOG: readonly ColorVarietySeed[] = BASE_COLORS.map((entry, i) => ({ name: entry.name, english: entry.english, canonicalGenotype: toDisplayString(representativeGenotype(entry)), sortOrder: i, })) /** Number of base-colour varieties currently catalogued. */ export const CATALOG_SIZE = BASE_COLORS.length