Skip to content

Commit

Permalink
Add live scoreboard
Browse files Browse the repository at this point in the history
  • Loading branch information
pomo-mondreganto committed Jan 3, 2025
1 parent 17589f1 commit cfca7f0
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 10 deletions.
4 changes: 3 additions & 1 deletion front/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
"@mui/icons-material": "^6.3.0",
"@mui/lab": "6.0.0-beta.21",
"@mui/material": "^6.3.0",
"@types/react-window": "^1.8.8",
"@uidotdev/usehooks": "^2.4.1",
"axios": "^1.7.9",
"centrifuge": "^5.2.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router": "^7.1.1"
"react-router": "^7.1.1",
"react-window": "^1.8.11"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
Expand Down
32 changes: 32 additions & 0 deletions front/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

136 changes: 136 additions & 0 deletions front/src/components/ForcAD/Live.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { centrifugeWSURL } from '@/config';
import { useServices, useTeams } from '@/lib/hooks/scoreboard';
import { AttackNotification_Batch } from '@/proto/receiver/receiver';
import { Box } from '@mui/material';
import { styled } from '@mui/material/styles';
import { useWindowSize } from '@uidotdev/usehooks';
import { Centrifuge } from 'centrifuge';
import { useEffect, useMemo, useState } from 'react';
import { FixedSizeList } from 'react-window';

interface Event {
victimName: string;
attackerName: string;
serviceName: string;
attackerDelta: number;
victimDelta: number;
}

const EventText = styled('div')({
fontFamily: 'Roboto Mono',
fontSize: '0.9rem',
color: '#00ff00',
});

const Highlight = styled('span')({
fontWeight: 'bold',
color: '#ffff00',
});

