🏆 Cálculo de Leaderboard
Visión General
El leaderboard de PuntoHack utiliza un sistema de scoring ponderado que combina:
- Evaluación técnica (12 criterios estándar)
- Evaluación de challenges (opcional, 20% del score)
Fórmula de Cálculo
Score Técnico (80% del total)
// Por cada submission
const technicalScore = criteria.reduce((total, criterion) => {
// Obtener scores de todos los jueces vigentes para este criterio
const scoresForCriterion = scores.filter(
s => s.criterionId === criterion.id &&
activeJudgeIds.includes(s.judgeId)
);
// Promediar scores de jueces
const averageScore = scoresForCriterion.length > 0
? scoresForCriterion.reduce((sum, s) => sum + s.value, 0) / scoresForCriterion.length
: 0;
// Aplicar peso del criterio
return total + (averageScore * criterion.weight);
}, 0);
// Normalizar a 100 puntos máximo
const maxPossibleScore = criteria.reduce((sum, c) => sum + (c.maxScore * c.weight), 0);
const normalizedTechnicalScore = maxPossibleScore > 0
? (technicalScore / maxPossibleScore) * 100
: 0;
Score de Challenges (20% del total)
// Solo challenges aprobados por sponsors
const approvedEvaluations = submission.challengeEvaluations.filter(
e => e.sponsorApprovalStatus === 'APPROVED'
);
if (approvedEvaluations.length > 0) {
// Calcular score promedio ponderado por challenge
const challengeScores = approvedEvaluations.map(evaluation => {
return (
evaluation.fulfillmentScore * 0.4 + // 40% cumplimiento
evaluation.technicalScore * 0.3 + // 30% técnico
evaluation.adoptionScore * 0.2 + // 20% adopción
evaluation.documentationScore * 0.1 // 10% documentación
);
});
const averageChallengeScore = challengeScores.reduce((sum, val) => sum + val, 0) / challengeScores.length;
// Agregar 20% al score total
challengeScore = averageChallengeScore * 0.2;
}
Score Final
const finalScore = Math.min(
normalizedTechnicalScore + challengeScore,
100 // Máximo 100 puntos
);
Diagrama de Cálculo
Ejemplo Numérico Completo
Configuración del Hackathon
Criterios:
- Stack Tecnológico (weight: 2, maxScore: 10)
- Arquitectura (weight: 3, maxScore: 10)
- Características (weight: 2, maxScore: 10)
- Desafíos Técnicos (weight: 2, maxScore: 10)
- Mejoras Futuras (weight: 1, maxScore: 10)
- Documentación Técnica (weight: 2, maxScore: 10)
- Documentación API (weight: 1, maxScore: 10)
- Guía Despliegue (weight: 1, maxScore: 10)
- Cobertura Tests (weight: 2, maxScore: 10)
- Métricas Rendimiento (weight: 1, maxScore: 10)
- Repositorio (weight: 2, maxScore: 10)
- Demo Funcional (weight: 1, maxScore: 10)
Total weight: 20
Max possible score raw: 20 × 10 = 200
Scores de Jueces
Submission A - 3 jueces evaluaron:
| Criterio | Juez 1 | Juez 2 | Juez 3 | Promedio | Weight | Score Ponderado |
|---|---|---|---|---|---|---|
| Stack | 9 | 8 | 9 | 8.67 | 2 | 17.34 |
| Arquitectura | 8 | 9 | 8 | 8.33 | 3 | 24.99 |
| Características | 7 | 8 | 6 | 7.00 | 2 | 14.00 |
| Desafíos | 8 | 7 | 8 | 7.67 | 2 | 15.34 |
| Mejoras | 6 | 7 | 6 | 6.33 | 1 | 6.33 |
| Doc Técnica | 9 | 8 | 9 | 8.67 | 2 | 17.34 |
| Doc API | 8 | 7 | 8 | 7.67 | 1 | 7.67 |
| Guía Despliegue | 7 | 8 | 7 | 7.33 | 1 | 7.33 |
| Tests | 8 | 9 | 8 | 8.33 | 2 | 16.66 |
| Rendimiento | 7 | 8 | 7 | 7.33 | 1 | 7.33 |
| Repositorio | 9 | 8 | 9 | 8.67 | 2 | 17.34 |
| Demo | 8 | 9 | 8 | 8.33 | 1 | 8.33 |
Total técnico raw: 154.32
Normalizado: (154.32 / 200) × 100 = 77.16 puntos
Challenge Evaluation (Opcional)
Si la submission tiene challenge aprobado:
Evaluación del Challenge:
- Fulfillment: 85/100
- Technical: 80/100
- Adoption: 75/100
- Documentation: 90/100
Cálculo:
Challenge Score = (85 × 0.4) + (80 × 0.3) + (75 × 0.2) + (90 × 0.1)
= 34 + 24 + 15 + 9
= 82 puntos
Agregar al total: 82 × 0.2 = 16.4 puntos adicionales
Score Final
Score Final = 77.16 (técnico) + 16.4 (challenges)
= 93.56 puntos
Implementación Completa
export async function calculateLeaderboard(hackathonId: string): Promise<LeaderboardEntry[]> {
// 1. Obtener todas las submissions
const submissions = await db.submission.findMany({
where: { hackathonId },
include: {
team: {
include: {
members: {
include: { profile: true },
},
},
},
scores: {
include: {
judge: { select: { id: true } },
criterion: {
select: {
id: true,
weight: true,
maxScore: true,
},
},
},
},
challengeEvaluations: {
where: {
sponsorApprovalStatus: 'APPROVED',
},
select: {
fulfillmentScore: true,
technicalScore: true,
adoptionScore: true,
documentationScore: true,
},
},
},
});
// 2. Obtener jueces vigentes
const activeJudges = await db.hackathonJudge.findMany({
where: { hackathonId },
select: { profileId: true },
});
const activeJudgeIds = new Set(activeJudges.map(j => j.profileId));
// 3. Obtener criterios estándar (12)
const allCriteria = await db.criterion.findMany({
where: { hackathonId },
});
const requiredCriteriaNames = [
'Stack Tecnológico',
'Arquitectura del Sistema',
'Características Principales',
'Resolución de Desafíos Técnicos',
'Visión y Mejoras Futuras',
'Documentación Técnica',
'Documentación de API',
'Guía de Despliegue',
'Cobertura de Tests',
'Métricas de Rendimiento',
'Repositorio y Código',
'Demo Funcional',
];
const criteria = allCriteria.filter(c => requiredCriteriaNames.includes(c.name));
// 4. Calcular maxPossibleScoreRaw
const maxPossibleScoreRaw = criteria.reduce(
(sum, c) => sum + (c.maxScore * c.weight),
0
);
// 5. Calcular score para cada submission
const leaderboard: LeaderboardEntry[] = submissions.map(submission => {
// Filtrar scores solo de jueces vigentes
const validScores = submission.scores.filter(
score => activeJudgeIds.has(score.judge.id)
);
// Agrupar scores por criterio
const scoresByCriterion = new Map<string, number[]>();
validScores.forEach(score => {
const criterionId = score.criterionId;
if (!scoresByCriterion.has(criterionId)) {
scoresByCriterion.set(criterionId, []);
}
scoresByCriterion.get(criterionId)!.push(score.value);
});
// Calcular score técnico
let technicalScore = 0;
criteria.forEach(criterion => {
const scoresForCriterion = scoresByCriterion.get(criterion.id) || [];
if (scoresForCriterion.length > 0) {
const averageScore = scoresForCriterion.reduce((sum, val) => sum + val, 0) / scoresForCriterion.length;
technicalScore += averageScore * criterion.weight;
}
});
// Normalizar score técnico
const normalizedTechnicalScore = maxPossibleScoreRaw > 0
? (technicalScore / maxPossibleScoreRaw) * 100
: 0;
// Calcular score de challenges
let challengeScore = 0;
if (submission.challengeEvaluations.length > 0) {
const challengeScores = submission.challengeEvaluations.map(evaluation => {
return (
evaluation.fulfillmentScore * 0.4 +
evaluation.technicalScore * 0.3 +
evaluation.adoptionScore * 0.2 +
evaluation.documentationScore * 0.1
);
});
const averageChallengeScore = challengeScores.reduce((sum, val) => sum + val, 0) / challengeScores.length;
challengeScore = averageChallengeScore * 0.2;
}
// Score final
const finalScore = Math.min(normalizedTechnicalScore + challengeScore, 100);
return {
position: 0, // Se asignará después
team: {
id: submission.team.id,
name: submission.team.name,
code: submission.team.code,
members: submission.team.members.map(m => ({ profile: m.profile })),
},
submission: {
id: submission.id,
title: submission.title,
description: submission.description,
repoUrl: submission.repoUrl,
demoUrl: submission.demoUrl,
},
weightedScore: Math.round(finalScore * 100) / 100,
totalScore: Math.round(normalizedTechnicalScore * 100) / 100,
maxPossibleScore: 100,
challengeScore: Math.round(challengeScore * 100) / 100,
};
});
// 6. Ordenar por score descendente
leaderboard.sort((a, b) => b.weightedScore - a.weightedScore);
// 7. Asignar posiciones
leaderboard.forEach((entry, index) => {
entry.position = index + 1;
});
return leaderboard;
}
Consideraciones Importantes
Solo Jueces Vigentes
Regla: Solo se consideran scores de jueces con HackathonJudge vigente.
Razón: Si un juez es removido, sus scores permanecen en la DB pero no cuentan para el cálculo.
// Obtener jueces vigentes
const activeJudges = await db.hackathonJudge.findMany({
where: { hackathonId },
});
// Filtrar scores
const validScores = submission.scores.filter(
score => activeJudgeIds.has(score.judge.id)
);
Criterios Estándar
Regla: Solo se usan los 12 criterios estándar para el cálculo.
Razón: Asegurar consistencia en la evaluación técnica.
Empates
Regla: Se permiten empates. Múltiples equipos pueden tener el mismo score.
Implementación: No hay criterio de desempate en el MVP.
Challenges Opcionales
Regla: Los challenges son opcionales. Si una submission no tiene challenges aprobados, su score es solo técnico.
Implementación: challengeScore es 0 si no hay evaluaciones aprobadas.
Próximos Pasos
- Evaluation System - Sistema de evaluación completo
- Challenge System - Sistema de challenges
- Team Formation - Formación de equipos
Siguiente: Evaluation System