67 lines
1.9 KiB
TypeScript
67 lines
1.9 KiB
TypeScript
|
|
/**
|
|
* 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;
|
|
};
|