dd-timer/tests/unit/domain/GaugeCalculator.test.ts
POL Mickaël 3e485fd09b chore: normalise fins de ligne CRLF → LF dans tout le repo
Applique .gitattributes sur tous les fichiers existants.
Élimine les différences fantômes entre WSL et Windows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 08:55:10 +02:00

225 lines
8.9 KiB
TypeScript
Executable File
Raw Permalink 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 { gainedIn, timeToGain, gaugeAfter, elapsed, computeGaugeState, TimerState } from '@domain/services/GaugeCalculator';
import type { GaugeRecharge } from '@domain/services/GaugeCalculator';
describe('GaugeCalculator', () => {
describe('gainedIn', () => {
it('tier 1 only: 100 pts in 100 sec', () => {
// Level 100, 100 sec = 10 ticks, rate 10 → gain 100
expect(gainedIn(100, 100)).toBe(100);
});
it('zero seconds = zero gain', () => {
expect(gainedIn(50000, 0)).toBe(0);
});
it('level 0 = zero gain', () => {
expect(gainedIn(0, 100)).toBe(0);
});
it('crosses tier boundary 2→1', () => {
// Level 40010, 10 sec = 1 tick. In tier 2 (40001-70000), rate 20.
// But only 10 pts above 40000, so 10/20 = 0 full ticks at rate 20 → 0 gained from tier 2
// Actually: a = 40010-40000 = 10, m = floor(10/20) = 0, u = 0. Move to tier 1.
// Tier 1: g=40010 > 0, a=40010-0=40010... wait g is now 40010 still since u=0
// Let me recalculate: g=40010, tier {lo:40000, r:20}: a=10, m=0, u=0. No drain.
// Tier {lo:0, r:10}: g=40010 > 0, a=40010, m=4001, u=min(4001,1)=1, out=10
// Actually g should still be 40010 since we didn't drain. Hmm but the loop says if g<=lo continue
// g=40010 > 40000 → process. a=10, m=floor(10/20)=0, u=0.
// g=40010 > 0 → process. a=40010, m=floor(40010/10)=4001, u=min(4001,1)=1, out=10.
expect(gainedIn(40010, 10)).toBe(10);
});
it('full tier 4 drain', () => {
// Level 100000, 250 ticks needed to drain tier 4 (10000/40=250)
// 250 ticks = 2500 sec
expect(gainedIn(100000, 2500)).toBe(10000);
});
it('large gauge drains through multiple tiers', () => {
// Level 100000, 5000 sec = 500 ticks
// Tier 4: 100000→90000 = 10000/40 = 250 ticks → gain 10000, tl=250
// Tier 3: 90000→70000 = 20000/30 = 666 ticks, but only 250 left → 250*30 = 7500
// Total = 17500
expect(gainedIn(100000, 5000)).toBe(17500);
});
it('negative level clamped to 0', () => {
expect(gainedIn(-100, 100)).toBe(0);
});
it('level above 100000 clamped', () => {
expect(gainedIn(150000, 10)).toBe(40); // tier 4 rate
});
});
describe('timeToGain', () => {
it('zero points = zero time', () => {
expect(timeToGain(50000, 0)).toBe(0);
});
it('negative points = zero time', () => {
expect(timeToGain(50000, -10)).toBe(0);
});
it('tier 1: 100 pts needs 100 sec', () => {
// Level 100, need 100 pts at rate 10 → 10 ticks → 100 sec
expect(timeToGain(100, 100)).toBe(100);
});
it('tier 2: 1000 pts from level 50000', () => {
// Level 50000, tier 2 rate 20. 1000/20 = 50 ticks → 500 sec
expect(timeToGain(50000, 1000)).toBe(500);
});
it('returns Infinity if level is 0', () => {
expect(timeToGain(0, 100)).toBe(Infinity);
});
});
describe('gaugeAfter', () => {
it('no time = same level', () => {
expect(gaugeAfter(50000, 0)).toBe(50000);
});
it('tier 1 full drain', () => {
// Level 100, 100 sec = 10 ticks. 100/10=10 ticks needed. Exactly drains to 0.
expect(gaugeAfter(100, 100)).toBe(0);
});
it('does not go below 0', () => {
expect(gaugeAfter(50, 1000)).toBe(0);
});
it('partial drain in tier 2', () => {
// Level 50000, 10 sec = 1 tick. Rate 20 at tier 2. 50000-20 = 49980
expect(gaugeAfter(50000, 10)).toBe(49980);
});
});
describe('computeGaugeState', () => {
const NO_RECHARGES: GaugeRecharge[] = [];
it('sans recharge ni cap : identique à gainedIn', () => {
const el = 1000;
const startGl = 50000;
const { gained, curGl, effectiveEl } = computeGaugeState(startGl, NO_RECHARGES, Infinity, el);
expect(gained).toBe(gainedIn(startGl, el));
expect(curGl).toBe(gaugeAfter(startGl, el));
expect(effectiveEl).toBe(el);
});
it('cap à 0 dès le départ (stat déjà au max) → gel immédiat', () => {
const { gained, effectiveEl } = computeGaugeState(90000, NO_RECHARGES, 0, 5000);
expect(gained).toBe(0);
expect(effectiveEl).toBe(0);
});
it('cap atteint en cours de segment → gel précis', () => {
// Tier 2 (50000), rate 20/tick. On veut 200 pts → 10 ticks → 100 sec.
const { gained, effectiveEl } = computeGaugeState(50000, NO_RECHARGES, 200, 5000);
expect(gained).toBe(200);
expect(effectiveEl).toBe(100); // timeToGain(50000, 200) = 100 sec
});
it('une recharge avant la fin, pas de cap → somme des deux segments', () => {
// Seg1 : 50000, 100 sec → gainedIn(50000, 100) = 10 ticks × 20 = 200 pts
// Recharge à 80000 à t=100
// Seg2 : 80000, 100 sec → gainedIn(80000, 100) = 10 ticks × 30 = 300 pts
// Total = 500 pts
const recharges: GaugeRecharge[] = [{ atSec: 100, level: 80000 }];
const { gained, effectiveEl } = computeGaugeState(50000, recharges, Infinity, 200);
expect(gained).toBe(500);
expect(effectiveEl).toBe(200);
});
it('cap atteint dans le premier segment (avant la recharge)', () => {
// Seg1 : 50000, cap=100 pts → gel à timeToGain(50000,100)=50 sec
// Recharge à t=100 ne doit pas être prise en compte
const recharges: GaugeRecharge[] = [{ atSec: 100, level: 90000 }];
const { gained, effectiveEl } = computeGaugeState(50000, recharges, 100, 500);
expect(gained).toBe(100);
expect(effectiveEl).toBe(50); // gel avant la recharge
});
it('cap atteint dans le deuxième segment (après une recharge)', () => {
// Seg1 : 50000, 100 sec → 200 pts, cap=500 → pas encore atteint
// Recharge à 80000 à t=100
// Seg2 : 80000, cap restant=300 pts → timeToGain(80000,300)=100 sec → effectiveEl=100+100=200
const recharges: GaugeRecharge[] = [{ atSec: 100, level: 80000 }];
const { gained, effectiveEl } = computeGaugeState(50000, recharges, 500, 9999);
expect(gained).toBe(500);
expect(effectiveEl).toBe(200);
});
it('plusieurs recharges successives', () => {
// Seg1 : 10000 (tier1), 50 sec → 5 ticks × 10 = 50 pts
// Recharge à 50000 à t=50
// Seg2 : 50000 (tier2), 50 sec → 5 ticks × 20 = 100 pts
// Recharge à 90000 à t=100
// Seg3 : 90000 (tier3), 50 sec → 5 ticks × 30 = 150 pts
// Total = 300 pts
const recharges: GaugeRecharge[] = [
{ atSec: 50, level: 50000 },
{ atSec: 100, level: 90000 },
];
const { gained } = computeGaugeState(10000, recharges, Infinity, 150);
expect(gained).toBe(300);
});
it('recharge après le cap → le cap prime, recharge ignorée', () => {
// Cap à 50 pts → gel à 50 sec (tier2, rate20, 50/20=2.5→3 ticks=30sec... wait)
// timeToGain(50000, 50) = ceil(50/20)*10 = 3*10 = 30 sec
// Recharge à t=100 → ignorée car gel à t=30
const recharges: GaugeRecharge[] = [{ atSec: 100, level: 90000 }];
const { gained, effectiveEl } = computeGaugeState(50000, recharges, 50, 9999);
expect(gained).toBe(50);
expect(effectiveEl).toBe(30);
});
it('curGl reflète le niveau après le gel', () => {
// Tier2 (50000), cap=200 pts → 10 ticks → 100 sec, gauge = 50000-10*20=49800
const { curGl } = computeGaugeState(50000, NO_RECHARGES, 200, 9999);
expect(curGl).toBe(49800);
});
it('curGl reflète la recharge dans le deuxième segment', () => {
// Seg1: 50000, 100sec → curGl après seg1 = gaugeAfter(50000,100)=49800 mais recharge à 80000
// Seg2: 80000, no cap → curGl = gaugeAfter(80000, 100sec) = 80000-10*30=79700
const recharges: GaugeRecharge[] = [{ atSec: 100, level: 80000 }];
const { curGl } = computeGaugeState(50000, recharges, Infinity, 200);
expect(curGl).toBe(gaugeAfter(80000, 100));
});
});
describe('elapsed', () => {
it('no start time = 0', () => {
const t: TimerState = { startTime: null, running: false, pausedAt: null, pausedMs: 0 };
expect(elapsed(t)).toBe(0);
});
it('running timer', () => {
const now = Date.now();
const t: TimerState = { startTime: now - 10000, running: true, pausedAt: null, pausedMs: 0 };
const el = elapsed(t);
expect(el).toBeGreaterThanOrEqual(9.9);
expect(el).toBeLessThanOrEqual(10.5);
});
it('paused timer', () => {
const now = Date.now();
const t: TimerState = { startTime: now - 10000, running: false, pausedAt: now - 5000, pausedMs: 0 };
const el = elapsed(t);
expect(el).toBeCloseTo(5, 0);
});
it('paused timer with accumulated pause', () => {
const now = Date.now();
const t: TimerState = { startTime: now - 20000, running: false, pausedAt: now - 5000, pausedMs: 5000 };
// elapsed = (pausedAt - startTime - pausedMs) / 1000 = (15000 - 5000) / 1000 = 10
expect(elapsed(t)).toBeCloseTo(10, 0);
});
});
});