/** * Dynamic Scoring Algorithm (Parabolic Decay) * Logic: * 1. Base Value = ((minimum - initial) / (decay^2)) * (solve_count^2) + initial * 2. If solve_count >= decay, return minimum. * 3. Fixed Bonuses (based on initial points): 1st: 15%, 2nd: 10%, 3rd: 5%. */ export const calculateChallengeValue = ( initial: number, minimum: number, decay: number, solveCount: number ): number => { if (solveCount === 0) return initial; if (decay <= 0 || solveCount >= decay) return minimum; // Parabolic formula: ((min - init) / decay^2) * solveCount^2 + init const value = ((minimum - initial) / (decay * decay)) * (solveCount * solveCount) + initial; return Math.ceil(value); }; export const getFirstBloodBonusFactor = (rank: number): number => { if (rank === 0) return 0.15; // 1st if (rank === 1) return 0.10; // 2nd if (rank === 2) return 0.05; // 3rd return 0; }; export const calculateTeamTotalScore = ( teamId: string, challenges: any[], solves: { teamId: string, challengeId: string, timestamp: number }[] ): number => { let total = 0; // Filter solves for this team const teamSolves = solves.filter(s => s.teamId === teamId); teamSolves.forEach(solve => { const challenge = challenges.find(c => c.id === solve.challengeId); if (!challenge) return; // Current dynamic base value (retroactive for everyone) const baseValue = calculateChallengeValue( challenge.initialPoints, challenge.minimumPoints || 0, challenge.decaySolves || 1, challenge.solves.length ); // Calculate rank for fixed bonus based on initial value const challengeSolves = solves .filter(s => s.challengeId === solve.challengeId) .sort((a, b) => a.timestamp - b.timestamp); const rank = challengeSolves.findIndex(s => s.teamId === teamId); const bonus = Math.floor(challenge.initialPoints * getFirstBloodBonusFactor(rank)); total += (baseValue + bonus); }); return total; };