/** * Dynamic Scoring Algorithm * Logic: * 1. Base Score starts at initialPoints. * 2. As solves increase, the score for EVERYONE who solved it decreases. * 3. We use a decay function: currentPoints = max(min_points, initial * decay_factor ^ (solves - 1)) * 4. Plus a "First Blood" bonus: 1st solver gets 10% extra, 2nd 5%, 3rd 2%. */ const MIN_POINTS_PERCENTAGE = 0.2; // Points won't drop below 20% of initial const DECAY_CONSTANT = 0.92; // Aggressive decay per solve export const calculateChallengeValue = (initialPoints: number, solveCount: number): number => { if (solveCount === 0) return initialPoints; const minPoints = Math.floor(initialPoints * MIN_POINTS_PERCENTAGE); const decayedPoints = Math.floor(initialPoints * Math.pow(DECAY_CONSTANT, solveCount - 1)); return Math.max(minPoints, decayedPoints); }; export const getFirstBloodBonus = (rank: number): number => { if (rank === 0) return 0.10; // 1st if (rank === 1) return 0.05; // 2nd if (rank === 2) return 0.02; // 3rd return 0; }; export const calculateTeamTotalScore = ( teamId: string, challenges: { id: string, initialPoints: number, solves: string[] }[], 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 value of challenge (shared by all) const baseValue = calculateChallengeValue(challenge.initialPoints, challenge.solves.length); // Calculate rank for bonus // Find all solves for this challenge sorted by time 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(baseValue * getFirstBloodBonus(rank)); total += (baseValue + bonus); }); return total; };