import React, { useMemo, useState } from 'react'; import { Trophy, Table, CheckCircle2, Medal, SearchX, History, LineChart as LineChartIcon, List } from 'lucide-react'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; import { useCTF } from './CTFContext'; import { calculateTeamTotalScore, calculateChallengeValue, getFirstBloodBonusFactor } from './services/scoring'; import { Button } from './UIComponents'; const COLORS = [ '#ff0000', // Red '#00ff00', // Green '#0000ff', // Blue '#ffff00', // Yellow '#ff00ff', // Magenta '#00ffff', // Cyan '#ffaa00', // Orange '#bf00ff', // Purple '#ff0080', // Pink '#aaff00' // Lime ]; type ScoreboardView = 'ranking' | 'matrix' | 'log' | 'graph'; export const Scoreboard: React.FC = () => { const { state } = useCTF(); const [view, setView] = useState('ranking'); const rankings = useMemo(() => { return 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 || a.name.localeCompare(b.name)); }, [state]); const top10Teams = useMemo(() => rankings.slice(0, 10), [rankings]); const sortedSolves = useMemo(() => { return [...state.solves].sort((a, b) => b.timestamp - a.timestamp); }, [state.solves]); const sortedChallenges = useMemo(() => { return [...state.challenges].sort((a, b) => (b.solves?.length || 0) - (a.solves?.length || 0)); }, [state.challenges]); const graphData = useMemo(() => { if (state.solves.length === 0) return []; const sortedSolvesAsc = [...state.solves].sort((a, b) => a.timestamp - b.timestamp); const dataPoints: any[] = []; const startTime = state.startTime || (sortedSolvesAsc.length > 0 ? sortedSolvesAsc[0].timestamp - 1000 : 0); const initialPoint: any = { time: startTime, displayTime: new Date(startTime).toLocaleTimeString() }; top10Teams.forEach(t => initialPoint[t.name] = 0); dataPoints.push(initialPoint); sortedSolvesAsc.forEach((solve, idx) => { const solvesUpToNow = sortedSolvesAsc.slice(0, idx + 1); const point: any = { time: solve.timestamp, displayTime: new Date(solve.timestamp).toLocaleTimeString() }; top10Teams.forEach(team => { let total = 0; const teamSolves = solvesUpToNow.filter(s => s.teamId === team.id); teamSolves.forEach(ts => { const challenge = state.challenges.find(c => c.id === ts.challengeId); if (!challenge) return; const challengeSolvesUpToNow = solvesUpToNow.filter(s => s.challengeId === ts.challengeId); const baseValue = calculateChallengeValue( challenge.initialPoints, challenge.minimumPoints || 0, challenge.decaySolves || 1, challengeSolvesUpToNow.length ); const rank = challengeSolvesUpToNow.findIndex(s => s.teamId === team.id); const bonus = Math.floor(challenge.initialPoints * getFirstBloodBonusFactor(rank)); total += (baseValue + bonus); }); point[team.name] = total; }); dataPoints.push(point); }); return dataPoints; }, [state, top10Teams]); const renderContent = () => { switch (view) { case 'ranking': return (
{rankings.map((team, idx) => ( ))}
RANK PLAYER SOLVES POINTS
{idx + 1}
{team.name} {team.solveCount} {team.score}
); case 'matrix': if (state.solves.length === 0) { return (

No solves yet!

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

); } return (
{sortedChallenges.map(c => { const diffColor = c.difficulty === 'Low' ? 'text-[#00ff00]' : c.difficulty === 'Medium' ? 'text-[#ffaa00]' : 'text-[#ff0000]'; return ( ); })} {rankings.map(team => ( {sortedChallenges.map(c => { const solve = state.solves.find(s => s.challengeId === c.id && s.teamId === team.id); if (!solve) { return ( ); } 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); 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 ( ); })} ))}
Player / Challenge
{c.title}
{(c.solves || []).length} SOLVES
{team.name} ({team.score})
{rank === 0 ? : rank === 1 ? : rank === 2 ? : }
{totalPoints}
); case 'log': return (
{sortedSolves.length === 0 ? (
No activities recorded yet.
) : ( sortedSolves.map((solve, idx) => { const team = state.teams.find(t => t.id === solve.teamId); const challenge = state.challenges.find(c => c.id === solve.challengeId); if (!team || !challenge) return null; const challengeSolves = state.solves.filter(s => s.challengeId === solve.challengeId).sort((a, b) => a.timestamp - b.timestamp); const rank = challengeSolves.findIndex(s => s.teamId === solve.teamId); const basePoints = calculateChallengeValue(challenge.initialPoints, challenge.minimumPoints || 0, challenge.decaySolves || 1, challengeSolves.length); const bonus = Math.floor(challenge.initialPoints * getFirstBloodBonusFactor(rank)); const pointsGained = basePoints + bonus; const difficultyColor = challenge.difficulty === 'Low' ? 'text-[#00ff00]' : challenge.difficulty === 'Medium' ? 'text-[#ffaa00]' : 'text-[#ff0000]'; return (
{rank === 0 ? : rank === 1 ? : rank === 2 ? : }
[{new Date(solve.timestamp).toLocaleString()}] {team.name} solved {challenge.category} - {challenge.title} and gained {pointsGained} points.
); }) )}
); case 'graph': return (

Top 10 Players - Score Progression

{graphData.length === 0 ? (
No data to display.
) : ( -item.value} /> {top10Teams.map((team, idx) => ( ))} )}
); } }; return (