import React, { useMemo } from 'react'; import { Link } from 'react-router-dom'; import { Trophy, Table, CheckCircle2, Medal, SearchX } from 'lucide-react'; import { useCTF } from './CTFContext'; import { calculateTeamTotalScore, calculateChallengeValue, getFirstBloodBonusFactor } from './services/scoring'; import { Button } from './UIComponents'; export const Scoreboard: React.FC = () => { const { state } = useCTF(); const rankings = useMemo(() => state.teams.filter(t => !t.isAdmin && !t.isDisabled).map(team => ({ ...team, score: calculateTeamTotalScore(team.id, state.challenges, state.solves), solveCount: state.solves.filter(s => s.teamId === team.id).length })).sort((a, b) => b.score - a.score), [state]); return (

SCOREBOARD

View_Matrix
{rankings.map((team, idx) => ( ))}
RANKTEAM_IDENTIFIERSOLVESTOTAL_POINTS
{idx + 1}
{team.name} {team.solveCount} {team.score}
); }; export const ScoreMatrix: React.FC = () => { const { state } = useCTF(); const sortedTeams = useMemo(() => { return state.teams .filter(t => !t.isAdmin && !t.isDisabled) .map(t => ({ ...t, score: calculateTeamTotalScore(t.id, state.challenges, state.solves) })) .sort((a, b) => b.score - a.score); }, [state]); const sortedChallenges = useMemo(() => { return [...state.challenges].sort((a, b) => (b.solves?.length || 0) - (a.solves?.length || 0)); }, [state.challenges]); if (state.solves.length === 0) { return (

SCORE_MATRIX

View_Rankings

No solves yet!

The matrix is currently empty. Be the first to solve a challenge!

); } return (

SCORE_MATRIX

View_Rankings
{sortedChallenges.map(c => { const diffColor = c.difficulty === 'Low' ? 'text-[#00ff00]' : c.difficulty === 'Medium' ? 'text-[#ffaa00]' : 'text-[#ff0000]'; return ( ); })} {sortedTeams.map(team => ( {sortedChallenges.map(c => { const solve = state.solves.find(s => s.challengeId === c.id && s.teamId === team.id); if (!solve) { return ( ); } // Find rank for the medal and bonus const challengeSolves = state.solves .filter(s => s.challengeId === c.id) .sort((a, b) => a.timestamp - b.timestamp); const rank = challengeSolves.findIndex(s => s.teamId === team.id); // Calculate point gain for this specific solve const baseValue = calculateChallengeValue( c.initialPoints, c.minimumPoints || 0, c.decaySolves || 1, (c.solves || []).length ); const bonus = Math.floor(c.initialPoints * getFirstBloodBonusFactor(rank)); const totalPoints = baseValue + bonus; return ( ); })} ))}
Team / Challenge {/* Vertical text container using writing-mode so it stretches cell height automatically */}
{c.title}
{/* Solve count tag at the bottom of the header cell */}
{(c.solves || []).length} SOLVES
{team.name} ({team.score})
{rank === 0 ? ( ) : rank === 1 ? ( ) : rank === 2 ? ( ) : ( )}
{totalPoints}
); };