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
}
]