diff --git a/backend/bracket/logic/ranking/elo.py b/backend/bracket/logic/ranking/elo.py index 6acad3849..2e816f3a6 100644 --- a/backend/bracket/logic/ranking/elo.py +++ b/backend/bracket/logic/ranking/elo.py @@ -35,10 +35,6 @@ def set_statistics_for_stage_item_input( match.stage_item_input1_score, match.stage_item_input2_score ) - # Set default for SWISS teams - if stage_item.type is StageType.SWISS and stage_item_input_id not in stats: - stats[stage_item_input_id].points = START_ELO - if has_won: stats[stage_item_input_id].wins += 1 swiss_score_diff = ranking.win_points @@ -73,7 +69,12 @@ def determine_ranking_for_stage_item( stage_item: StageItemWithRounds, ranking: Ranking, ) -> defaultdict[StageItemInputId, TeamStatistics]: - team_x_stats: defaultdict[StageItemInputId, TeamStatistics] = defaultdict(TeamStatistics) + input_x_stats: defaultdict[StageItemInputId, TeamStatistics] = defaultdict(TeamStatistics) + + if stage_item.type is StageType.SWISS: + for input_ in stage_item.inputs: + input_x_stats[input_.id].points = START_ELO + matches = [ match for round_ in stage_item.rounds @@ -85,14 +86,14 @@ def determine_ranking_for_stage_item( for team_index, stage_item_input in enumerate(match.stage_item_inputs): set_statistics_for_stage_item_input( team_index, - team_x_stats, + input_x_stats, match, stage_item_input.id, ranking, stage_item, ) - return team_x_stats + return input_x_stats def determine_team_ranking_for_stage_item( diff --git a/backend/bracket/logic/scheduling/builder.py b/backend/bracket/logic/scheduling/builder.py index 28d10ff74..477f8eefa 100644 --- a/backend/bracket/logic/scheduling/builder.py +++ b/backend/bracket/logic/scheduling/builder.py @@ -1,5 +1,6 @@ from fastapi import HTTPException +from bracket.logic.ranking.elo import recalculate_ranking_for_stage_item_id from bracket.logic.scheduling.elimination import ( build_single_elimination_stage_item, get_number_of_rounds_to_create_single_elimination, @@ -71,6 +72,8 @@ async def build_matches_for_stage_item(stage_item: StageItem, tournament_id: Tou 400, f"Cannot automatically create matches for stage type {stage_item.type}" ) + await recalculate_ranking_for_stage_item_id(tournament_id, stage_item.id) + def determine_available_inputs( teams: list[FullTeamWithPlayers], diff --git a/backend/bracket/routes/rankings.py b/backend/bracket/routes/rankings.py index fa1bb4297..f889a397f 100644 --- a/backend/bracket/routes/rankings.py +++ b/backend/bracket/routes/rankings.py @@ -1,5 +1,6 @@ from fastapi import APIRouter, Depends +from bracket.logic.ranking.elo import recalculate_ranking_for_stage_item_id from bracket.logic.subscriptions import check_requirement from bracket.models.db.ranking import RankingBody, RankingCreateBody from bracket.models.db.user import UserPublic @@ -17,6 +18,7 @@ sql_delete_ranking, sql_update_ranking, ) +from bracket.sql.stage_item_inputs import get_stage_item_input_ids_by_ranking_id from bracket.utils.id_types import RankingId, TournamentId router = APIRouter() @@ -42,6 +44,9 @@ async def update_ranking_by_id( ranking_id=ranking_id, ranking_body=ranking_body, ) + stage_item_ids = await get_stage_item_input_ids_by_ranking_id(ranking_id) + for stage_item_id in stage_item_ids: + await recalculate_ranking_for_stage_item_id(tournament_id, stage_item_id) return SuccessResponse() diff --git a/backend/bracket/routes/stage_items.py b/backend/bracket/routes/stage_items.py index b9e4bea4c..8b02f08f3 100644 --- a/backend/bracket/routes/stage_items.py +++ b/backend/bracket/routes/stage_items.py @@ -8,6 +8,7 @@ get_draft_round, schedule_all_matches_for_swiss_round, ) +from bracket.logic.ranking.elo import recalculate_ranking_for_stage_item_id from bracket.logic.scheduling.builder import ( build_matches_for_stage_item, ) @@ -109,6 +110,7 @@ async def update_stage_item( query=query, values={"stage_item_id": stage_item_id, "name": stage_item_body.name}, ) + await recalculate_ranking_for_stage_item_id(tournament_id, stage_item_id) return SuccessResponse() diff --git a/backend/bracket/sql/stage_item_inputs.py b/backend/bracket/sql/stage_item_inputs.py index 9bcf7b15e..2f20c309a 100644 --- a/backend/bracket/sql/stage_item_inputs.py +++ b/backend/bracket/sql/stage_item_inputs.py @@ -10,7 +10,7 @@ StageItemInputFinal, ) from bracket.sql.teams import get_team_by_id -from bracket.utils.id_types import StageItemId, StageItemInputId, TeamId, TournamentId +from bracket.utils.id_types import RankingId, StageItemId, StageItemInputId, TeamId, TournamentId async def get_stage_item_input_by_id( @@ -37,6 +37,20 @@ async def get_stage_item_input_by_id( return TypeAdapter(StageItemInput).validate_python(result) +async def get_stage_item_input_ids_by_ranking_id(ranking_id: RankingId) -> list[StageItemId]: + query = """ + SELECT id + FROM stage_items + WHERE ranking_id = :ranking_id + """ + results = await database.fetch_all( + query=query, + values={"ranking_id": ranking_id}, + ) + + return [StageItemId(result["id"]) for result in results] + + async def sql_set_team_id_for_stage_item_input( tournament_id: TournamentId, stage_item_input_id: StageItemInputId, team_id: TeamId | None ) -> None: diff --git a/backend/tests/unit_tests/ranking_calculation_test.py b/backend/tests/unit_tests/ranking_calculation_test.py index b107cf9cd..d5a76e007 100644 --- a/backend/tests/unit_tests/ranking_calculation_test.py +++ b/backend/tests/unit_tests/ranking_calculation_test.py @@ -247,7 +247,7 @@ def test_determine_ranking_for_stage_item_swiss_no_matches() -> None: ], inputs=[stage_item_input1, stage_item_input2], type_name="Swiss", - team_count=4, + team_count=2, ranking_id=None, id=StageItemId(-1), stage_id=StageId(-1), @@ -267,4 +267,7 @@ def test_determine_ranking_for_stage_item_swiss_no_matches() -> None: ), ) - assert not ranking + assert ranking == { + -2: TeamStatistics(wins=0, draws=0, losses=0, points=Decimal("1200")), + -1: TeamStatistics(wins=0, draws=0, losses=0, points=Decimal("1200")), + } diff --git a/frontend/src/components/brackets/courts_large.tsx b/frontend/src/components/brackets/courts_large.tsx index b474eb99c..e34f4a057 100644 --- a/frontend/src/components/brackets/courts_large.tsx +++ b/frontend/src/components/brackets/courts_large.tsx @@ -23,21 +23,16 @@ export function CourtBadge({ name, color }: { name: string; color: MantineColor ); } -function getRoundsGridCols(match: MatchInterface | null) { - if (match == null) { - return null; - } - return ; -} - export default function CourtsLarge({ court, activeMatch, nextMatch, + stageItemsLookup, }: { court: Court; activeMatch: MatchInterface | null; nextMatch: MatchInterface | null; + stageItemsLookup: any; }) { return ( @@ -45,10 +40,22 @@ export default function CourtsLarge({ - {getRoundsGridCols(activeMatch)} + + {activeMatch != null && ( + + )} + - {getRoundsGridCols(nextMatch)} + + {nextMatch != null && ( + + )} + ); diff --git a/frontend/src/components/brackets/match_large.tsx b/frontend/src/components/brackets/match_large.tsx index 82fb610f8..54310e4c8 100644 --- a/frontend/src/components/brackets/match_large.tsx +++ b/frontend/src/components/brackets/match_large.tsx @@ -1,21 +1,28 @@ import { Card, Center, Grid, Text } from '@mantine/core'; -import assert from 'assert'; import React from 'react'; import { MatchInterface } from '../../interfaces/match'; +import { formatStageItemInput } from '../../interfaces/stage_item_input'; import { Time } from '../utils/datetime'; -export default function MatchLarge({ match }: { match: MatchInterface }) { - assert(match.stage_item_input1?.team != null); - assert(match.stage_item_input2?.team != null); - +export default function MatchLarge({ + match, + stageItemsLookup, +}: { + match: MatchInterface; + stageItemsLookup: any; +}) { const bracket = (
- {match.stage_item_input1.team.name} - {match.stage_item_input2.team.name} + + {formatStageItemInput(match.stage_item_input1, stageItemsLookup) || N/A} + + + {formatStageItemInput(match.stage_item_input2, stageItemsLookup) || N/A} +
diff --git a/frontend/src/components/info/player_list.tsx b/frontend/src/components/info/player_list.tsx index 23ddab42d..e1e325d1d 100644 --- a/frontend/src/components/info/player_list.tsx +++ b/frontend/src/components/info/player_list.tsx @@ -1,5 +1,7 @@ +import { Text } from '@mantine/core'; + import { TeamInterface } from '../../interfaces/team'; export default function PlayerList({ team }: { team: TeamInterface }) { - return {team.name}; + return {team.name}; } diff --git a/frontend/src/components/info/player_score.tsx b/frontend/src/components/info/player_score.tsx index ad1c9694c..76ffd8628 100644 --- a/frontend/src/components/info/player_score.tsx +++ b/frontend/src/components/info/player_score.tsx @@ -5,16 +5,25 @@ interface ScoreProps { min_score: number; max_score: number; decimals: number; + fontSizeInPixels: number; } -export function PlayerScore({ score, min_score, max_score, decimals }: ScoreProps) { +export function PlayerScore({ + score, + min_score, + max_score, + decimals, + fontSizeInPixels, +}: ScoreProps) { const percentageScale = 100.0 / (max_score - min_score); const empty = max_score - min_score === 0; return ( - + - {Number(score).toFixed(decimals)} + + {Number(score).toFixed(decimals)} + ); diff --git a/frontend/src/components/info/player_statistics.tsx b/frontend/src/components/info/player_statistics.tsx index d98e7ca41..dc97e182f 100644 --- a/frontend/src/components/info/player_statistics.tsx +++ b/frontend/src/components/info/player_statistics.tsx @@ -4,23 +4,30 @@ interface PlayerStatisticsProps { wins: number; draws: number; losses: number; + fontSizeInPixels: number; } -export function WinDistribution({ wins, draws, losses }: PlayerStatisticsProps) { +export function WinDistribution({ wins, draws, losses, fontSizeInPixels }: PlayerStatisticsProps) { const percentageScale = 100.0 / (wins + draws + losses); const empty = wins + draws + losses === 0; return ( <> - + - {`${wins.toFixed(0)}`} + + {`${wins.toFixed(0)}`} + - {`${draws.toFixed(0)}`} + + {`${draws.toFixed(0)}`} + - {`${losses.toFixed(0)}`} + + {`${losses.toFixed(0)}`} + diff --git a/frontend/src/components/no_content/empty_table_info.tsx b/frontend/src/components/no_content/empty_table_info.tsx index 40c7c6a01..1e96724d9 100644 --- a/frontend/src/components/no_content/empty_table_info.tsx +++ b/frontend/src/components/no_content/empty_table_info.tsx @@ -41,7 +41,7 @@ export function NoContent({
{icon || }
{title} - + {description}
diff --git a/frontend/src/components/tables/standings.tsx b/frontend/src/components/tables/standings.tsx index 298f7faee..836eaa42e 100644 --- a/frontend/src/components/tables/standings.tsx +++ b/frontend/src/components/tables/standings.tsx @@ -3,9 +3,7 @@ import { useTranslation } from 'next-i18next'; import React from 'react'; import { StageItemWithRounds } from '../../interfaces/stage_item'; -import { StageItemInputFinal } from '../../interfaces/stage_item_input'; -import { TeamInterface } from '../../interfaces/team'; -import PlayerList from '../info/player_list'; +import { StageItemInputFinal, formatStageItemInput } from '../../interfaces/stage_item_input'; import { PlayerScore } from '../info/player_score'; import { WinDistribution } from '../info/player_statistics'; import { EmptyTableInfo } from '../no_content/empty_table_info'; @@ -13,71 +11,16 @@ import { WinDistributionTitle } from './players'; import { ThNotSortable, ThSortable, getTableState, sortTableEntries } from './table'; import TableLayoutLarge from './table_large'; -export default function StandingsTable({ teams }: { teams: TeamInterface[] }) { - const { t } = useTranslation(); - const tableState = getTableState('elo_score', false); - - const minELOScore = Math.min(...teams.map((team) => team.elo_score)); - const maxELOScore = Math.max(...teams.map((team) => team.elo_score)); - - const rows = teams - .sort((p1: TeamInterface, p2: TeamInterface) => (p1.name < p2.name ? 1 : -1)) - .sort((p1: TeamInterface, p2: TeamInterface) => (p1.draws > p2.draws ? 1 : -1)) - .sort((p1: TeamInterface, p2: TeamInterface) => (p1.wins > p2.wins ? 1 : -1)) - .sort((p1: TeamInterface, p2: TeamInterface) => sortTableEntries(p1, p2, tableState)) - .map((team, index) => ( - - {index + 1} - - {team.name} - - - - - - - - - - - - )); - - if (rows.length < 1) return ; - - return ( - - - - # - - {t('name_table_header')} - - {t('members_table_header')} - - {t('elo_score')} - - - - - - - {rows} - - ); -} - export function StandingsTableForStageItem({ teams_with_inputs, stageItem, + fontSizeInPixels, + stageItemsLookup, }: { teams_with_inputs: StageItemInputFinal[]; stageItem: StageItemWithRounds; + fontSizeInPixels: number; + stageItemsLookup: any; }) { const { t } = useTranslation(); const tableState = getTableState('points', false); @@ -91,15 +34,15 @@ export function StandingsTableForStageItem({ sortTableEntries(p1, p2, tableState) ) .map((team_with_input, index) => ( - + {index + 1} - - {team_with_input.team.name} + + {formatStageItemInput(team_with_input, stageItemsLookup)} - + {team_with_input.points} @@ -110,6 +53,7 @@ export function StandingsTableForStageItem({ min_score={minPoints} max_score={maxPoints} decimals={0} + fontSizeInPixels={fontSizeInPixels} /> ) : ( @@ -118,6 +62,7 @@ export function StandingsTableForStageItem({ wins={team_with_input.wins} draws={team_with_input.draws} losses={team_with_input.losses} + fontSizeInPixels={fontSizeInPixels} /> )} diff --git a/frontend/src/components/tables/table.tsx b/frontend/src/components/tables/table.tsx index fb10f842a..7a1c176e2 100644 --- a/frontend/src/components/tables/table.tsx +++ b/frontend/src/components/tables/table.tsx @@ -76,9 +76,9 @@ export function ThSortable({ children, field, visibleFrom, state }: ThProps) { const onSort = () => setSorting(state, field); return ( - + - + {children}
{getSortIcon(sorted, state.reversed)}
@@ -98,7 +98,7 @@ export function ThNotSortable({ return ( - + {children} diff --git a/frontend/src/components/tables/table_large.tsx b/frontend/src/components/tables/table_large.tsx index 1d08d0726..6945fb6a9 100644 --- a/frontend/src/components/tables/table_large.tsx +++ b/frontend/src/components/tables/table_large.tsx @@ -5,7 +5,13 @@ export default function TableLayoutLarge({ children }: any) { return ( <> - +
{children}
diff --git a/frontend/src/interfaces/stage_item_input.tsx b/frontend/src/interfaces/stage_item_input.tsx index aec27cb31..2b4fd5b23 100644 --- a/frontend/src/interfaces/stage_item_input.tsx +++ b/frontend/src/interfaces/stage_item_input.tsx @@ -7,23 +7,29 @@ export interface StageItemInput { slot: number; tournament_id: number; stage_item_id: number; - team_id: number | null; - winner_from_stage_item_id: number | null; - winner_position: number | null; wins: number; draws: number; losses: number; points: number; + team_id: number | null; team: TeamInterface | null; + winner_from_stage_item_id: number | null; + winner_position: number | null; } export interface StageItemInputFinal { id: number; - team: TeamInterface; + slot: number; + tournament_id: number; + stage_item_id: number; wins: number; draws: number; losses: number; points: number; + team_id: number; + team: TeamInterface; + winner_from_stage_item_id: number | null; + winner_position: number | null; } export interface StageItemInputCreateBody { diff --git a/frontend/src/pages/tournaments/[id]/dashboard/present/courts.tsx b/frontend/src/pages/tournaments/[id]/dashboard/present/courts.tsx index 01185e0e2..35c4c7a7a 100644 --- a/frontend/src/pages/tournaments/[id]/dashboard/present/courts.tsx +++ b/frontend/src/pages/tournaments/[id]/dashboard/present/courts.tsx @@ -22,7 +22,7 @@ import { isMatchInTheFuture, } from '../../../../../interfaces/match'; import { getCourtsLive, getStagesLive } from '../../../../../services/adapter'; -import { getMatchLookupByCourt } from '../../../../../services/lookups'; +import { getMatchLookupByCourt, getStageItemLookup } from '../../../../../services/lookups'; import { getTournamentResponseByEndpointName } from '../../../../../services/tournament'; export default function CourtsPage() { @@ -43,6 +43,7 @@ export default function CourtsPage() { if (notFound) { return ; } + const stageItemsLookup = getStageItemLookup(swrStagesResponse); const tournamentDataFull = tournamentResponse != null ? tournamentResponse[0] : null; const courts = responseIsValid(swrCourtsResponse) ? swrCourtsResponse.data.data : []; @@ -53,23 +54,22 @@ export default function CourtsPage() { const rows = courts.map((court: Court) => { const matchesForCourt = matchesByCourtId[court.id] || []; const activeMatch = matchesForCourt.filter((m: MatchInterface) => isMatchHappening(m))[0]; - const futureMatch = matchesForCourt.filter((m: MatchInterface) => isMatchInTheFuture(m))[0]; + const futureMatch = matchesForCourt + .filter((m: MatchInterface) => isMatchInTheFuture(m)) + .sort((m1: MatchInterface, m2: MatchInterface) => + m1.start_time > m2.start_time ? 1 : -1 + )[0]; return ( - + ); }); - const header = ( - - - - - - - - - - ); return ( <> @@ -83,7 +83,15 @@ export default function CourtsPage() { - {header} + + + + + + + + + {rows} diff --git a/frontend/src/pages/tournaments/[id]/dashboard/present/standings.tsx b/frontend/src/pages/tournaments/[id]/dashboard/present/standings.tsx index 6c67c1ece..49393fd77 100644 --- a/frontend/src/pages/tournaments/[id]/dashboard/present/standings.tsx +++ b/frontend/src/pages/tournaments/[id]/dashboard/present/standings.tsx @@ -11,11 +11,11 @@ import { TournamentQRCode, TournamentTitle, } from '../../../../../components/dashboard/layout'; -import StandingsTable from '../../../../../components/tables/standings'; import RequestErrorAlert from '../../../../../components/utils/error_alert'; import { TableSkeletonTwoColumns } from '../../../../../components/utils/skeletons'; -import { getTeamsLive } from '../../../../../services/adapter'; +import { getStagesLive, getTeamsLive } from '../../../../../services/adapter'; import { getTournamentResponseByEndpointName } from '../../../../../services/tournament'; +import { StandingsContent } from '../standings'; export default function Standings() { const tournamentResponse = getTournamentResponseByEndpointName(); @@ -25,6 +25,7 @@ export default function Standings() { const tournamentId = !notFound ? tournamentResponse[0].id : null; const swrTeamsResponse: SWRResponse = getTeamsLive(tournamentId); + const swrStagesResponse = getStagesLive(tournamentId); if (swrTeamsResponse.isLoading) { return ; @@ -38,6 +39,7 @@ export default function Standings() { if (swrTeamsResponse.error) return ; + const fontSizeInPixels = 28; return ( <> @@ -49,8 +51,11 @@ export default function Standings() { - - + + diff --git a/frontend/src/pages/tournaments/[id]/dashboard/standings.tsx b/frontend/src/pages/tournaments/[id]/dashboard/standings.tsx index 45ac42e05..9e2a25985 100644 --- a/frontend/src/pages/tournaments/[id]/dashboard/standings.tsx +++ b/frontend/src/pages/tournaments/[id]/dashboard/standings.tsx @@ -17,7 +17,13 @@ import { getStagesLive } from '../../../../services/adapter'; import { getStageItemLookup, getStageItemTeamsLookup } from '../../../../services/lookups'; import { getTournamentResponseByEndpointName } from '../../../../services/tournament'; -function StandingsContent({ swrStagesResponse }: { swrStagesResponse: SWRResponse }) { +export function StandingsContent({ + swrStagesResponse, + fontSizeInPixels, +}: { + swrStagesResponse: SWRResponse; + fontSizeInPixels: number; +}) { const { t } = useTranslation(); const stageItemsLookup = getStageItemLookup(swrStagesResponse); @@ -32,12 +38,14 @@ function StandingsContent({ swrStagesResponse }: { swrStagesResponse: SWRRespons ) .map((stageItemId) => (
- + {stageItemsLookup[stageItemId].name}
)); @@ -84,7 +92,7 @@ export default function Standings() { - + diff --git a/frontend/src/services/lookups.tsx b/frontend/src/services/lookups.tsx index f0d94f3a1..32bacfbc2 100644 --- a/frontend/src/services/lookups.tsx +++ b/frontend/src/services/lookups.tsx @@ -20,6 +20,7 @@ export function getTeamsLookup(tournamentId: number) { export function getStageItemLookup(swrStagesResponse: SWRResponse) { let result: any[] = []; + if (swrStagesResponse?.data == null) return Object.fromEntries(result); swrStagesResponse.data.data.map((stage: StageWithStageItems) => stage.stage_items.forEach((stage_item) => {