export type Ranked = T & { rank: number }; export type RankedProfile = Ranked; // Classement "compétition" générique : les ex-aequo (selon le comparateur) // partagent le même rang, et le rang suivant saute en conséquence // (1, 2, 2, 4 — pas 1, 2, 2, 3). export function computeRanksBy(items: T[], compare: (a: T, b: T) => number): Ranked[] { const sorted = [...items].sort(compare); let lastItem: T | null = null; let lastRank = 0; return sorted.map((item, index) => { if (lastItem === null || compare(item, lastItem) !== 0) { lastRank = index + 1; lastItem = item; } return { ...item, rank: lastRank }; }); } export function computeRanks(profiles: T[]): RankedProfile[] { return computeRanksBy(profiles, (a, b) => b.points - a.points); } const ROMAN_DIGITS: [number, string][] = [ [10, "X"], [9, "IX"], [5, "V"], [4, "IV"], [1, "I"], ]; export function toRoman(value: number): string { let remaining = value; let result = ""; for (const [digit, symbol] of ROMAN_DIGITS) { while (remaining >= digit) { result += symbol; remaining -= digit; } } return result || "0"; } export type Progress = { direction: "up" | "down" | "same" | "none"; amount: number }; export function computeProgress(currentRank: number, previousRank: number | null): Progress { if (previousRank == null) { return { direction: "none", amount: 0 }; } const amount = previousRank - currentRank; if (amount > 0) return { direction: "up", amount }; if (amount < 0) return { direction: "down", amount: -amount }; return { direction: "same", amount: 0 }; }