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', },