dd-timer/tests/unit/domain/StockSimulator.test.ts
POL Mickaël 203c423f19 test: 302 tests unitaires + 20 E2E Playwright (couverture 94%)
- Unit : domain (GaugeCalculator, Enclos, Dragodinde, XpTable, Race, Tier...)
- Unit : application (commands, queries, CommandBus)
- Fonctionnel : breeding-workflow, enclos-management, timer-workflow
- Régression : gauge-tier, gauge-recharge, xp-timer, level-target, breeding
- E2E Playwright + Electron : navigation, timer, recharge jauge,
  accouplement, persistance des données

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 05:43:29 +02:00

164 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, it, expect } from 'vitest';
import { simulateStock } from '@domain/services/StockSimulator';
describe('simulateStock', () => {
it('stock vide → aucun croisement', () => {
const r = simulateStock({});
expect(r.crossings).toHaveLength(0);
expect(r.unusedStock).toHaveLength(0);
});
it('stock à zéro → aucun croisement', () => {
const r = simulateStock({ Rousse: { m: 0, f: 0 } });
expect(r.crossings).toHaveLength(0);
});
it('♂Rousse + ♀Dorée → Dorée et Rousse ×1 (config1)', () => {
const r = simulateStock({ Rousse: { m: 1, f: 0 }, Dorée: { m: 0, f: 1 } });
expect(r.crossings).toHaveLength(1);
expect(r.crossings[0]!.baby).toBe('Dorée et Rousse');
expect(r.crossings[0]!.count).toBe(1);
expect(r.crossings[0]!.pAMale).toBe(1); // ♂Rousse
expect(r.crossings[0]!.pAFemale).toBe(0);
expect(r.crossings[0]!.pBFemale).toBe(1); // ♀Dorée
expect(r.crossings[0]!.pBMale).toBe(0);
});
it('♀Rousse + ♂Dorée → Dorée et Rousse ×1 (config2)', () => {
const r = simulateStock({ Rousse: { m: 0, f: 1 }, Dorée: { m: 1, f: 0 } });
expect(r.crossings).toHaveLength(1);
expect(r.crossings[0]!.baby).toBe('Dorée et Rousse');
expect(r.crossings[0]!.count).toBe(1);
expect(r.crossings[0]!.pAMale).toBe(0);
expect(r.crossings[0]!.pAFemale).toBe(1); // ♀Rousse config2
});
it('♂♂Rousse + ♀♀Dorée (même sexe d\'un côté) → 2 bébés config1 seulement', () => {
const r = simulateStock({ Rousse: { m: 2, f: 0 }, Dorée: { m: 0, f: 2 } });
expect(r.crossings).toHaveLength(1);
expect(r.crossings[0]!.count).toBe(2);
expect(r.crossings[0]!.pAMale).toBe(2); // 2× ♂Rousse × ♀Dorée
expect(r.crossings[0]!.pAFemale).toBe(0); // pas de config2
});
it('les deux configs simultanées : 2♂/2♀ × 2♂/2♀ → 4 bébés', () => {
const r = simulateStock({ Rousse: { m: 2, f: 2 }, Dorée: { m: 2, f: 2 } });
const c = r.crossings.find(x => x.baby === 'Dorée et Rousse');
expect(c).toBeDefined();
expect(c!.count).toBe(4);
expect(c!.pAMale).toBe(2); // c1 = min(♂R=2, ♀D=2)
expect(c!.pAFemale).toBe(2); // c2 = min(♀R=2, ♂D=2)
});
it('distribution proportionnelle : 4♂/4♀ × 3 races → 4 bébés par croisement Gen2', () => {
const r = simulateStock({
Rousse: { m: 4, f: 4 },
Dorée: { m: 4, f: 4 },
Amande: { m: 4, f: 4 },
});
const gen2 = r.crossings.filter(c => c.gen === 2);
expect(gen2).toHaveLength(3);
for (const c of gen2) {
expect(c.count).toBe(4);
}
});
it('cascade complète Gen2→Gen3→Gen4→Gen5 avec 4♂/4♀ × 3 races', () => {
const r = simulateStock({
Rousse: { m: 4, f: 4 },
Dorée: { m: 4, f: 4 },
Amande: { m: 4, f: 4 },
});
// Gen2 : AmR×4, DorR×4, AmD×4 = 12
const gen2 = r.crossings.filter(c => c.gen === 2);
expect(gen2.reduce((s, c) => s + c.count, 0)).toBe(12);
// Gen3 : Ebène×2, Indigo×2 = 4
const gen3 = r.crossings.filter(c => c.gen === 3);
expect(gen3).toHaveLength(2);
expect(gen3.find(c => c.baby === 'Ebène')!.count).toBe(2);
expect(gen3.find(c => c.baby === 'Indigo')!.count).toBe(2);
// Gen4 : Ebène et Indigo×2
const gen4 = r.crossings.filter(c => c.gen === 4);
expect(gen4).toHaveLength(1);
expect(gen4[0]!.baby).toBe('Ebène et Indigo');
expect(gen4[0]!.count).toBe(2);
// Gen5 : Orchidée×2 (Pourpre ignoré car EI allocation = 0)
const gen5 = r.crossings.filter(c => c.gen === 5);
expect(gen5).toHaveLength(1);
expect(gen5[0]!.baby).toBe('Orchidée');
expect(gen5[0]!.count).toBe(2);
// Total : 12 + 4 + 2 + 2 = 20
const total = r.crossings.reduce((s, c) => s + c.count, 0);
expect(total).toBe(20);
});
it('allocation proportionnelle donne la priorité au second croisement quand le premier reçoit 0', () => {
// EI participe à Pourpre (1er) et Orchidée (2nd) en Gen5
// Avec 1♂/1♀ EI, alloc = floor(1/2) = 0 pour Pourpre → 0 bébés
// Orchidée : EI n'a plus qu'1 croisement restant → alloc = 1 → 2 bébés
const r = simulateStock({
'Ebène et Indigo': { m: 1, f: 1 },
'Amande et Rousse': { m: 1, f: 1 },
'Dorée et Rousse': { m: 1, f: 1 },
});
expect(r.crossings.find(c => c.baby === 'Pourpre')).toBeUndefined();
expect(r.crossings.find(c => c.baby === 'Orchidée')?.count).toBe(2);
});
it('race sans partenaire → stock inutilisé', () => {
const r = simulateStock({ Rousse: { m: 5, f: 5 } });
expect(r.crossings).toHaveLength(0);
expect(r.unusedStock).toHaveLength(1);
expect(r.unusedStock[0]!.race).toBe('Rousse');
expect(r.unusedStock[0]!.m).toBe(5);
expect(r.unusedStock[0]!.f).toBe(5);
});
it('stock restant après simulation correctement identifié', () => {
// 1♂ Rousse + 1♀ Dorée → 1 DorR. Rousse ♀ = 3 restantes
const r = simulateStock({ Rousse: { m: 1, f: 3 }, Dorée: { m: 0, f: 1 } });
expect(r.crossings).toHaveLength(1);
expect(r.crossings[0]!.count).toBe(1);
const leftR = r.unusedStock.find(u => u.race === 'Rousse');
expect(leftR).toBeDefined();
expect(leftR!.f).toBe(3);
});
it('bébés répartis ♂/♀ pour les générations suivantes', () => {
// 1♂/1♀ Rousse + 1♂/1♀ Dorée → 2 DorR (1♂, 1♀)
// 1♂/1♀ Amande + 1♂/1♀ Rousse_new? Non, Amande n'a pas de stock
const r = simulateStock({ Rousse: { m: 1, f: 1 }, Dorée: { m: 1, f: 1 } });
expect(r.crossings).toHaveLength(1);
expect(r.crossings[0]!.count).toBe(2);
// Bébés DorR dans unused : 1♂ + 1♀
const baby = r.unusedStock.find(u => u.race === 'Dorée et Rousse');
expect(baby).toBeDefined();
expect(baby!.m).toBe(1);
expect(baby!.f).toBe(1);
});
it('ne modifie pas l\'inventaire d\'entrée (immutabilité)', () => {
const inv = { Rousse: { m: 2, f: 2 }, Dorée: { m: 2, f: 2 } };
simulateStock(inv);
expect(inv.Rousse.m).toBe(2);
expect(inv.Rousse.f).toBe(2);
expect(inv.Dorée.m).toBe(2);
expect(inv.Dorée.f).toBe(2);
});
it('stock asymétrique : 3♂/1♀ Rousse + 1♂/3♀ Dorée', () => {
const r = simulateStock({ Rousse: { m: 3, f: 1 }, Dorée: { m: 1, f: 3 } });
const c = r.crossings.find(x => x.baby === 'Dorée et Rousse');
expect(c).toBeDefined();
// c1 = min(♂R=3, ♀D=3) = 3, c2 = min(♀R=1, ♂D=1) = 1 → bred = 4
expect(c!.count).toBe(4);
expect(c!.pAMale).toBe(3);
expect(c!.pAFemale).toBe(1);
});
});