From 1e7dc8f91db406c929277eaa53752f440a6097fd Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 6 Jun 2026 10:25:48 +0200 Subject: [PATCH 1/4] =?UTF-8?q?GEN-3a:=20Uw=3DG=20alias,=20second=20spotti?= =?UTF-8?q?ng=20locus=20Sls=20(SlSl=20lethal),=20Sp=C3=97Sls=20Superscheck?= =?UTF-8?q?e,=20flag/metadata=20token=20tolerance=20(dea/WFNZ/DP/RV/GV)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gerbil-manager-web/src/genetics/genotype.ts | 84 ++++++++++++++++++-- gerbil-manager-web/src/genetics/lethality.ts | 43 ++++++++-- gerbil-manager-web/src/genetics/loci.ts | 18 +++-- gerbil-manager-web/src/genetics/warnings.ts | 4 + 4 files changed, 128 insertions(+), 21 deletions(-) diff --git a/gerbil-manager-web/src/genetics/genotype.ts b/gerbil-manager-web/src/genetics/genotype.ts index 0138cc0..3702b9f 100644 --- a/gerbil-manager-web/src/genetics/genotype.ts +++ b/gerbil-manager-web/src/genetics/genotype.ts @@ -64,18 +64,28 @@ export function makeGenotype(input: Record): Genotype { export function wildType(): Genotype { const out = {} as Record for (const locus of LOCUS_ORDER) { - // Wild-type is homozygous for the most dominant allele, EXCEPT the - // marker loci Sp/Re whose wild form is the recessive (unmarked) allele. + // Wild-type is homozygous for the most dominant allele, EXCEPT the marker + // loci Sp/Re/Sls whose wild form is the recessive (unmarked) allele. const alleles = LOCI[locus].alleles - const a = locus === 'Sp' || locus === 'Re' ? alleles[alleles.length - 1] : alleles[0] + const marker = locus === 'Sp' || locus === 'Re' || locus === 'Sls' + const a = marker ? alleles[alleles.length - 1] : alleles[0] out[locus] = [a, a] } return out } -/** Compact display string, e.g. "Aa CC Dd EE GG Pp spsp rere". */ +/** + * Compact display string, e.g. "Aa CC Dd EE GG Pp spsp rere". + * The Sls locus is OMITTED when wild-type (sl/sl) so legacy 8-locus strings and + * the colour catalog stay byte-identical; it only appears for WP/Sls carriers + * (e.g. "… spsp rere Slsl"). Round-trips: a missing Sls re-parses to sl/sl. + */ export function toDisplayString(g: Genotype): string { - return LOCUS_ORDER.map((locus) => g[locus][0] + g[locus][1]).join(' ') + return LOCUS_ORDER.filter( + (locus) => locus !== 'Sls' || !(g.Sls[0] === 'sl' && g.Sls[1] === 'sl'), + ) + .map((locus) => g[locus][0] + g[locus][1]) + .join(' ') } /** Stable JSON-storable object (already the in-memory shape; returned as a copy). */ @@ -114,14 +124,72 @@ function splitToken(token: string): [string, string] { } /** - * Parse a display string ("Aa CC Dd EE GG Pp Spsp rere") back into a Genotype. - * Tokens may be given in any order; each token must belong to a distinct locus. + * GEN-3a: tokens that are NOT genotype loci — health/provenance metadata that may + * appear in a herd-book genotype string. Stripped on parse (see extractGenotypeFlags). + * - dea/Dea/taub = deafness flag (after spsp); DP/DarkPatch = non-Mendelian patch flag + * - WFNZ/RV/GV = provenance/breeding-method annotations + */ +const FLAG_TOKENS = new Set(['DP', 'DarkPatch', 'dea', 'Dea', 'taub', 'WFNZ', 'RV', 'GV']) + +/** + * Normalize one whitespace-token to canonical allele symbols, or null if it is a + * non-genotype flag/metadata token (to be stripped): + * - Uw/uw -> G/g (international Underwhite == German Grey locus) + * - S(l)/s(l) -> Sl/sl (second spotting locus notation) + * - WP -> Slsl (WP is the visible S(l)s(l) heterozygote) + */ +function normalizeToken(tok: string): string | null { + if (FLAG_TOKENS.has(tok)) return null + let t = tok + if (t === 'WP') t = 'Slsl' + t = t.replace(/S\(l\)/g, 'Sl').replace(/s\(l\)/g, 'sl') + t = t.replace(/Uw/g, 'G').replace(/uw/g, 'g') + return t +} + +/** + * Canonical normalized genotype string (flags/metadata removed, Uw/S(l)/WP + * resolved). Exported so the import pipeline (GEN-3b) can mirror this exactly. + */ +export function normalizeGenotypeString(input: string): string { + return input + .trim() + .split(/\s+/) + .filter(Boolean) + .map(normalizeToken) + .filter((t): t is string => t !== null) + .join(' ') +} + +/** + * Extract non-Punkett flags from a raw genotype string: deafness (dea/taub = + * deaf, Dea = hearing) and provenance/pattern tags (WFNZ/RV/GV/DP). + */ +export function extractGenotypeFlags(input: string): { deaf?: boolean; tags: string[] } { + const tokens = input.trim().split(/\s+/).filter(Boolean) + let deaf: boolean | undefined + const tags: string[] = [] + for (const tok of tokens) { + if (tok === 'dea' || tok === 'taub') deaf = true + else if (tok === 'Dea') deaf = false + else if (tok === 'DP' || tok === 'DarkPatch' || tok === 'WFNZ' || tok === 'RV' || tok === 'GV') + tags.push(tok) + } + return { deaf, tags } +} + +/** + * Parse a display string ("Aa CC Dd EE GG Pp Spsp rere [Slsl]") back into a + * Genotype. Tokens may be in any order; each must belong to a distinct locus. + * Uw/S(l)/WP are normalized and flag/metadata tokens (dea, WFNZ, …) are stripped. * Missing loci default to wild-type. */ export function fromDisplayString(input: string): Genotype { const tokens = input.trim().split(/\s+/).filter(Boolean) const acc = {} as Record - for (const token of tokens) { + for (const raw of tokens) { + const token = normalizeToken(raw) + if (token === null) continue // flag/metadata token — not a locus const [a, b] = splitToken(token) const refAllele = a === WILDCARD ? b : a if (refAllele === WILDCARD) { diff --git a/gerbil-manager-web/src/genetics/lethality.ts b/gerbil-manager-web/src/genetics/lethality.ts index 993698d..647164f 100644 --- a/gerbil-manager-web/src/genetics/lethality.ts +++ b/gerbil-manager-web/src/genetics/lethality.ts @@ -25,6 +25,7 @@ interface LethalRule { const LETHAL_RULES: readonly LethalRule[] = [ { locus: 'Sp', allele: 'Sp', kind: 'lethal', warning: GeneticsWarningCode.ScheckeLethal }, + { locus: 'Sls', allele: 'Sl', kind: 'lethal', warning: GeneticsWarningCode.SlsLethal }, { locus: 'Re', allele: 'Re', kind: 'semi', warning: GeneticsWarningCode.RexSemiLethal }, ] @@ -67,14 +68,23 @@ export function applyLethality(dist: DistEntry[]): LethalityResult { ? survivors : survivors.map((e) => ({ value: e.value, probability: divide(e.probability, survivingMass) })) - if (lethalMass.num > 0) { - warnings.push({ - code: GeneticsWarningCode.ScheckeLethal, - detail: { - youngLostFraction: toString(lethalMass), - youngLostPercent: Number(((lethalMass.num / lethalMass.den) * 100).toFixed(2)), - }, - }) + // One lethal warning PER lethal rule that actually removed young (so SpSp -> + // ScheckeLethal and S(l)S(l) -> SlsLethal are reported distinctly). + for (const rule of LETHAL_RULES) { + if (rule.kind !== 'lethal') continue + const mass = dist.reduce( + (acc, e) => (isHomozygous(e.value, rule.locus, rule.allele) ? add(acc, e.probability) : acc), + ZERO, + ) + if (mass.num > 0) { + warnings.push({ + code: rule.warning, + detail: { + youngLostFraction: toString(mass), + youngLostPercent: Number(((mass.num / mass.den) * 100).toFixed(2)), + }, + }) + } } // Semi-lethal: warn if any surviving genotype is homozygous for a semi-lethal allele. @@ -95,5 +105,22 @@ export function applyLethality(dist: DistEntry[]): LethalityResult { } } + // Superschecke: surviving young carrying BOTH spotting markers (Sp present and + // S(l) present) are very-high-white and deafness-prone — info warning. + const superMass = distribution.reduce((acc, e) => { + const hasSp = e.value.Sp.includes('Sp') + const hasSl = e.value.Sls.includes('Sl') + return hasSp && hasSl ? add(acc, e.probability) : acc + }, ZERO) + if (superMass.num > 0) { + warnings.push({ + code: GeneticsWarningCode.SuperscheckeDeaf, + detail: { + affectedFraction: toString(superMass), + affectedPercent: Number(((superMass.num / superMass.den) * 100).toFixed(2)), + }, + }) + } + return { distribution, warnings } } diff --git a/gerbil-manager-web/src/genetics/loci.ts b/gerbil-manager-web/src/genetics/loci.ts index f983ed2..990e803 100644 --- a/gerbil-manager-web/src/genetics/loci.ts +++ b/gerbil-manager-web/src/genetics/loci.ts @@ -12,8 +12,12 @@ * - 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 +/** + * Canonical locus keys, in conventional display order. Sls (second spotting + * locus) is appended LAST so legacy 8-locus genotype strings still parse — a + * missing Sls token defaults to wild-type sl/sl. + */ +export const LOCUS_ORDER = ['A', 'C', 'D', 'E', 'G', 'P', 'Sp', 'Re', 'Sls'] as const export type LocusKey = (typeof LOCUS_ORDER)[number] export interface LocusDef { @@ -33,9 +37,12 @@ export interface LocusDef { * 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. + * Sp/Re/Sls 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. + * Sls = second spotting locus (S(l), WP/Minimalschecke). S(l)s(l) het = the WP + * phenotype; S(l)S(l) homozygous = lethal (Rumpback/megacolon). Sp + Sls + * together => Superschecke (very high white, deafness-prone). */ export const LOCI: Readonly> = { A: { key: 'A', nameDe: 'Agouti', alleles: ['A', 'a'] }, @@ -46,6 +53,7 @@ export const LOCI: Readonly> = { 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'] }, + Sls: { key: 'Sls', nameDe: 'Zweite Scheckung (WP)', alleles: ['Sl', 'sl'] }, } /** Set of all valid allele symbols, longest-first (for maximal-munch parsing). */ diff --git a/gerbil-manager-web/src/genetics/warnings.ts b/gerbil-manager-web/src/genetics/warnings.ts index 0f35238..5a22698 100644 --- a/gerbil-manager-web/src/genetics/warnings.ts +++ b/gerbil-manager-web/src/genetics/warnings.ts @@ -10,6 +10,10 @@ export const GeneticsWarningCode = { ScheckeLethal: 'SCHECKE_LETHAL', /** Rex × Rex: ReRe is semi-lethal; reduced viability of homozygous young. */ RexSemiLethal: 'REX_SEMI_LETHAL', + /** WP × WP: S(l)S(l) is prenatal-lethal (Rumpback/megacolon); fewer live young. */ + SlsLethal: 'SLS_LETHAL', + /** Sp + Sls together -> Superschecke: very high white, deafness-prone (info). */ + SuperscheckeDeaf: 'SUPERSCHECKE_DEAF', } as const export type GeneticsWarningCode = From a0b720df7392f1cd3a66a9de321e8170e88c0e79 Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 6 Jun 2026 10:25:48 +0200 Subject: [PATCH 2/4] =?UTF-8?q?GEN-3a:=20Schimmel=20catalog=20fix=20(breed?= =?UTF-8?q?er=20C5)=20=E2=80=94=20remove=20Schwarzschimmel,=20efef=20base?= =?UTF-8?q?=3DOrangeschimmel,=20Silberschimmel=20wins=20efef-gg,=20+CP-Ora?= =?UTF-8?q?ngeschimmel;=20regenerate=20ColorVariety=20seed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gerbil-manager-web/src/genetics/catalog.ts | 16 +- .../genetics/colorVarietySeed.generated.json | 896 +++++++++--------- 2 files changed, 468 insertions(+), 444 deletions(-) diff --git a/gerbil-manager-web/src/genetics/catalog.ts b/gerbil-manager-web/src/genetics/catalog.ts index a9a5f3a..7cd7c88 100644 --- a/gerbil-manager-web/src/genetics/catalog.ts +++ b/gerbil-manager-web/src/genetics/catalog.ts @@ -41,12 +41,14 @@ export interface FarbschlagEntry { * portal collision group. */ export const BASE_COLORS: readonly FarbschlagEntry[] = [ - // ── Frozen 18 (names are DB-key contract; do not rename) ── + // ── Frozen names (DB-key contract; do not rename) ── + // GEN-3a: 'Schwarzschimmel' REMOVED (breeder C5: no such variety; efef base is + // Orangeschimmel — see the GEN-2 block below). This was an authorized exception + // to the frozen-name rule; the ColorVariety seed drops it too. { name: 'Pink Eyed White (PEW)', english: 'Pink Eyed White', tokens: { C: 'ch', P: 'p' }, image: 'rotaugen-weiss-pew-d-sep-e-sep.jpg' }, { name: 'Hermelin', english: 'Dark Tailed White', tokens: { A: 'a', C: 'ch', D: 'D', P: 'P' }, image: 'hermelin.jpeg' }, { name: 'Himalaya', english: 'Himalayan', tokens: { A: 'A', C: 'ch', D: 'D', P: 'P' }, image: 'himalaya.jpg' }, { name: 'Zobel', english: 'Sable', tokens: { A: 'a', C: 'cchm', D: 'D', E: 'E', G: 'g', P: 'P' }, image: 'zobel.jpeg' }, - { 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' }, image: 'rotaugen-schimmel.jpg' }, { name: 'Agouti', english: 'Golden Agouti', tokens: { A: 'A', C: 'C', D: 'D', E: 'E', G: 'G', P: 'P' }, image: 'agouti-mit-erklaerung-der-genloci.JPG' }, { name: 'Schwarz', english: 'Black', tokens: { A: 'a', C: 'C', D: 'D', E: 'E', G: 'G', P: 'P' }, image: 'schwarz.jpg' }, @@ -72,7 +74,8 @@ export const BASE_COLORS: readonly FarbschlagEntry[] = [ { name: 'Siam (Marder-Hell)', tokens: { A: 'a', C: 'cchm', D: 'D', E: 'E', G: 'G', P: 'P' }, image: 'siam-marder-hell.JPG' }, { name: 'Polarfuchs', tokens: { A: 'A', C: 'C', D: 'D', E: 'e', G: 'g', P: 'P' }, image: 'polarfuchs.jpg' }, { name: 'Saphir', tokens: { A: 'a', C: 'C', D: 'D', E: 'E', G: 'G', P: 'p' }, image: 'saphir.jpg' }, - { name: 'Schimmel (Orangeschimmel)', tokens: { C: 'C', D: 'D', E: 'ef', G: 'G', P: 'P' }, image: 'schimmel-orangeschimmel.jpg' }, + // GEN-3a: efef base (otherwise wild C/D/G/P) = Orangeschimmel (breeder C5). + { name: 'Orangeschimmel', tokens: { C: 'C', D: 'D', E: 'ef', G: 'G', P: 'P' }, image: 'schimmel-orangeschimmel.jpg' }, { name: 'Topas', tokens: { A: 'A', C: 'C', D: 'D', E: 'E', G: 'G', P: 'p' }, image: 'topas.jpg' }, { name: 'Platin-Hell', tokens: { A: 'a', C: 'C', D: 'D', E: 'E', G: 'G', P: 'p' }, image: 'platin-hell.jpg' }, { name: 'Agouti dd', tokens: { A: 'A', C: 'C', D: 'd', E: 'E', G: 'G', P: 'P' }, image: 'agouti-dd.jpg' }, @@ -81,8 +84,10 @@ export const BASE_COLORS: readonly FarbschlagEntry[] = [ { name: 'Anthrazit dd', tokens: { A: 'a', C: 'C', D: 'd', E: 'E', G: 'g', P: 'P' }, image: 'anthrazit-dd.jpg' }, { name: 'Agouti CP-Hell', tokens: { A: 'A', C: 'cchm', D: 'D', E: 'E', G: 'G', P: 'P' }, image: 'agouti-cp-hell.JPG' }, { name: 'Blaufuchs CP', tokens: { A: 'a', C: 'cchm', D: 'D', E: 'e', G: 'g', P: 'P' }, image: 'blaufuchs-cp.jpg' }, - { name: 'Polarfuchsschimmel', tokens: { A: 'A', C: 'C', D: 'D', E: 'ef', G: 'g', P: 'P' }, image: 'polarfuchsschimmel.jpg' }, + // GEN-3a: efef gg base = Silberschimmel (breeder C5) — listed before the + // A-specific Polarfuchsschimmel so the canonical efef-gg reverse-matches here. { name: 'Silberschimmel', tokens: { C: 'C', D: 'D', E: 'ef', G: 'g', P: 'P' }, image: 'silberschimmel.jpg' }, + { name: 'Polarfuchsschimmel', tokens: { A: 'A', C: 'C', D: 'D', E: 'ef', G: 'g', P: 'P' }, image: 'polarfuchsschimmel.jpg' }, { name: 'Algierfuchsschimmel', tokens: { A: 'A', C: 'C', D: 'D', E: 'ef', G: 'G', P: 'P' }, image: 'algierfuchsschimmel.jpg' }, { name: 'Polarfuchs-Hell CP', tokens: { A: 'A', C: 'cchm', D: 'D', E: 'e', G: 'g', P: 'P' }, image: 'polarfuchs-hell-cp.jpg' }, { name: 'Kohlfuchsschimmel', tokens: { A: 'a', C: 'C', D: 'D', E: 'ef', G: 'G', P: 'P' }, image: 'kohlfuchsschimmel.jpg' }, @@ -117,6 +122,9 @@ export const BASE_COLORS: readonly FarbschlagEntry[] = [ { name: 'Zobel-Hell dd', tokens: { A: 'a', C: 'cchm', D: 'd', E: 'E', G: 'g', P: 'P' }, image: 'zobel-hell-dd.jpg' }, { name: 'Kohlfuchsschimmel CP', tokens: { A: 'a', C: 'cchm', D: 'D', E: 'ef', G: 'G', P: 'P' }, image: 'kohlfuchsschimmel-cp.JPG' }, { name: 'Blaufuchs dd', tokens: { A: 'a', C: 'C', D: 'd', E: 'e', G: 'g', P: 'p' }, image: 'blaufuchs-dd.jpg' }, + // GEN-3a: c[chm]c[chm] efef base = CP-Orangeschimmel (breeder C5). A unspecified + // (listed after the A-specific cchm-Schimmel entries so they win for those). + { name: 'CP-Orangeschimmel', tokens: { C: 'cchm', D: 'D', E: 'ef', G: 'G', P: 'P' } }, ] export const UNKNOWN_FARBSCHLAG = 'Unbekannter Farbschlag' diff --git a/gerbil-manager-web/src/genetics/colorVarietySeed.generated.json b/gerbil-manager-web/src/genetics/colorVarietySeed.generated.json index 6c8e820..6591f1f 100644 --- a/gerbil-manager-web/src/genetics/colorVarietySeed.generated.json +++ b/gerbil-manager-web/src/genetics/colorVarietySeed.generated.json @@ -1,440 +1,456 @@ -[ - { - "name": "Pink Eyed White (PEW)", - "canonicalGenotype": "AA chch DD EE GG pp spsp rere", - "sortOrder": 0, - "image": "rotaugen-weiss-pew-d-sep-e-sep.jpg" - }, - { - "name": "Hermelin", - "canonicalGenotype": "aa chch DD EE GG PP spsp rere", - "sortOrder": 1, - "image": "hermelin.jpeg" - }, - { - "name": "Himalaya", - "canonicalGenotype": "AA chch DD EE GG PP spsp rere", - "sortOrder": 2, - "image": "himalaya.jpg" - }, - { - "name": "Zobel", - "canonicalGenotype": "aa cchmcchm DD EE gg PP spsp rere", - "sortOrder": 3, - "image": "zobel.jpeg" - }, - { - "name": "Schwarzschimmel", - "canonicalGenotype": "AA CC DD efef GG PP spsp rere", - "sortOrder": 4, - "image": null - }, - { - "name": "Rotaugenschimmel", - "canonicalGenotype": "AA CC DD efef GG pp spsp rere", - "sortOrder": 5, - "image": "rotaugen-schimmel.jpg" - }, - { - "name": "Agouti", - "canonicalGenotype": "AA CC DD EE GG PP spsp rere", - "sortOrder": 6, - "image": "agouti-mit-erklaerung-der-genloci.JPG" - }, - { - "name": "Schwarz", - "canonicalGenotype": "aa CC DD EE GG PP spsp rere", - "sortOrder": 7, - "image": "schwarz.jpg" - }, - { - "name": "Silberagouti", - "canonicalGenotype": "AA CC DD EE gg PP spsp rere", - "sortOrder": 8, - "image": "silberagouti.jpg" - }, - { - "name": "Anthrazit", - "canonicalGenotype": "aa CC DD EE gg PP spsp rere", - "sortOrder": 9, - "image": "anthrazit.jpg" - }, - { - "name": "Algierfuchs", - "canonicalGenotype": "AA CC DD ee GG PP spsp rere", - "sortOrder": 10, - "image": "algierfuchs.jpg" - }, - { - "name": "Blau", - "canonicalGenotype": "aa CC dd EE GG PP spsp rere", - "sortOrder": 11, - "image": "blau-schwarz-dd.JPG" - }, - { - "name": "Gold", - "canonicalGenotype": "AA CC DD EE GG pp spsp rere", - "sortOrder": 12, - "image": "gold.jpg" - }, - { - "name": "Platin", - "canonicalGenotype": "aa CC DD EE GG pp spsp rere", - "sortOrder": 13, - "image": "platin.JPG" - }, - { - "name": "Goldfuchs", - "canonicalGenotype": "AA CC DD ee GG pp spsp rere", - "sortOrder": 14, - "image": "goldfuchs.jpg" - }, - { - "name": "Rotfuchs", - "canonicalGenotype": "aa CC DD ee GG pp spsp rere", - "sortOrder": 15, - "image": "rotfuchs.JPG" - }, - { - "name": "dd Gold", - "canonicalGenotype": "AA CC dd EE GG pp spsp rere", - "sortOrder": 16, - "image": "gold-dd.jpg" - }, - { - "name": "dd Platin", - "canonicalGenotype": "aa CC dd EE GG pp spsp rere", - "sortOrder": 17, - "image": "platin-dd.jpg" - }, - { - "name": "Altweiss (REW)", - "canonicalGenotype": "aa CC DD EE gg pp spsp rere", - "sortOrder": 18, - "image": "altweiss-rew.jpeg" - }, - { - "name": "Apricot (Blassfuchs)", - "canonicalGenotype": "AA CC DD ee gg pp spsp rere", - "sortOrder": 19, - "image": "apricot-blassfuchs.jpg" - }, - { - "name": "Blaufuchs", - "canonicalGenotype": "aa CC DD ee gg PP spsp rere", - "sortOrder": 20, - "image": "blaufuchs.jpg" - }, - { - "name": "C-Separator", - "canonicalGenotype": "aa CC DD ee gg pp spsp rere", - "sortOrder": 21, - "image": "c-separator.jpg" - }, - { - "name": "Elfenbein", - "canonicalGenotype": "AA CC DD EE gg pp spsp rere", - "sortOrder": 22, - "image": "elfenbein.jpg" - }, - { - "name": "Kohlfuchs", - "canonicalGenotype": "aa CC DD ee GG PP spsp rere", - "sortOrder": 23, - "image": "kohlfuchs.jpg" - }, - { - "name": "Marder", - "canonicalGenotype": "aa cchmcchm DD EE GG PP spsp rere", - "sortOrder": 24, - "image": "marder.JPG" - }, - { - "name": "Siam (Marder-Hell)", - "canonicalGenotype": "aa cchmcchm DD EE GG PP spsp rere", - "sortOrder": 25, - "image": "siam-marder-hell.JPG" - }, - { - "name": "Polarfuchs", - "canonicalGenotype": "AA CC DD ee gg PP spsp rere", - "sortOrder": 26, - "image": "polarfuchs.jpg" - }, - { - "name": "Saphir", - "canonicalGenotype": "aa CC DD EE GG pp spsp rere", - "sortOrder": 27, - "image": "saphir.jpg" - }, - { - "name": "Schimmel (Orangeschimmel)", - "canonicalGenotype": "AA CC DD efef GG PP spsp rere", - "sortOrder": 28, - "image": "schimmel-orangeschimmel.jpg" - }, - { - "name": "Topas", - "canonicalGenotype": "AA CC DD EE GG pp spsp rere", - "sortOrder": 29, - "image": "topas.jpg" - }, - { - "name": "Platin-Hell", - "canonicalGenotype": "aa CC DD EE GG pp spsp rere", - "sortOrder": 30, - "image": "platin-hell.jpg" - }, - { - "name": "Agouti dd", - "canonicalGenotype": "AA CC dd EE GG PP spsp rere", - "sortOrder": 31, - "image": "agouti-dd.jpg" - }, - { - "name": "Silberagouti dd", - "canonicalGenotype": "AA CC dd EE gg PP spsp rere", - "sortOrder": 32, - "image": "silberagouti-dd.jpg" - }, - { - "name": "Kohlfuchs dd", - "canonicalGenotype": "aa CC dd ee GG PP spsp rere", - "sortOrder": 33, - "image": "kohlfuchs-dd.jpg" - }, - { - "name": "Anthrazit dd", - "canonicalGenotype": "aa CC dd EE gg PP spsp rere", - "sortOrder": 34, - "image": "anthrazit-dd.jpg" - }, - { - "name": "Agouti CP-Hell", - "canonicalGenotype": "AA cchmcchm DD EE GG PP spsp rere", - "sortOrder": 35, - "image": "agouti-cp-hell.JPG" - }, - { - "name": "Blaufuchs CP", - "canonicalGenotype": "aa cchmcchm DD ee gg PP spsp rere", - "sortOrder": 36, - "image": "blaufuchs-cp.jpg" - }, - { - "name": "Polarfuchsschimmel", - "canonicalGenotype": "AA CC DD efef gg PP spsp rere", - "sortOrder": 37, - "image": "polarfuchsschimmel.jpg" - }, - { - "name": "Silberschimmel", - "canonicalGenotype": "AA CC DD efef gg PP spsp rere", - "sortOrder": 38, - "image": "silberschimmel.jpg" - }, - { - "name": "Algierfuchsschimmel", - "canonicalGenotype": "AA CC DD efef GG PP spsp rere", - "sortOrder": 39, - "image": "algierfuchsschimmel.jpg" - }, - { - "name": "Polarfuchs-Hell CP", - "canonicalGenotype": "AA cchmcchm DD ee gg PP spsp rere", - "sortOrder": 40, - "image": "polarfuchs-hell-cp.jpg" - }, - { - "name": "Kohlfuchsschimmel", - "canonicalGenotype": "aa CC DD efef GG PP spsp rere", - "sortOrder": 41, - "image": "kohlfuchsschimmel.jpg" - }, - { - "name": "Blaufuchsschimmel", - "canonicalGenotype": "aa CC DD efef gg PP spsp rere", - "sortOrder": 42, - "image": "blaufuchsschimmel.jpg" - }, - { - "name": "Kohlfuchs, hell", - "canonicalGenotype": "aa CC DD ee GG PP spsp rere", - "sortOrder": 43, - "image": "kohlfuchs-hell.jpg" - }, - { - "name": "Goldfuchs, hell", - "canonicalGenotype": "AA CC DD ee GG pp spsp rere", - "sortOrder": 44, - "image": "goldfuchs-hell.jpg" - }, - { - "name": "Goldfuchsschimmel", - "canonicalGenotype": "AA CC DD efef GG pp spsp rere", - "sortOrder": 45, - "image": "goldfuchsschimmel.jpg" - }, - { - "name": "Gold-Hell", - "canonicalGenotype": "AA CC DD EE GG pp spsp rere", - "sortOrder": 46, - "image": "gold-hell.jpg" - }, - { - "name": "Siam (Marder-Hell) dd", - "canonicalGenotype": "aa cchmcchm dd EE GG PP spsp rere", - "sortOrder": 47, - "image": "siam-marder-hell-dd.jpg" - }, - { - "name": "Marder dd", - "canonicalGenotype": "aa cchmcchm dd EE GG PP spsp rere", - "sortOrder": 48, - "image": "marder-dd.jpg" - }, - { - "name": "Zobel-Hell", - "canonicalGenotype": "aa cchmcchm DD EE gg PP spsp rere", - "sortOrder": 49, - "image": "zobel-hell.jpg" - }, - { - "name": "Silberagouti dd CP", - "canonicalGenotype": "AA cchmcchm dd EE gg PP spsp rere", - "sortOrder": 50, - "image": "silberagouti-dd-cp.jpg" - }, - { - "name": "Silberagouti-Hell dd CP", - "canonicalGenotype": "AA cchmcchm dd EE gg PP spsp rere", - "sortOrder": 51, - "image": "silberagouti-hell-dd-cp.jpg" - }, - { - "name": "Agouti dd CP", - "canonicalGenotype": "AA cchmcchm dd EE GG PP spsp rere", - "sortOrder": 52, - "image": "agouti-dd-cp.jpg" - }, - { - "name": "Agouti-Hell dd CP", - "canonicalGenotype": "AA cchmcchm dd EE GG PP spsp rere", - "sortOrder": 53, - "image": "agouti-hell-dd-cp.jpg" - }, - { - "name": "Blaufuchs, hell", - "canonicalGenotype": "aa CC DD ee gg PP spsp rere", - "sortOrder": 54, - "image": "blaufuchs-hell.jpeg" - }, - { - "name": "Rotfuchsschimmel", - "canonicalGenotype": "aa CC DD efef GG pp spsp rere", - "sortOrder": 55, - "image": "rotfuchsschimmel.jpg" - }, - { - "name": "Polarfuchs, hell", - "canonicalGenotype": "AA CC DD ee gg PP spsp rere", - "sortOrder": 56, - "image": "polarfuchs-hell.jpeg" - }, - { - "name": "Kohlfuchsschimmel, hell", - "canonicalGenotype": "aa CC DD efef GG PP spsp rere", - "sortOrder": 57, - "image": "kohlfuchsschimmel-hell.jpg" - }, - { - "name": "Rotfuchs, hell", - "canonicalGenotype": "aa CC DD ee GG pp spsp rere", - "sortOrder": 58, - "image": "rotfuchs-hell.jpg" - }, - { - "name": "Zobel dd", - "canonicalGenotype": "aa cchmcchm dd EE gg PP spsp rere", - "sortOrder": 59, - "image": "zobel-dd.jpg" - }, - { - "name": "Kohlfuchs-Hell", - "canonicalGenotype": "aa CC DD ee GG PP spsp rere", - "sortOrder": 60, - "image": "kohlfuchs-hell-2.jpg" - }, - { - "name": "Kohlfuchs CP", - "canonicalGenotype": "aa cchmcchm DD ee GG PP spsp rere", - "sortOrder": 61, - "image": "kohlfuchs-cp.jpg" - }, - { - "name": "Algierfuchs CP", - "canonicalGenotype": "AA cchmcchm DD ee GG PP spsp rere", - "sortOrder": 62, - "image": "algierfuchs-cp.jpg" - }, - { - "name": "Silberagouti CP", - "canonicalGenotype": "AA cchmcchm DD EE gg PP spsp rere", - "sortOrder": 63, - "image": "silberagouti-cp.JPG" - }, - { - "name": "Agouti CP", - "canonicalGenotype": "AA cchmcchm DD EE GG PP spsp rere", - "sortOrder": 64, - "image": "agouti-cp.jpg" - }, - { - "name": "Algierfuchs-Hell CP", - "canonicalGenotype": "AA cchmcchm DD ee GG PP spsp rere", - "sortOrder": 65, - "image": "algierfuchs-hell-cp.jpg" - }, - { - "name": "Kohlfuchs,hell CP", - "canonicalGenotype": "aa cchmcchm DD ee GG PP spsp rere", - "sortOrder": 66, - "image": "kohlfuchs-hell-cp.jpg" - }, - { - "name": "Polarfuchs CP", - "canonicalGenotype": "AA cchmcchm DD ee gg PP spsp rere", - "sortOrder": 67, - "image": "polarfuchs-cp.jpg" - }, - { - "name": "Algierfuchs, hell", - "canonicalGenotype": "AA CC DD ee GG PP spsp rere", - "sortOrder": 68, - "image": "algierfuchs-hell.JPG" - }, - { - "name": "Topas dd", - "canonicalGenotype": "AA CC dd EE GG pp spsp rere", - "sortOrder": 69, - "image": "topas-dd.jpg" - }, - { - "name": "Zobel-Hell dd", - "canonicalGenotype": "aa cchmcchm dd EE gg PP spsp rere", - "sortOrder": 70, - "image": "zobel-hell-dd.jpg" - }, - { - "name": "Kohlfuchsschimmel CP", - "canonicalGenotype": "aa cchmcchm DD efef GG PP spsp rere", - "sortOrder": 71, - "image": "kohlfuchsschimmel-cp.JPG" - }, - { - "name": "Blaufuchs dd", - "canonicalGenotype": "aa CC dd ee gg PP spsp rere", - "sortOrder": 72, - "image": "blaufuchs-dd.jpg" - } -] +[ + { + "name": "Pink Eyed White (PEW)", + "english": "Pink Eyed White", + "canonicalGenotype": "AA chch DD EE GG pp spsp rere", + "sortOrder": 0, + "image": "rotaugen-weiss-pew-d-sep-e-sep.jpg" + }, + { + "name": "Hermelin", + "english": "Dark Tailed White", + "canonicalGenotype": "aa chch DD EE GG PP spsp rere", + "sortOrder": 1, + "image": "hermelin.jpeg" + }, + { + "name": "Himalaya", + "english": "Himalayan", + "canonicalGenotype": "AA chch DD EE GG PP spsp rere", + "sortOrder": 2, + "image": "himalaya.jpg" + }, + { + "name": "Zobel", + "english": "Sable", + "canonicalGenotype": "aa cchmcchm DD EE gg PP spsp rere", + "sortOrder": 3, + "image": "zobel.jpeg" + }, + { + "name": "Rotaugenschimmel", + "english": "Red-Eyed Roan", + "canonicalGenotype": "AA CC DD efef GG pp spsp rere", + "sortOrder": 4, + "image": "rotaugen-schimmel.jpg" + }, + { + "name": "Agouti", + "english": "Golden Agouti", + "canonicalGenotype": "AA CC DD EE GG PP spsp rere", + "sortOrder": 5, + "image": "agouti-mit-erklaerung-der-genloci.JPG" + }, + { + "name": "Schwarz", + "english": "Black", + "canonicalGenotype": "aa CC DD EE GG PP spsp rere", + "sortOrder": 6, + "image": "schwarz.jpg" + }, + { + "name": "Silberagouti", + "english": "Grey Agouti", + "canonicalGenotype": "AA CC DD EE gg PP spsp rere", + "sortOrder": 7, + "image": "silberagouti.jpg" + }, + { + "name": "Anthrazit", + "english": "Slate", + "canonicalGenotype": "aa CC DD EE gg PP spsp rere", + "sortOrder": 8, + "image": "anthrazit.jpg" + }, + { + "name": "Algierfuchs", + "english": "Dark-Eyed Honey", + "canonicalGenotype": "AA CC DD ee GG PP spsp rere", + "sortOrder": 9, + "image": "algierfuchs.jpg" + }, + { + "name": "Blau", + "english": "Blue", + "canonicalGenotype": "aa CC dd EE GG PP spsp rere", + "sortOrder": 10, + "image": "blau-schwarz-dd.JPG" + }, + { + "name": "Gold", + "english": "Argente Golden", + "canonicalGenotype": "AA CC DD EE GG pp spsp rere", + "sortOrder": 11, + "image": "gold.jpg" + }, + { + "name": "Platin", + "english": "Lilac", + "canonicalGenotype": "aa CC DD EE GG pp spsp rere", + "sortOrder": 12, + "image": "platin.JPG" + }, + { + "name": "Goldfuchs", + "english": "Yellow Fox", + "canonicalGenotype": "AA CC DD ee GG pp spsp rere", + "sortOrder": 13, + "image": "goldfuchs.jpg" + }, + { + "name": "Rotfuchs", + "english": "Argente Nutmeg", + "canonicalGenotype": "aa CC DD ee GG pp spsp rere", + "sortOrder": 14, + "image": "rotfuchs.JPG" + }, + { + "name": "dd Gold", + "english": "dd Argente Golden", + "canonicalGenotype": "AA CC dd EE GG pp spsp rere", + "sortOrder": 15, + "image": "gold-dd.jpg" + }, + { + "name": "dd Platin", + "english": "dd Lilac", + "canonicalGenotype": "aa CC dd EE GG pp spsp rere", + "sortOrder": 16, + "image": "platin-dd.jpg" + }, + { + "name": "Altweiss (REW)", + "canonicalGenotype": "aa CC DD EE gg pp spsp rere", + "sortOrder": 17, + "image": "altweiss-rew.jpeg" + }, + { + "name": "Apricot (Blassfuchs)", + "canonicalGenotype": "AA CC DD ee gg pp spsp rere", + "sortOrder": 18, + "image": "apricot-blassfuchs.jpg" + }, + { + "name": "Blaufuchs", + "canonicalGenotype": "aa CC DD ee gg PP spsp rere", + "sortOrder": 19, + "image": "blaufuchs.jpg" + }, + { + "name": "C-Separator", + "canonicalGenotype": "aa CC DD ee gg pp spsp rere", + "sortOrder": 20, + "image": "c-separator.jpg" + }, + { + "name": "Elfenbein", + "canonicalGenotype": "AA CC DD EE gg pp spsp rere", + "sortOrder": 21, + "image": "elfenbein.jpg" + }, + { + "name": "Kohlfuchs", + "canonicalGenotype": "aa CC DD ee GG PP spsp rere", + "sortOrder": 22, + "image": "kohlfuchs.jpg" + }, + { + "name": "Marder", + "canonicalGenotype": "aa cchmcchm DD EE GG PP spsp rere", + "sortOrder": 23, + "image": "marder.JPG" + }, + { + "name": "Siam (Marder-Hell)", + "canonicalGenotype": "aa cchmcchm DD EE GG PP spsp rere", + "sortOrder": 24, + "image": "siam-marder-hell.JPG" + }, + { + "name": "Polarfuchs", + "canonicalGenotype": "AA CC DD ee gg PP spsp rere", + "sortOrder": 25, + "image": "polarfuchs.jpg" + }, + { + "name": "Saphir", + "canonicalGenotype": "aa CC DD EE GG pp spsp rere", + "sortOrder": 26, + "image": "saphir.jpg" + }, + { + "name": "Orangeschimmel", + "canonicalGenotype": "AA CC DD efef GG PP spsp rere", + "sortOrder": 27, + "image": "schimmel-orangeschimmel.jpg" + }, + { + "name": "Topas", + "canonicalGenotype": "AA CC DD EE GG pp spsp rere", + "sortOrder": 28, + "image": "topas.jpg" + }, + { + "name": "Platin-Hell", + "canonicalGenotype": "aa CC DD EE GG pp spsp rere", + "sortOrder": 29, + "image": "platin-hell.jpg" + }, + { + "name": "Agouti dd", + "canonicalGenotype": "AA CC dd EE GG PP spsp rere", + "sortOrder": 30, + "image": "agouti-dd.jpg" + }, + { + "name": "Silberagouti dd", + "canonicalGenotype": "AA CC dd EE gg PP spsp rere", + "sortOrder": 31, + "image": "silberagouti-dd.jpg" + }, + { + "name": "Kohlfuchs dd", + "canonicalGenotype": "aa CC dd ee GG PP spsp rere", + "sortOrder": 32, + "image": "kohlfuchs-dd.jpg" + }, + { + "name": "Anthrazit dd", + "canonicalGenotype": "aa CC dd EE gg PP spsp rere", + "sortOrder": 33, + "image": "anthrazit-dd.jpg" + }, + { + "name": "Agouti CP-Hell", + "canonicalGenotype": "AA cchmcchm DD EE GG PP spsp rere", + "sortOrder": 34, + "image": "agouti-cp-hell.JPG" + }, + { + "name": "Blaufuchs CP", + "canonicalGenotype": "aa cchmcchm DD ee gg PP spsp rere", + "sortOrder": 35, + "image": "blaufuchs-cp.jpg" + }, + { + "name": "Silberschimmel", + "canonicalGenotype": "AA CC DD efef gg PP spsp rere", + "sortOrder": 36, + "image": "silberschimmel.jpg" + }, + { + "name": "Polarfuchsschimmel", + "canonicalGenotype": "AA CC DD efef gg PP spsp rere", + "sortOrder": 37, + "image": "polarfuchsschimmel.jpg" + }, + { + "name": "Algierfuchsschimmel", + "canonicalGenotype": "AA CC DD efef GG PP spsp rere", + "sortOrder": 38, + "image": "algierfuchsschimmel.jpg" + }, + { + "name": "Polarfuchs-Hell CP", + "canonicalGenotype": "AA cchmcchm DD ee gg PP spsp rere", + "sortOrder": 39, + "image": "polarfuchs-hell-cp.jpg" + }, + { + "name": "Kohlfuchsschimmel", + "canonicalGenotype": "aa CC DD efef GG PP spsp rere", + "sortOrder": 40, + "image": "kohlfuchsschimmel.jpg" + }, + { + "name": "Blaufuchsschimmel", + "canonicalGenotype": "aa CC DD efef gg PP spsp rere", + "sortOrder": 41, + "image": "blaufuchsschimmel.jpg" + }, + { + "name": "Kohlfuchs, hell", + "canonicalGenotype": "aa CC DD ee GG PP spsp rere", + "sortOrder": 42, + "image": "kohlfuchs-hell.jpg" + }, + { + "name": "Goldfuchs, hell", + "canonicalGenotype": "AA CC DD ee GG pp spsp rere", + "sortOrder": 43, + "image": "goldfuchs-hell.jpg" + }, + { + "name": "Goldfuchsschimmel", + "canonicalGenotype": "AA CC DD efef GG pp spsp rere", + "sortOrder": 44, + "image": "goldfuchsschimmel.jpg" + }, + { + "name": "Gold-Hell", + "canonicalGenotype": "AA CC DD EE GG pp spsp rere", + "sortOrder": 45, + "image": "gold-hell.jpg" + }, + { + "name": "Siam (Marder-Hell) dd", + "canonicalGenotype": "aa cchmcchm dd EE GG PP spsp rere", + "sortOrder": 46, + "image": "siam-marder-hell-dd.jpg" + }, + { + "name": "Marder dd", + "canonicalGenotype": "aa cchmcchm dd EE GG PP spsp rere", + "sortOrder": 47, + "image": "marder-dd.jpg" + }, + { + "name": "Zobel-Hell", + "canonicalGenotype": "aa cchmcchm DD EE gg PP spsp rere", + "sortOrder": 48, + "image": "zobel-hell.jpg" + }, + { + "name": "Silberagouti dd CP", + "canonicalGenotype": "AA cchmcchm dd EE gg PP spsp rere", + "sortOrder": 49, + "image": "silberagouti-dd-cp.jpg" + }, + { + "name": "Silberagouti-Hell dd CP", + "canonicalGenotype": "AA cchmcchm dd EE gg PP spsp rere", + "sortOrder": 50, + "image": "silberagouti-hell-dd-cp.jpg" + }, + { + "name": "Agouti dd CP", + "canonicalGenotype": "AA cchmcchm dd EE GG PP spsp rere", + "sortOrder": 51, + "image": "agouti-dd-cp.jpg" + }, + { + "name": "Agouti-Hell dd CP", + "canonicalGenotype": "AA cchmcchm dd EE GG PP spsp rere", + "sortOrder": 52, + "image": "agouti-hell-dd-cp.jpg" + }, + { + "name": "Blaufuchs, hell", + "canonicalGenotype": "aa CC DD ee gg PP spsp rere", + "sortOrder": 53, + "image": "blaufuchs-hell.jpeg" + }, + { + "name": "Rotfuchsschimmel", + "canonicalGenotype": "aa CC DD efef GG pp spsp rere", + "sortOrder": 54, + "image": "rotfuchsschimmel.jpg" + }, + { + "name": "Polarfuchs, hell", + "canonicalGenotype": "AA CC DD ee gg PP spsp rere", + "sortOrder": 55, + "image": "polarfuchs-hell.jpeg" + }, + { + "name": "Kohlfuchsschimmel, hell", + "canonicalGenotype": "aa CC DD efef GG PP spsp rere", + "sortOrder": 56, + "image": "kohlfuchsschimmel-hell.jpg" + }, + { + "name": "Rotfuchs, hell", + "canonicalGenotype": "aa CC DD ee GG pp spsp rere", + "sortOrder": 57, + "image": "rotfuchs-hell.jpg" + }, + { + "name": "Zobel dd", + "canonicalGenotype": "aa cchmcchm dd EE gg PP spsp rere", + "sortOrder": 58, + "image": "zobel-dd.jpg" + }, + { + "name": "Kohlfuchs-Hell", + "canonicalGenotype": "aa CC DD ee GG PP spsp rere", + "sortOrder": 59, + "image": "kohlfuchs-hell-2.jpg" + }, + { + "name": "Kohlfuchs CP", + "canonicalGenotype": "aa cchmcchm DD ee GG PP spsp rere", + "sortOrder": 60, + "image": "kohlfuchs-cp.jpg" + }, + { + "name": "Algierfuchs CP", + "canonicalGenotype": "AA cchmcchm DD ee GG PP spsp rere", + "sortOrder": 61, + "image": "algierfuchs-cp.jpg" + }, + { + "name": "Silberagouti CP", + "canonicalGenotype": "AA cchmcchm DD EE gg PP spsp rere", + "sortOrder": 62, + "image": "silberagouti-cp.JPG" + }, + { + "name": "Agouti CP", + "canonicalGenotype": "AA cchmcchm DD EE GG PP spsp rere", + "sortOrder": 63, + "image": "agouti-cp.jpg" + }, + { + "name": "Algierfuchs-Hell CP", + "canonicalGenotype": "AA cchmcchm DD ee GG PP spsp rere", + "sortOrder": 64, + "image": "algierfuchs-hell-cp.jpg" + }, + { + "name": "Kohlfuchs,hell CP", + "canonicalGenotype": "aa cchmcchm DD ee GG PP spsp rere", + "sortOrder": 65, + "image": "kohlfuchs-hell-cp.jpg" + }, + { + "name": "Polarfuchs CP", + "canonicalGenotype": "AA cchmcchm DD ee gg PP spsp rere", + "sortOrder": 66, + "image": "polarfuchs-cp.jpg" + }, + { + "name": "Algierfuchs, hell", + "canonicalGenotype": "AA CC DD ee GG PP spsp rere", + "sortOrder": 67, + "image": "algierfuchs-hell.JPG" + }, + { + "name": "Topas dd", + "canonicalGenotype": "AA CC dd EE GG pp spsp rere", + "sortOrder": 68, + "image": "topas-dd.jpg" + }, + { + "name": "Zobel-Hell dd", + "canonicalGenotype": "aa cchmcchm dd EE gg PP spsp rere", + "sortOrder": 69, + "image": "zobel-hell-dd.jpg" + }, + { + "name": "Kohlfuchsschimmel CP", + "canonicalGenotype": "aa cchmcchm DD efef GG PP spsp rere", + "sortOrder": 70, + "image": "kohlfuchsschimmel-cp.JPG" + }, + { + "name": "Blaufuchs dd", + "canonicalGenotype": "aa CC dd ee gg pp spsp rere", + "sortOrder": 71, + "image": "blaufuchs-dd.jpg" + }, + { + "name": "CP-Orangeschimmel", + "canonicalGenotype": "AA cchmcchm DD efef GG PP spsp rere", + "sortOrder": 72 + } +] From 7aac59f3e50ff311d9ebef541e89178fcfd807cc Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 6 Jun 2026 10:25:48 +0200 Subject: [PATCH 3/4] GEN-3a: de.ts warning texts (SLS_LETHAL/SUPERSCHECKE_DEAF) + genetics tests (Uw alias, Sls het/lethal, Superschecke, flag tolerance, Schimmel fix) --- .../src/genetics/__tests__/genetics.test.ts | 81 ++++++++++++++++++- gerbil-manager-web/src/strings/de.ts | 4 + 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/gerbil-manager-web/src/genetics/__tests__/genetics.test.ts b/gerbil-manager-web/src/genetics/__tests__/genetics.test.ts index af69e1a..eba549e 100644 --- a/gerbil-manager-web/src/genetics/__tests__/genetics.test.ts +++ b/gerbil-manager-web/src/genetics/__tests__/genetics.test.ts @@ -15,6 +15,7 @@ import { toJSON, fromJSON, wildType, + extractGenotypeFlags, } from '../genotype' import { combineLocus } from '../punnett' import { LOCI, type LocusKey } from '../loci' @@ -28,7 +29,7 @@ import { } from '../catalog' /** The first 18 entries are the frozen contract names (must round-trip exactly). */ -const FROZEN_COUNT = 18 +const FROZEN_COUNT = 17 import { breed } from '../breed' import { GeneticsWarningCode } from '../warnings' @@ -64,6 +65,7 @@ describe('Genotype serialization', () => { P: ['p', 'P'], Sp: ['sp', 'sp'], Re: ['re', 're'], + Sls: ['sl', 'sl'], }) expect(g.A).toEqual(['A', 'a']) expect(g.C).toEqual(['C', 'ch']) @@ -240,6 +242,7 @@ describe('Partially-unknown parents (wildcards)', () => { P: ['P', 'P'], Sp: ['sp', 'sp'], Re: ['re', 're'], + Sls: ['sl', 'sl'], }) const mother = fromDisplayString('aa CC DD EE GG PP spsp rere') const result = breed(father, mother) @@ -263,3 +266,79 @@ describe('Probabilities are exact fractions summing to 1', () => { expect(toNumber(sum)).toBeCloseTo(1, 10) }) }) + +describe('GEN-3a: Uw=G alias', () => { + it('Uwuw parses as Gg, UwUw as GG', () => { + expect(fromDisplayString('AA CC DD EE Uwuw PP spsp rere').G).toEqual(['G', 'g']) + expect(fromDisplayString('AA CC DD EE UwUw PP spsp rere').G).toEqual(['G', 'G']) + expect(fromDisplayString('AA CC DD EE uwuw PP spsp rere').G).toEqual(['g', 'g']) + }) +}) + +describe('GEN-3a: second spotting locus Sls (WP)', () => { + it('S(l)s(l) and WP both parse to the Sls heterozygote Slsl', () => { + expect(fromDisplayString('AA CC DD EE GG PP spsp rere S(l)s(l)').Sls).toEqual(['Sl', 'sl']) + expect(fromDisplayString('AA CC DD EE GG PP spsp rere WP').Sls).toEqual(['Sl', 'sl']) + }) + + it('toDisplayString omits wild-type Sls but shows Slsl', () => { + expect(toDisplayString(wildType())).toBe('AA CC DD EE GG PP spsp rere') + expect(toDisplayString(fromDisplayString('AA CC DD EE GG PP spsp rere WP'))).toBe( + 'AA CC DD EE GG PP spsp rere Slsl', + ) + }) + + it('WP × WP: S(l)S(l) is lethal — removed, renormalized, SLS_LETHAL warning', () => { + const parent = fromDisplayString('AA CC DD EE GG PP spsp rere Slsl') + const result = breed(parent, parent) + expect(result.offspring.every((o) => !o.genotype.includes('SlSl'))).toBe(true) + const sum = result.offspring.reduce((acc, o) => acc + o.probability.value, 0) + expect(sum).toBeCloseTo(1, 10) + // survivors 2/3 Slsl : 1/3 slsl + const wp = result.offspring.find((o) => o.genotype.includes('Slsl'))! + expect(wp.probability.text).toBe('2/3') + const warn = result.warnings.find((w) => w.code === GeneticsWarningCode.SlsLethal) + expect(warn?.detail?.youngLostFraction).toBe('1/4') + }) + + it('Sp × Sls → Superschecke deafness warning', () => { + const father = fromDisplayString('AA CC DD EE GG PP Spsp rere') + const mother = fromDisplayString('AA CC DD EE GG PP spsp rere Slsl') + const result = breed(father, mother) + expect( + result.warnings.some((w) => w.code === GeneticsWarningCode.SuperscheckeDeaf), + ).toBe(true) + }) +}) + +describe('GEN-3a: flag/metadata tokens tolerated', () => { + it('dea/taub/Dea/DP/WFNZ/RV/GV do not break parsing (stripped)', () => { + const g = fromDisplayString('AA CC DD EE GG PP spsp rere dea WFNZ DP RV GV') + expect(toDisplayString(g)).toBe('AA CC DD EE GG PP spsp rere') + }) + + it('extractGenotypeFlags reads deafness + tags', () => { + expect(extractGenotypeFlags('AA CC DD EE GG PP spsp rere dea WFNZ').deaf).toBe(true) + expect(extractGenotypeFlags('AA CC DD EE GG PP spsp rere Dea').deaf).toBe(false) + expect(extractGenotypeFlags('AA CC DD EE GG PP spsp rere WFNZ RV').tags).toEqual(['WFNZ', 'RV']) + }) +}) + +describe('GEN-3a: Schimmel catalog fix (breeder C5)', () => { + it('efef base -> Orangeschimmel (not Schwarzschimmel)', () => { + expect(genotypeToFarbschlag(fromDisplayString('AA CC DD efef GG PP spsp rere'))).toBe( + 'Orangeschimmel', + ) + }) + it('efef pp -> Rotaugenschimmel, efef gg -> Silberschimmel', () => { + expect(genotypeToFarbschlag(fromDisplayString('AA CC DD efef GG pp spsp rere'))).toBe( + 'Rotaugenschimmel', + ) + expect(genotypeToFarbschlag(fromDisplayString('AA CC DD efef gg PP spsp rere'))).toBe( + 'Silberschimmel', + ) + }) + it('no Schwarzschimmel anywhere in the catalog', () => { + expect(BASE_COLORS.some((e) => e.name === 'Schwarzschimmel')).toBe(false) + }) +}) diff --git a/gerbil-manager-web/src/strings/de.ts b/gerbil-manager-web/src/strings/de.ts index 50bbe10..124ab90 100644 --- a/gerbil-manager-web/src/strings/de.ts +++ b/gerbil-manager-web/src/strings/de.ts @@ -625,6 +625,10 @@ export const de = { 'Schecke × Schecke: Reinerbige Tiere (SpSp) sterben bereits im Mutterleib — etwa ein Viertel weniger Jungtiere.', REX_SEMI_LETHAL: 'Rex × Rex: Reinerbige Tiere (ReRe) sind nur eingeschränkt lebensfähig.', + SLS_LETHAL: + 'WP × WP: Reinerbige Tiere (S(l)S(l)) sterben bereits im Mutterleib (Rumpback) — weniger Jungtiere.', + SUPERSCHECKE_DEAF: + 'Schecke × WP: Superschecken (sehr hoher Weißanteil) sind möglich — erhöhtes Taubheitsrisiko.', }, unknownFarbschlag: 'Unbekannter Farbschlag', }, From e2fd32ea12623276ce7ae7d248fb8d3e873b907f Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 6 Jun 2026 10:27:08 +0200 Subject: [PATCH 4/4] GEN-3a: test that Uw input renders as canonical G (never echoed back) --- .../src/genetics/__tests__/genetics.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gerbil-manager-web/src/genetics/__tests__/genetics.test.ts b/gerbil-manager-web/src/genetics/__tests__/genetics.test.ts index eba549e..40a6608 100644 --- a/gerbil-manager-web/src/genetics/__tests__/genetics.test.ts +++ b/gerbil-manager-web/src/genetics/__tests__/genetics.test.ts @@ -273,6 +273,14 @@ describe('GEN-3a: Uw=G alias', () => { expect(fromDisplayString('AA CC DD EE UwUw PP spsp rere').G).toEqual(['G', 'G']) expect(fromDisplayString('AA CC DD EE uwuw PP spsp rere').G).toEqual(['g', 'g']) }) + + it('always RENDERS G, never Uw (breeder preference)', () => { + // Uw/uw is an input/import alias only; output must echo G/g. + expect(toDisplayString(fromDisplayString('AA CC DD EE Uwuw PP spsp rere'))).toBe( + 'AA CC DD EE Gg PP spsp rere', + ) + expect(toDisplayString(fromDisplayString('AA CC DD EE uwuw PP spsp rere'))).not.toContain('uw') + }) }) describe('GEN-3a: second spotting locus Sls (WP)', () => {