fix(genetics): Genotyp-/Farbschlag-Engine-Bugs aus Ticket-Triage
- uw/Uwuw[d] (dichtes Underwhite) wird vor dem uw→G-Locus-Alias korrekt geparst (Vance: nicht mehr „Unbekannter Farbschlag", zeigt G-Locus statt international uw). [#34,#32] - ee[-] = sichtbarer Fuchs (rezessiv homozygot) → korrekt als Fuchs; bare „e-" (rezessiv + unbekannt) ist genetisch unmöglich → genotypeInvalid statt still falsch. [#42] - aa-Colourpoint-Zweig berücksichtigt jetzt D (Dilute) und E (Fuchs), statt fix Zobel/Marder/Siam zu erzwingen. [#3] - Wurf-Farbprognose: unbekanntes Allel (?) wird nicht mehr über die volle Dominanz expandiert → keine unmöglichen Dilute-/Schimmel-/„Unbekannt"-Nachkommen. [#37,#39,#40,#41,#38] Tests: test_genotype.py erweitert; genetics.test.ts +5 Blöcke (135 vitest grün, tsc/eslint sauber). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -291,6 +291,62 @@ describe('GEN-3a: Uw=G alias', () => {
|
||||
)
|
||||
expect(toDisplayString(fromDisplayString('AA CC DD EE uwuw PP spsp rere'))).not.toContain('uw')
|
||||
})
|
||||
|
||||
it('#32/#34: dense-underwhite [d] annotation is stripped (Uwuw[d] → Gg, never uw)', () => {
|
||||
// Vance's real genotype: "aa Cc[chm] D- ee Uwuw[d] PP spsp" → Kohlfuchs, not Unbekannt.
|
||||
expect(fromDisplayString('AA CC DD EE Uwuw[d] PP spsp rere').G).toEqual(['G', 'g'])
|
||||
expect(fromDisplayString('AA CC DD EE uw[d]uw[d] PP spsp rere').G).toEqual(['g', 'g'])
|
||||
const vance = fromDisplayString('aa Cc[chm] D- ee Uwuw[d] PP spsp')
|
||||
expect(vance.G).toEqual(['G', 'g'])
|
||||
const display = toDisplayString(vance)
|
||||
expect(display).toContain('Gg')
|
||||
expect(display.toLowerCase()).not.toContain('uw')
|
||||
expect(genotypeToFarbschlag(vance)).toBe('Kohlfuchs')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#42: E-locus Algierfuchs (ee[-] visible Fuchs)', () => {
|
||||
it('Aa CC D- ee[-] Gg Pp spsp → Algierfuchs', () => {
|
||||
expect(genotypeToFarbschlag(fromDisplayString('Aa CC D- ee[-] Gg Pp spsp'))).toBe('Algierfuchs')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#3: aa colourpoint respects D (dilute) and E (Fuchs)', () => {
|
||||
const name = (s: string) => genotypeToFarbschlag(fromDisplayString(s))
|
||||
it('does NOT collapse aa colourpoint with dd/Fuchs to Zobel', () => {
|
||||
expect(name('aa c[chm]c[chm] dd ee[-] gg Pp Spsp')).not.toBe('Zobel')
|
||||
expect(name('aa c[chm]c[chm] dd ee[-] gg Pp Spsp')).not.toBe('Zobel Schecke')
|
||||
})
|
||||
it('aa cchm with full D + full extension still resolves to Marder/Zobel (regression)', () => {
|
||||
expect(name('aa cchmcchm DD EE GG PP spsp rere')).toBe('Marder')
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#37/#39/#40/#41: wildcard expansion is dominance/visibility constrained', () => {
|
||||
it('an unknown E partner of a visible E never introduces ef (no phantom Schimmel)', () => {
|
||||
// Mamta Mini (Ee[-] = [E,?]) × Gold (Ee): no Schimmel/efef/Unbekannt offspring.
|
||||
const r = breed(
|
||||
fromDisplayString('AA CC D- Ee[-] Gg PP spsp'),
|
||||
fromDisplayString('Aa CC D- Ee Gg pp spsp'),
|
||||
)
|
||||
const names = r.byFarbschlag.map((f) => f.farbschlag)
|
||||
expect(names).not.toContain('Unbekannter Farbschlag')
|
||||
expect(names.some((n) => /schimmel/i.test(n))).toBe(false)
|
||||
expect(r.offspring.every((o) => !o.genotype.includes('e[f]'))).toBe(true)
|
||||
})
|
||||
|
||||
it('a fox parent (ee) × Ee never yields impossible non-fox-only morphs from a wildcard', () => {
|
||||
// Geely (ee gg, with A?/C?/D? wildcards) × Gaida: no Unbekannt, no Schimmel.
|
||||
const r = breed(
|
||||
fromDisplayString('A- C- D- ee[-] gg Pp spsp'),
|
||||
fromDisplayString('A- CC D- Ee GG Pp spsp'),
|
||||
)
|
||||
const names = r.byFarbschlag.map((f) => f.farbschlag)
|
||||
expect(names).not.toContain('Unbekannter Farbschlag')
|
||||
expect(names.some((n) => /schimmel/i.test(n))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GEN-3a: second spotting locus Sls (WP)', () => {
|
||||
@@ -670,22 +726,23 @@ describe('GEN-3h: breeder bracket-notation display + E-locus e-before-ef order',
|
||||
)
|
||||
})
|
||||
|
||||
it('e[-] standalone: parses as [e,?], displays e-', () => {
|
||||
const g = fromDisplayString('aa CC DD e[-] GG PP spsp rere')
|
||||
expect(g.E).toEqual(['e', '?'])
|
||||
expect(toDisplayString(g)).toBe('aa CC DD e- GG PP spsp')
|
||||
it('#42: e[-]/e- standalone is INVALID (lone recessive fox + unknown) → throws', () => {
|
||||
// Fox (e) is recessive; a single e with an unknown partner is genetically
|
||||
// impossible. The breeder's rule: only "E-" and "ee[-]" exist, never "e-".
|
||||
expect(() => fromDisplayString('aa CC DD e[-] GG PP spsp rere')).toThrow()
|
||||
expect(() => fromDisplayString('aa CC DD e- GG PP spsp rere')).toThrow()
|
||||
})
|
||||
|
||||
it('CR-1a: Silvain oracle ee[-] parses without crash → [e,?], displays e-', () => {
|
||||
// Real herdbook notation: ee[-] = fox allele e + unknown e-type second allele.
|
||||
// The lookbehind rule strips the second e[-] → '?', leaving 'e?' for splitToken.
|
||||
it('#42: ee[-] resolves to ee (Fuchs) — recessive phenotype implies homozygosity', () => {
|
||||
// Real herdbook notation: ee[-] = visible fox. Since fox is recessive a
|
||||
// visible fox MUST be ee, so the unknown second allele resolves to e.
|
||||
const input = 'aa c[chm]c[chm] Dd ee[-] Gg Pp Spsp'
|
||||
const g = fromDisplayString(input)
|
||||
expect(g.E).toEqual(['e', '?'])
|
||||
expect(g.E).toEqual(['e', 'e'])
|
||||
expect(g.C).toEqual(['cchm', 'cchm'])
|
||||
expect(g.D).toEqual(['D', 'd'])
|
||||
expect(g.Sp).toEqual(['Sp', 'sp'])
|
||||
expect(toDisplayString(g)).toBe('aa c[chm]c[chm] Dd e- Gg Pp Spsp')
|
||||
expect(toDisplayString(g)).toBe('aa c[chm]c[chm] Dd ee Gg Pp Spsp')
|
||||
})
|
||||
|
||||
// ── GENOTYPE-PARSE-CRASH / displayGenotypeSafe resilience ───────────────
|
||||
|
||||
Reference in New Issue
Block a user