diff --git a/gerbil-manager-web/src/genetics/__tests__/genetics.test.ts b/gerbil-manager-web/src/genetics/__tests__/genetics.test.ts index 799eddf..9da7ca6 100644 --- a/gerbil-manager-web/src/genetics/__tests__/genetics.test.ts +++ b/gerbil-manager-web/src/genetics/__tests__/genetics.test.ts @@ -179,10 +179,11 @@ describe('Farbschlag catalog', () => { expect(match.name).toBe('Unbekannter Farbschlag') }) - it('has the expected catalogue coverage (GEN-3f: 61 after cchm CP reconciliation)', () => { - // GEN-3f collapsed the 24 portal cchm colourpoint rows to 12 breeder-named - // varieties (Marder/Siam/Zobel/Zobel-Hell + CP-), so 73 -> 61. - expect(CATALOG_SIZE).toBe(61) + it('has the expected catalogue coverage (GEN-3g: 66 after adding CP-*-Hell het variants)', () => { + // GEN-3f: 73 -> 61 (cchm CP reconciliation). + // GEN-3g: +5 het variants (CP-Agouti/Silberagouti/Algierfuchs/Polarfuchs/Orangeschimmel -Hell), + // giving 61 + 5 = 66. CP-Fuchs-Hell was already counted. + expect(CATALOG_SIZE).toBe(66) }) it('frozen contract names round-trip to themselves (DB-key guard)', () => { @@ -414,8 +415,12 @@ describe('GEN-3e: C-locus colourpoint naming', () => { expect(name('AA cchmcchm DD EE GG PP spsp rere')).toBe('CP-Agouti') }) - it('A- + cchm/ch -> CP-', () => { - expect(name('AA cchmch DD EE GG PP spsp rere')).toBe('CP-Agouti') + it('A- + cchm/ch -> CP--Hell (GEN-3g: het gets -Hell suffix)', () => { + expect(name('AA cchmch DD EE GG PP spsp rere')).toBe('CP-Agouti-Hell') + expect(name('AA cchmch DD EE gg PP spsp rere')).toBe('CP-Silberagouti-Hell') + expect(name('AA cchmch DD ee GG PP spsp rere')).toBe('CP-Algierfuchs-Hell') + expect(name('AA cchmch DD ee gg PP spsp rere')).toBe('CP-Polarfuchs-Hell') + expect(name('AA cchmch dd ee GG PP spsp rere')).toBe('CP-Fuchs-Hell') }) it('aa fixed colourpoint names (Marder/Siam/Zobel/Zobel-Hell)', () => { @@ -471,10 +476,50 @@ describe('GEN-3f: CP catalog reconciled to the breeder CP- naming (matches her l expect(name('AA cchmcchm DD efef GG PP spsp rere')).toBe('CP-Orangeschimmel') }) - it('het cchm/ch colourpoints (Siam, Zobel-Hell) resolve to their own names', () => { + it('het cchm/ch colourpoints (Siam, Zobel-Hell, CP-*-Hell) resolve to their own names', () => { const siam = BASE_COLORS.find((e) => e.name === 'Siam')! const zh = BASE_COLORS.find((e) => e.name === 'Zobel-Hell')! + const cpah = BASE_COLORS.find((e) => e.name === 'CP-Agouti-Hell')! + const cpfh = BASE_COLORS.find((e) => e.name === 'CP-Fuchs-Hell')! expect(genotypeToFarbschlag(representativeGenotype(siam))).toBe('Siam') expect(genotypeToFarbschlag(representativeGenotype(zh))).toBe('Zobel-Hell') + expect(genotypeToFarbschlag(representativeGenotype(cpah))).toBe('CP-Agouti-Hell') + expect(genotypeToFarbschlag(representativeGenotype(cpfh))).toBe('CP-Fuchs-Hell') + }) +}) + +describe('GEN-3g: "-Hell" in variety name == cchm/ch het; hom == cchm/cchm', () => { + const name = (s: string) => genotypeToFarbschlag(fromDisplayString(s)) + const has = (n: string) => BASE_COLORS.some((e) => e.name === n) + + it('all new -Hell het entries exist in catalog', () => { + for (const n of [ + 'CP-Agouti-Hell', 'CP-Silberagouti-Hell', 'CP-Algierfuchs-Hell', + 'CP-Polarfuchs-Hell', 'CP-Orangeschimmel-Hell', + ]) { + expect(has(n)).toBe(true) + } + }) + + it('engine correctly maps het (cchm/ch) to -Hell suffix for all A- bases', () => { + expect(name('AA cchmch DD EE GG PP spsp rere')).toBe('CP-Agouti-Hell') + expect(name('AA cchmch DD EE gg PP spsp rere')).toBe('CP-Silberagouti-Hell') + expect(name('AA cchmch DD ee GG PP spsp rere')).toBe('CP-Algierfuchs-Hell') + expect(name('AA cchmch DD ee gg PP spsp rere')).toBe('CP-Polarfuchs-Hell') + expect(name('AA cchmch dd ee GG PP spsp rere')).toBe('CP-Fuchs-Hell') + expect(name('AA cchmch DD efef GG PP spsp rere')).toBe('CP-Orangeschimmel-Hell') + }) + + it('hom (cchm/cchm) still maps without -Hell suffix', () => { + expect(name('AA cchmcchm DD EE GG PP spsp rere')).toBe('CP-Agouti') + expect(name('AA cchmcchm DD EE gg PP spsp rere')).toBe('CP-Silberagouti') + expect(name('AA cchmcchm DD efef GG PP spsp rere')).toBe('CP-Orangeschimmel') + }) + + it('aa non-agouti branch is unchanged (Marder/Siam/Zobel/Zobel-Hell)', () => { + expect(name('aa cchmcchm DD EE GG PP spsp rere')).toBe('Marder') + expect(name('aa cchmch DD EE GG PP spsp rere')).toBe('Siam') + expect(name('aa cchmcchm DD EE gg PP spsp rere')).toBe('Zobel') + expect(name('aa cchmch DD EE gg PP spsp rere')).toBe('Zobel-Hell') }) }) diff --git a/gerbil-manager-web/src/genetics/catalog.ts b/gerbil-manager-web/src/genetics/catalog.ts index 49fee37..677c733 100644 --- a/gerbil-manager-web/src/genetics/catalog.ts +++ b/gerbil-manager-web/src/genetics/catalog.ts @@ -107,27 +107,29 @@ export const BASE_COLORS: readonly FarbschlagEntry[] = [ { name: 'Topas dd', tokens: { A: 'A', C: 'C', D: 'd', E: 'E', G: 'G', P: 'p' }, image: 'topas-dd.jpg' }, { name: 'Blaufuchs dd', tokens: { A: 'a', C: 'C', D: 'd', E: 'e', G: 'g', P: 'p' }, image: 'blaufuchs-dd.jpg' }, - // ── GEN-3f: c^chm colourpoint varieties, reconciled to the breeder's CP- naming ── - // The merged GEN-3e colourpointName() rule is authoritative: aa points are the - // marten/sable group (Marder/Siam, +gg Zobel/Zobel-Hell — E and D irrelevant); - // A- points take the 'CP-' prefix and the '-Hell' shade variants - // collapse (other loci irrelevant for the CP prefix). These names == the strings - // in her live data (god: extract animals.json) so the re-import name-matches and - // the Farbschlag mismatch hint stops. The het cchm/ch points (Siam, Zobel-Hell, - // CP-Fuchs-Hell) use the 'cchm/ch' pair token. The agouti fox/dilute points - // (CP-Fuchs/CP-Blaufuchs) resolve through the engine's E-family fallback to - // 'CP-Fuchs'; their distinct dropdown names remain for hand-pick + import match. + // ── GEN-3f/3g: c^chm colourpoint varieties ── + // GEN-3f: aa points = marten/sable group (Marder/Siam, +gg Zobel/Zobel-Hell). + // GEN-3g (breeder rule): '-Hell' == cchm/ch het; no '-Hell' == cchm/cchm hom. + // A- points: hom -> 'CP-', het -> 'CP--Hell' (colourpointName()). + // CP-Fuchs is a Sammelbegriff (unknown loci); its -Hell het = CP-Fuchs-Hell. + // CP-Blaufuchs (D:d, G:g) still resolves engine-side to 'CP-Fuchs' (dd/gg + // fox CP has no dedicated base entry); kept for import name-match + hand-pick. { name: 'Marder', tokens: { A: 'a', C: 'cchm', D: 'D', E: 'E', G: 'G', P: 'P' }, image: 'marder.JPG' }, { name: 'Siam', tokens: { A: 'a', C: 'cchm/ch', D: 'D', E: 'E', G: 'G', P: 'P' }, image: 'siam-marder-hell.JPG' }, { name: 'Zobel-Hell', tokens: { A: 'a', C: 'cchm/ch', D: 'D', E: 'E', G: 'g', P: 'P' }, image: 'zobel-hell.jpg' }, { name: 'CP-Agouti', tokens: { A: 'A', C: 'cchm', D: 'D', E: 'E', G: 'G', P: 'P' }, image: 'agouti-cp.jpg' }, + { name: 'CP-Agouti-Hell', tokens: { A: 'A', C: 'cchm/ch', D: 'D', E: 'E', G: 'G', P: 'P' } }, { name: 'CP-Silberagouti', tokens: { A: 'A', C: 'cchm', D: 'D', E: 'E', G: 'g', P: 'P' }, image: 'silberagouti-cp.JPG' }, + { name: 'CP-Silberagouti-Hell', tokens: { A: 'A', C: 'cchm/ch', D: 'D', E: 'E', G: 'g', P: 'P' } }, { name: 'CP-Algierfuchs', tokens: { A: 'A', C: 'cchm', D: 'D', E: 'e', G: 'G', P: 'P' }, image: 'algierfuchs-cp.jpg' }, + { name: 'CP-Algierfuchs-Hell', tokens: { A: 'A', C: 'cchm/ch', D: 'D', E: 'e', G: 'G', P: 'P' } }, { name: 'CP-Polarfuchs', tokens: { A: 'A', C: 'cchm', D: 'D', E: 'e', G: 'g', P: 'P' }, image: 'polarfuchs-cp.jpg' }, + { name: 'CP-Polarfuchs-Hell', tokens: { A: 'A', C: 'cchm/ch', D: 'D', E: 'e', G: 'g', P: 'P' } }, { name: 'CP-Fuchs', tokens: { A: 'A', C: 'cchm', D: 'd', E: 'e', G: 'G', P: 'P' } }, { name: 'CP-Fuchs-Hell', tokens: { A: 'A', C: 'cchm/ch', D: 'd', E: 'e', G: 'G', P: 'P' } }, { name: 'CP-Blaufuchs', tokens: { A: 'A', C: 'cchm', D: 'd', E: 'e', G: 'g', P: 'P' } }, { name: 'CP-Orangeschimmel', tokens: { C: 'cchm', D: 'D', E: 'ef', G: 'G', P: 'P' } }, + { name: 'CP-Orangeschimmel-Hell', tokens: { C: 'cchm/ch', D: 'D', E: 'ef', G: 'G', P: 'P' } }, ] export const UNKNOWN_FARBSCHLAG = 'Unbekannter Farbschlag' @@ -207,12 +209,14 @@ function baseColourFor(g: Genotype): string | null { } /** - * GEN-3e: the C-locus colourpoint NAMING transform (breeder-authoritative). + * GEN-3e/3g: the C-locus colourpoint NAMING transform (breeder-authoritative). * Returns the colourpoint name, or null when it doesn't apply (full C present, * or chch — which the base matcher names Hermelin/Himalaya, preserving both). * aa cchm/cchm -> Marder | aa cchm/ch -> Siam * aa cchm/cchm gg -> Zobel | aa cchm/ch gg -> Zobel-Hell - * A- cchm/cchm | cchm/ch -> CP- (base computed as if C were full) + * A- cchm/cchm -> CP- | A- cchm/ch -> CP--Hell + * GEN-3g (breeder rule): "-Hell" in variety name == c[h]-Allel (cchm/ch het); + * no "-Hell" == cchm/cchm hom. CP-Fuchs is a Sammelbegriff (unknown loci). */ function colourpointName(g: Genotype): string | null { const c = resolvedPair(g, 'C') @@ -227,9 +231,9 @@ function colourpointName(g: Genotype): string | null { if (grey) return bothCchm ? 'Zobel' : 'Zobel-Hell' return bothCchm ? 'Marder' : 'Siam' } - // A- colourpoint -> CP-, base as if C were full. + // A- colourpoint: base as if C were full; het (cchm/ch) -> '-Hell' suffix. const base = baseColourFor(makeGenotype({ ...g, C: ['C', 'C'] })) - return base ? `CP-${base}` : null + return base ? `CP-${base}${bothCchm ? '' : '-Hell'}` : null } export function farbschlagFor(g: Genotype): FarbschlagMatch { diff --git a/gerbil-manager-web/src/genetics/colorVarietySeed.generated.json b/gerbil-manager-web/src/genetics/colorVarietySeed.generated.json index 386c585..c696cbc 100644 --- a/gerbil-manager-web/src/genetics/colorVarietySeed.generated.json +++ b/gerbil-manager-web/src/genetics/colorVarietySeed.generated.json @@ -340,42 +340,67 @@ "sortOrder": 53, "image": "agouti-cp.jpg" }, + { + "name": "CP-Agouti-Hell", + "canonicalGenotype": "AA cchmch DD EE GG PP spsp rere", + "sortOrder": 54 + }, { "name": "CP-Silberagouti", "canonicalGenotype": "AA cchmcchm DD EE gg PP spsp rere", - "sortOrder": 54, + "sortOrder": 55, "image": "silberagouti-cp.JPG" }, + { + "name": "CP-Silberagouti-Hell", + "canonicalGenotype": "AA cchmch DD EE gg PP spsp rere", + "sortOrder": 56 + }, { "name": "CP-Algierfuchs", "canonicalGenotype": "AA cchmcchm DD ee GG PP spsp rere", - "sortOrder": 55, + "sortOrder": 57, "image": "algierfuchs-cp.jpg" }, + { + "name": "CP-Algierfuchs-Hell", + "canonicalGenotype": "AA cchmch DD ee GG PP spsp rere", + "sortOrder": 58 + }, { "name": "CP-Polarfuchs", "canonicalGenotype": "AA cchmcchm DD ee gg PP spsp rere", - "sortOrder": 56, + "sortOrder": 59, "image": "polarfuchs-cp.jpg" }, + { + "name": "CP-Polarfuchs-Hell", + "canonicalGenotype": "AA cchmch DD ee gg PP spsp rere", + "sortOrder": 60 + }, { "name": "CP-Fuchs", "canonicalGenotype": "AA cchmcchm dd ee GG PP spsp rere", - "sortOrder": 57 + "sortOrder": 61 }, { "name": "CP-Fuchs-Hell", "canonicalGenotype": "AA cchmch dd ee GG PP spsp rere", - "sortOrder": 58 + "sortOrder": 62 }, { "name": "CP-Blaufuchs", "canonicalGenotype": "AA cchmcchm dd ee gg PP spsp rere", - "sortOrder": 59 + "sortOrder": 63 }, { "name": "CP-Orangeschimmel", "canonicalGenotype": "AA cchmcchm DD efef GG PP spsp rere", - "sortOrder": 60 + "sortOrder": 64 + }, + { + "name": "CP-Orangeschimmel-Hell", + "canonicalGenotype": "AA cchmch DD efef GG PP spsp rere", + "sortOrder": 65 } ]