GEN-4: Dilute-Präfix, REW-Erkennung, CP-Fuchs-Fix, BreedingResultView Gencode+Layout
Katalog (catalog.ts): - 8 'X dd'/'dd X' Einträge → 'Dilute X' (Agouti/Silberagouti/Kohlfuchs/Anthrazit/ Topas/Blaufuchs dd + dd Gold/Platin → Dilute Gold/Platin etc.) - 4 neue Dilute-Fuchs-Basiseinträge (Dilute Algierfuchs/Goldfuchs/Rotfuchs/Polarfuchs) → verhindert naked 'Fuchs' Fallback für agouti+dilute+fox Genotypen. 66→70 Einträge. - colourpointName: 'Dilute X' Base → 'Dilute CP-X' statt 'CP-Dilute X' (Präfix-Reihenfolge) → AA cchmcchm dd ee GG PP = 'Dilute CP-Algierfuchs' statt 'CP-Fuchs'. Engine (farbschlagFor): - REW-Check: c0==cchm && c1==cchm && p0==p && p1==p → 'REW' (Rotaugenweiß). Unabhängig von A/D/E/G. Flagged to god zur Bestätigung. BreedingResultView: - Gencode (bracket notation) pro Farbschlag-Karte (erster/wahrscheinlichster Genotyp). - Layout-Fix: flexbox body (name+gencode gestapelt), min-width:0 gegen Overflow, word-break, grid min 15rem, prob flex-shrink:0 rechtsbündig. Tests: GEN-4 describe (Dilute/REW/no-bare-Fuchs Fixtures); CATALOG_SIZE 66→70; CP-Fuchs/CP-Fuchs-Hell → Dilute CP-Algierfuchs(-Hell) in bestehenden Tests. Gate: build ✓ eslint ✓ vitest 95/95 ✓ e2e 148/148 ✓
This commit is contained in:
@@ -43,16 +43,29 @@ export default function BreedingResultView({ result, title }: BreedingResultView
|
||||
) : (
|
||||
<>
|
||||
<ul className="farbschlag-cards">
|
||||
{result.byFarbschlag.map((f) => (
|
||||
<li key={f.farbschlag} className="farbschlag-card">
|
||||
<FarbschlagImage name={f.farbschlag} />
|
||||
<span className="farbschlag-card__name">{f.farbschlag}</span>
|
||||
<span className="farbschlag-card__prob">
|
||||
{f.probability.percent}
|
||||
<small> ({f.probability.text})</small>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
{result.byFarbschlag.map((f) => {
|
||||
// Representative genotype: highest-prob offspring for this farbschlag.
|
||||
const repGenotype = result.offspring.find(
|
||||
(o) => o.farbschlag === f.farbschlag,
|
||||
)?.genotype
|
||||
return (
|
||||
<li key={f.farbschlag} className="farbschlag-card">
|
||||
<div className="farbschlag-card__img">
|
||||
<FarbschlagImage name={f.farbschlag} />
|
||||
</div>
|
||||
<div className="farbschlag-card__body">
|
||||
<span className="farbschlag-card__name">{f.farbschlag}</span>
|
||||
{repGenotype && (
|
||||
<code className="farbschlag-card__geno">{repGenotype}</code>
|
||||
)}
|
||||
</div>
|
||||
<span className="farbschlag-card__prob">
|
||||
{f.probability.percent}
|
||||
<small> ({f.probability.text})</small>
|
||||
</span>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
|
||||
<button
|
||||
|
||||
@@ -179,11 +179,9 @@ describe('Farbschlag catalog', () => {
|
||||
expect(match.name).toBe('Unbekannter Farbschlag')
|
||||
})
|
||||
|
||||
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('has the expected catalogue coverage (GEN-4: 70 after adding 4 dilute-fox entries)', () => {
|
||||
// GEN-3f: 73 -> 61. GEN-3g: +5 -> 66. GEN-4: +4 (Dilute Algierfuchs/Goldfuchs/Rotfuchs/Polarfuchs) -> 70.
|
||||
expect(CATALOG_SIZE).toBe(70)
|
||||
})
|
||||
|
||||
it('frozen contract names round-trip to themselves (DB-key guard)', () => {
|
||||
@@ -421,7 +419,8 @@ describe('GEN-3e: C-locus colourpoint naming', () => {
|
||||
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')
|
||||
// GEN-4: dd base = 'Dilute Algierfuchs' → 'Dilute CP-Algierfuchs-Hell'
|
||||
expect(name('AA cchmch dd ee GG PP spsp rere')).toBe('Dilute CP-Algierfuchs-Hell')
|
||||
})
|
||||
|
||||
it('aa fixed colourpoint names (Marder/Siam/Zobel/Zobel-Hell)', () => {
|
||||
@@ -471,8 +470,8 @@ describe('GEN-3f: CP catalog reconciled to the breeder CP- naming (matches her l
|
||||
expect(name('AA cchmcchm DD EE gg PP spsp rere')).toBe('CP-Silberagouti')
|
||||
expect(name('AA cchmcchm DD ee GG PP spsp rere')).toBe('CP-Algierfuchs')
|
||||
expect(name('AA cchmcchm DD ee gg PP spsp rere')).toBe('CP-Polarfuchs')
|
||||
// dd agouti fox has no dedicated base -> the eFamily fallback yields 'CP-Fuchs'.
|
||||
expect(name('AA cchmcchm dd ee GG PP spsp rere')).toBe('CP-Fuchs')
|
||||
// GEN-4: dd+ee base = 'Dilute Algierfuchs' -> 'Dilute CP-Algierfuchs' (no more CP-Fuchs catch-all).
|
||||
expect(name('AA cchmcchm dd ee GG PP spsp rere')).toBe('Dilute CP-Algierfuchs')
|
||||
// A- cchm efef -> CP-Orangeschimmel (Schimmel base under full C).
|
||||
expect(name('AA cchmcchm DD efef GG PP spsp rere')).toBe('CP-Orangeschimmel')
|
||||
})
|
||||
@@ -481,11 +480,12 @@ describe('GEN-3f: CP catalog reconciled to the breeder CP- naming (matches her l
|
||||
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')!
|
||||
// GEN-4: CP-Fuchs-Hell kept for import/hand-pick but engine now returns the specific name.
|
||||
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')
|
||||
expect(genotypeToFarbschlag(representativeGenotype(cpfh))).toBe('Dilute CP-Algierfuchs-Hell')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -507,7 +507,8 @@ describe('GEN-3g: "-Hell" in variety name == cchm/ch het; hom == cchm/cchm', ()
|
||||
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')
|
||||
// GEN-4: dd+ee base = 'Dilute Algierfuchs' → prefix ordering: 'Dilute CP-Algierfuchs-Hell'
|
||||
expect(name('AA cchmch dd ee GG PP spsp rere')).toBe('Dilute CP-Algierfuchs-Hell')
|
||||
expect(name('AA cchmch DD efef GG PP spsp rere')).toBe('CP-Orangeschimmel-Hell')
|
||||
})
|
||||
|
||||
@@ -525,6 +526,36 @@ describe('GEN-3g: "-Hell" in variety name == cchm/ch het; hom == cchm/cchm', ()
|
||||
})
|
||||
})
|
||||
|
||||
describe('GEN-4: Dilute prefix, REW, no-bare-Fuchs', () => {
|
||||
const name = (s: string) => genotypeToFarbschlag(fromDisplayString(s))
|
||||
|
||||
it('dd entries use Dilute prefix — no more X-dd names', () => {
|
||||
expect(name('AA CC dd EE GG PP spsp rere')).toBe('Dilute Agouti')
|
||||
expect(name('aa CC dd EE gg PP spsp rere')).toBe('Dilute Anthrazit')
|
||||
expect(name('aa CC dd ee GG PP spsp rere')).toBe('Dilute Kohlfuchs')
|
||||
expect(name('aa CC dd ee gg pp spsp rere')).toBe('Dilute Blaufuchs')
|
||||
expect(name('AA CC dd EE GG pp spsp rere')).toBe('Dilute Gold')
|
||||
expect(name('aa CC dd EE GG pp spsp rere')).toBe('Dilute Platin')
|
||||
})
|
||||
|
||||
it('REW: cchm/cchm + pp = REW regardless of A/D/E/G', () => {
|
||||
expect(name('AA cchmcchm DD EE GG pp spsp rere')).toBe('REW') // CP-Gold
|
||||
expect(name('AA cchmcchm DD ee GG pp spsp rere')).toBe('REW') // CP-Goldfuchs
|
||||
expect(name('AA cchmcchm DD EE gg pp spsp rere')).toBe('REW') // CP-Elfenbein
|
||||
expect(name('AA cchmcchm DD ee gg pp spsp rere')).toBe('REW') // CP-Apricot
|
||||
expect(name('AA cchmcchm dd EE GG pp spsp rere')).toBe('REW') // CP-dd Gold
|
||||
// cchm/ch (het) + pp is NOT REW (only homozygous cchm/cchm qualifies)
|
||||
expect(name('AA cchmch DD EE GG pp spsp rere')).not.toBe('REW')
|
||||
})
|
||||
|
||||
it('bare Fuchs never appears — dilute-fox combinations are named specifically', () => {
|
||||
expect(name('AA CC dd ee GG PP spsp rere')).toBe('Dilute Algierfuchs')
|
||||
expect(name('AA CC dd ee GG pp spsp rere')).toBe('Dilute Goldfuchs')
|
||||
expect(name('aa CC dd ee GG pp spsp rere')).toBe('Dilute Rotfuchs')
|
||||
expect(name('AA CC dd ee gg PP spsp rere')).toBe('Dilute Polarfuchs')
|
||||
})
|
||||
})
|
||||
|
||||
describe('GEN-3h: breeder bracket-notation display + E-locus e-before-ef order', () => {
|
||||
// ── Display symbols ────────────────────────────────────────────────────
|
||||
it('ef displays as e[f], cchm as c[chm], ch as c[h]', () => {
|
||||
|
||||
@@ -66,8 +66,8 @@ export const BASE_COLORS: readonly FarbschlagEntry[] = [
|
||||
{ name: 'Platin', english: 'Lilac', tokens: { A: 'a', C: 'C', D: 'D', E: 'E', G: 'G', P: 'p' }, image: 'platin.JPG' },
|
||||
{ name: 'Goldfuchs', english: 'Yellow Fox', tokens: { A: 'A', C: 'C', D: 'D', E: 'e', G: 'G', P: 'p' }, image: 'goldfuchs.jpg' },
|
||||
{ name: 'Rotfuchs', english: 'Argente Nutmeg', tokens: { A: 'a', C: 'C', D: 'D', E: 'e', G: 'G', P: 'p' }, image: 'rotfuchs.JPG' },
|
||||
{ name: 'dd Gold', english: 'dd Argente Golden', tokens: { A: 'A', C: 'C', D: 'd', E: 'E', G: 'G', P: 'p' }, image: 'gold-dd.jpg' },
|
||||
{ name: 'dd Platin', english: 'dd Lilac', tokens: { A: 'a', C: 'C', D: 'd', E: 'E', G: 'G', P: 'p' }, image: 'platin-dd.jpg' },
|
||||
{ name: 'Dilute Gold', english: 'dd Argente Golden', tokens: { A: 'A', C: 'C', D: 'd', E: 'E', G: 'G', P: 'p' }, image: 'gold-dd.jpg' },
|
||||
{ name: 'Dilute Platin', english: 'dd Lilac', tokens: { A: 'a', C: 'C', D: 'd', E: 'E', G: 'G', P: 'p' }, image: 'platin-dd.jpg' },
|
||||
|
||||
// ── baseportal.de varieties (GEN-2), normalized to the frozen allele table ──
|
||||
{ name: 'Altweiss (REW)', tokens: { A: 'a', C: 'C', D: 'D', E: 'E', G: 'g', P: 'p' }, image: 'altweiss-rew.jpeg' },
|
||||
@@ -82,10 +82,16 @@ export const BASE_COLORS: readonly FarbschlagEntry[] = [
|
||||
{ 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' },
|
||||
{ name: 'Silberagouti dd', tokens: { A: 'A', C: 'C', D: 'd', E: 'E', G: 'g', P: 'P' }, image: 'silberagouti-dd.jpg' },
|
||||
{ name: 'Kohlfuchs dd', tokens: { A: 'a', C: 'C', D: 'd', E: 'e', G: 'G', P: 'P' }, image: 'kohlfuchs-dd.jpg' },
|
||||
{ name: 'Anthrazit dd', tokens: { A: 'a', C: 'C', D: 'd', E: 'E', G: 'g', P: 'P' }, image: 'anthrazit-dd.jpg' },
|
||||
{ name: 'Dilute Agouti', tokens: { A: 'A', C: 'C', D: 'd', E: 'E', G: 'G', P: 'P' }, image: 'agouti-dd.jpg' },
|
||||
{ name: 'Dilute Silberagouti', tokens: { A: 'A', C: 'C', D: 'd', E: 'E', G: 'g', P: 'P' }, image: 'silberagouti-dd.jpg' },
|
||||
{ name: 'Dilute Kohlfuchs', tokens: { A: 'a', C: 'C', D: 'd', E: 'e', G: 'G', P: 'P' }, image: 'kohlfuchs-dd.jpg' },
|
||||
{ name: 'Dilute Anthrazit', tokens: { A: 'a', C: 'C', D: 'd', E: 'E', G: 'g', P: 'P' }, image: 'anthrazit-dd.jpg' },
|
||||
// GEN-4: dilute fox base entries — prevent bare 'Fuchs' family fallback for
|
||||
// agouti+dilute+fox combinations not otherwise covered in the catalog.
|
||||
{ name: 'Dilute Algierfuchs', tokens: { A: 'A', C: 'C', D: 'd', E: 'e', G: 'G', P: 'P' } },
|
||||
{ name: 'Dilute Goldfuchs', tokens: { A: 'A', C: 'C', D: 'd', E: 'e', G: 'G', P: 'p' } },
|
||||
{ name: 'Dilute Rotfuchs', tokens: { A: 'a', C: 'C', D: 'd', E: 'e', G: 'G', P: 'p' } },
|
||||
{ name: 'Dilute Polarfuchs', tokens: { A: 'A', C: 'C', D: 'd', E: 'e', G: 'g', P: 'P' } },
|
||||
// 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' },
|
||||
@@ -104,8 +110,8 @@ export const BASE_COLORS: readonly FarbschlagEntry[] = [
|
||||
{ name: 'Rotfuchs, hell', tokens: { A: 'a', C: 'C', D: 'D', E: 'e', G: 'G', P: 'p' }, image: 'rotfuchs-hell.jpg' },
|
||||
{ name: 'Kohlfuchs-Hell', tokens: { A: 'a', C: 'C', D: 'D', E: 'e', G: 'G', P: 'P' }, image: 'kohlfuchs-hell-2.jpg' },
|
||||
{ name: 'Algierfuchs, hell', tokens: { A: 'A', C: 'C', D: 'D', E: 'e', G: 'G', P: 'P' }, image: 'algierfuchs-hell.JPG' },
|
||||
{ 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' },
|
||||
{ name: 'Dilute Topas', tokens: { A: 'A', C: 'C', D: 'd', E: 'E', G: 'G', P: 'p' }, image: 'topas-dd.jpg' },
|
||||
{ name: 'Dilute Blaufuchs', tokens: { A: 'a', C: 'C', D: 'd', E: 'e', G: 'g', P: 'p' }, image: 'blaufuchs-dd.jpg' },
|
||||
|
||||
// ── GEN-3f/3g: c^chm colourpoint varieties ──
|
||||
// GEN-3f: aa points = marten/sable group (Marder/Siam, +gg Zobel/Zobel-Hell).
|
||||
@@ -233,7 +239,13 @@ function colourpointName(g: Genotype): string | null {
|
||||
}
|
||||
// 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}${bothCchm ? '' : '-Hell'}` : null
|
||||
if (!base) return null
|
||||
// GEN-4: if base is a Dilute variety, prefix ordering is 'Dilute CP-X' not 'CP-Dilute X'.
|
||||
const DILUTE = 'Dilute '
|
||||
if (base.startsWith(DILUTE)) {
|
||||
return `${DILUTE}CP-${base.slice(DILUTE.length)}${bothCchm ? '' : '-Hell'}`
|
||||
}
|
||||
return `CP-${base}${bothCchm ? '' : '-Hell'}`
|
||||
}
|
||||
|
||||
export function farbschlagFor(g: Genotype): FarbschlagMatch {
|
||||
@@ -241,6 +253,17 @@ export function farbschlagFor(g: Genotype): FarbschlagMatch {
|
||||
if (locusToken(g, 'Sp') === 'Sp') modifiers.push('Schecke')
|
||||
if (locusToken(g, 'Re') === 'Re') modifiers.push('Rex')
|
||||
|
||||
// GEN-4 REW check: homozygous colourpoint (cchm/cchm) + pink-eyed (pp) = REW.
|
||||
// This combination removes both fur pigment (C locus) and eye pigment (P locus)
|
||||
// regardless of A/D/E/G. Examples: CP-Gold, CP-Goldfuchs, CP-Elfenbein, CP-Apricot.
|
||||
// Flagged to god for confirmation (condition: cchm×cchm + p×p).
|
||||
const [c0, c1] = resolvedPair(g, 'C')
|
||||
const [p0, p1] = resolvedPair(g, 'P')
|
||||
if (c0 === 'cchm' && c1 === 'cchm' && p0 === 'p' && p1 === 'p') {
|
||||
const name = ['REW', ...modifiers].join(' ')
|
||||
return { name, base: null, unknown: false }
|
||||
}
|
||||
|
||||
const baseName = colourpointName(g) ?? baseColourFor(g)
|
||||
if (!baseName) {
|
||||
return { name: UNKNOWN_FARBSCHLAG, base: null, unknown: true }
|
||||
|
||||
@@ -539,28 +539,57 @@ textarea {
|
||||
margin: 0.5rem 0 1rem;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
|
||||
/* GEN-4: wider min-width prevents long names (CP-Silberagouti-Hell) from overflowing */
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* GEN-4 layout fix: image left, body centre (name + genotype stacked), prob right.
|
||||
align-items: stretch so all three columns fill the card height consistently. */
|
||||
.farbschlag-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
padding: 0.6rem 0.9rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.6rem;
|
||||
background: var(--color-surface);
|
||||
overflow: hidden; /* contain long genotype strings */
|
||||
}
|
||||
|
||||
.farbschlag-card__img {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Body takes the middle space and allows wrapping for long names */
|
||||
.farbschlag-card__body {
|
||||
flex: 1;
|
||||
min-width: 0; /* allow flex child to shrink below content size */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.farbschlag-card__name {
|
||||
font-weight: 600;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Genotype code shown below the name — muted, small, wrappable */
|
||||
.farbschlag-card__geno {
|
||||
font-size: 0.7rem;
|
||||
color: var(--color-text-muted, #666);
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-all;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.farbschlag-card__prob {
|
||||
flex-shrink: 0;
|
||||
color: var(--color-accent);
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* UX-MOBILE-2: scroll wrapper so genotype table scrolls horizontally on mobile
|
||||
|
||||
Reference in New Issue
Block a user