export default function Live() {
const [events, setEvents] = useState<Event[]>([]);
const teams = useTeams();
const services = useServices();

const { height: windowHeight } = useWindowSize();

const teamMap = useMemo(() => {
return new Map(teams?.map((team) => [team.id, team]));
}, [teams]);

const serviceMap = useMemo(() => {
return new Map(services?.map((service) => [service.id, service]));
}, [services]);

useEffect(() => {
if (!teamMap || !serviceMap) {
return;
}

setEvents(
Array.from({ length: 1000 }, () => ({
victimName:
teamMap.get(
Array.from(teamMap.keys())[
Math.floor(Math.random() * teamMap.size)
],
)?.name ?? 'Unknown',
attackerName:
teamMap.get(
Array.from(teamMap.keys())[
Math.floor(Math.random() * teamMap.size)
],
)?.name ?? 'Unknown',
serviceName:
serviceMap.get(
Array.from(serviceMap.keys())[
Math.floor(Math.random() * serviceMap.size)
],
)?.name ?? 'Unknown',
attackerDelta: Math.floor(Math.random() * 100),
victimDelta: Math.floor(Math.random() * 100),
})),
);

const centrifuge = new Centrifuge(centrifugeWSURL);

const sub = centrifuge.newSubscription('attacks');

sub.on('publication', (ctx) => {
const attackBatch = AttackNotification_Batch.fromJSON(ctx.data);

const newEvents = attackBatch.attacks.map((attack) => ({
victimName: teamMap.get(attack.victimId)?.name ?? 'Unknown',
attackerName: teamMap.get(attack.attackerId)?.name ?? 'Unknown',
serviceName: serviceMap.get(attack.serviceId)?.name ?? 'Unknown',
attackerDelta: attack.attackerDelta,
victimDelta: attack.victimDelta,
}));

setEvents((prev) => [...newEvents, ...prev].slice(0, 50));
});

sub.subscribe();
centrifuge.connect();

return () => {
sub.unsubscribe();
centrifuge.disconnect();
};
}, [serviceMap, teamMap]);

const Row = ({
index,
style,
}: {
index: number;
style: React.CSSProperties;
}) => {
const event = events[index];
return (
<EventText style={style}>
<Highlight>{event.attackerName}</Highlight> attacked{' '}
<Highlight>{event.victimName}</Highlight>'s service{' '}
<Highlight>{event.serviceName}</Highlight> and got{' '}
<Highlight>{event.attackerDelta.toFixed(2)}</Highlight> points
</EventText>
);
};

if (!windowHeight) {
return <div>Loading...</div>;
}

return (
<Box sx={{ height: '100vh', width: '100vw', backgroundColor: '#000' }}>
<FixedSizeList
height={windowHeight}
width="100%"
itemCount={events.length}
itemSize={20}
>
{Row}
</FixedSizeList>
</Box>
);
}
16 changes: 13 additions & 3 deletions front/src/components/ForcAD/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,35 @@ export default function TopBar() {
<Typography
variant="h6"
component="div"
sx={{ flexGrow: 0, fontFamily: 'Roboto Mono', mr: 4 }}
sx={{ flexGrow: 0, fontFamily: 'Roboto Mono' }}
>
FastAD
</Typography>

<Box
component={Link}
to="/live"
sx={{ textDecoration: 'none', color: 'inherit', mr: 2 }}
sx={{ textDecoration: 'none', color: 'inherit', ml: 4 }}
>
<Typography variant="body1" sx={{ fontFamily: 'Roboto Mono' }}>
Live
</Typography>
</Box>

<Box
component={Link}
to="/attack_data"
sx={{ textDecoration: 'none', color: 'inherit', ml: 3 }}
>
<Typography variant="body1" sx={{ fontFamily: 'Roboto Mono' }}>
Attack Data
</Typography>
</Box>

<Box
component={Link}
to="https://github.com/C4T-BuT-S4D/FastAD"
sx={{ textDecoration: 'none', color: 'inherit' }}
sx={{ textDecoration: 'none', color: 'inherit', ml: 3 }}
>
<Typography variant="body1" sx={{ fontFamily: 'Roboto Mono' }}>
GitHub
Expand Down
9 changes: 9 additions & 0 deletions front/src/layouts/Live.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Live from '@/components/ForcAD/Live';

export default function LiveLayout() {
return (
<>
<Live />
</>
);
}
29 changes: 23 additions & 6 deletions front/src/lib/hooks/scoreboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,46 @@ import { useEffect, useMemo, useState } from 'react';
import { createProtoJSONTransform } from '../clients/common';
import { ScoreboardState } from '../models/scoreboardState';

export function useScoreboard() {
export function useTeams() {
const [teams, setTeams] = useState<Team[] | null>(null);
const [services, setServices] = useState<Service[] | null>(null);
const [scoreboard, setScoreboard] = useState<ScoreboardState | null>(null);

useEffect(() => {
async function fetchTeams() {
const { data } = await axios.get<Team_Batch>('/teams', {
transformResponse: createProtoJSONTransform(Team_Batch),
});
data.teams[0].name = 'very very very long team name (yes it is)';
setTeams(data.teams);
}

fetchTeams();
}, []);

return teams;
}

export function useServices() {
const [services, setServices] = useState<Service[] | null>(null);

useEffect(() => {
async function fetchServices() {
const res = await axios.get<Service_Batch>('/services', {
transformResponse: createProtoJSONTransform(Service_Batch),
});
setServices(res.data.services);
}

fetchServices();
}, []);

return services;
}

export function useScoreboard() {
const teams = useTeams();
const services = useServices();
const [scoreboard, setScoreboard] = useState<ScoreboardState | null>(null);

useEffect(() => {
async function fetchScoreboard() {
const { data: scoreboardData } = await axios.get<Scoreboard>(
'/scoreboard',
Expand All @@ -41,8 +60,6 @@ export function useScoreboard() {
setScoreboard(scoreboardState);
}

fetchTeams();
fetchServices();
fetchScoreboard();
}, []);

Expand Down
2 changes: 2 additions & 0 deletions front/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ForcADScoreboardLayout from '@/layouts/ForcADScoreboard.tsx';
import LiveLayout from '@/layouts/Live.tsx';
import TeamHistoryLayout from '@/layouts/TeamHistory.tsx';
import { setupInterceptorsTo } from '@/lib/clients/common.ts';
import theme from '@/theme.ts';
Expand All @@ -24,6 +25,7 @@ createRoot(document.getElementById('root')!).render(
path="/teams/:teamID/history"
element={<TeamHistoryLayout />}
/>
<Route path="/live" element={<LiveLayout />} />
</Routes>
</BrowserRouter>
</ThemeProvider>
Expand Down

0 comments on commit cfca7f0

Please sign in to comment.