Skip to content

Commit

Permalink
Show updates to stage item inputs when going to next stage (#966)
Browse files Browse the repository at this point in the history
  • Loading branch information
evroon authored Nov 6, 2024
1 parent 4fc2731 commit 932e5a2
Show file tree
Hide file tree
Showing 48 changed files with 601 additions and 579 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ on:
- 'master'

jobs:
build:
build_docker:
runs-on: ubuntu-22.04

steps:
Expand Down
34 changes: 34 additions & 0 deletions backend/alembic/versions/e6e2718365dc_drop_rounds_is_active.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""drop rounds.is_active
Revision ID: e6e2718365dc
Revises: 55b14b08be04
Create Date: 2024-11-06 12:02:45.234018
"""

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str | None = "e6e2718365dc"
down_revision: str | None = "55b14b08be04"
branch_labels: str | None = None
depends_on: str | None = None


def upgrade() -> None:
op.drop_column("rounds", "is_active")


def downgrade() -> None:
op.add_column(
"rounds",
sa.Column(
"is_active",
sa.BOOLEAN(),
server_default=sa.text("false"),
autoincrement=False,
nullable=False,
),
)
15 changes: 4 additions & 11 deletions backend/bracket/logic/planning/rounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,13 @@ class MatchTimingAdjustmentInfeasible(Exception):
pass


def get_active_and_next_rounds(
def get_draft_round(
stage_item: StageItemWithRounds,
) -> tuple[RoundWithMatches | None, RoundWithMatches | None]:
active_round = next((round_ for round_ in stage_item.rounds if round_.is_active), None)

def is_round_in_future(round_: RoundWithMatches) -> bool:
return (round_.id > active_round.id) if active_round is not None else True

rounds_chronologically_sorted = sorted(stage_item.rounds, key=lambda r: r.id)
next_round = next(
(round_ for round_ in rounds_chronologically_sorted if is_round_in_future(round_)),
) -> RoundWithMatches | None:
return next(
(round_ for round_ in sorted(stage_item.rounds, key=lambda r: r.id) if round_.is_draft),
None,
)
return active_round, next_round


async def schedule_all_matches_for_swiss_round(
Expand Down
26 changes: 11 additions & 15 deletions backend/bracket/logic/ranking/elo.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ def set_statistics_for_stage_item_input(
stats: defaultdict[StageItemInputId, TeamStatistics],
match: MatchWithDetailsDefinitive,
stage_item_input_id: StageItemInputId,
rating_team1_before: Decimal,
rating_team2_before: Decimal,
ranking: Ranking,
stage_item: StageItemWithRounds,
) -> None:
Expand Down Expand Up @@ -61,7 +59,9 @@ def set_statistics_for_stage_item_input(
stats[stage_item_input_id].points += swiss_score_diff

case StageType.SWISS:
rating_diff = (rating_team2_before - rating_team1_before) * (1 if is_team1 else -1)
rating_diff = (match.stage_item_input2.elo - match.stage_item_input1.elo) * (
1 if is_team1 else -1
)
expected_score = Decimal(1.0 / (1.0 + math.pow(10.0, rating_diff / D)))
stats[stage_item_input_id].points += int(K * (swiss_score_diff - expected_score))

Expand All @@ -80,21 +80,17 @@ def determine_ranking_for_stage_item(
if not round_.is_draft
for match in round_.matches
if isinstance(match, MatchWithDetailsDefinitive)
if match.stage_item_input1_score != 0 or match.stage_item_input2_score != 0
]
for match in matches:
for team_index, stage_item_input in enumerate(match.stage_item_inputs):
if stage_item_input.id is not None:
set_statistics_for_stage_item_input(
team_index,
team_x_stats,
match,
stage_item_input.id,
match.stage_item_input1.elo,
match.stage_item_input2.elo,
ranking,
stage_item,
)
set_statistics_for_stage_item_input(
team_index,
team_x_stats,
match,
stage_item_input.id,
ranking,
stage_item,
)

return team_x_stats

Expand Down
59 changes: 38 additions & 21 deletions backend/bracket/logic/scheduling/handle_stage_activation.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
from collections import defaultdict

from pydantic import BaseModel

from bracket.logic.ranking.elo import (
determine_team_ranking_for_stage_item,
)
from bracket.logic.ranking.statistics import TeamStatistics
from bracket.models.db.stage_item_inputs import StageItemInputFinal, StageItemInputTentative
from bracket.models.db.team import Team
from bracket.models.db.util import StageWithStageItems
from bracket.sql.rankings import get_ranking_for_stage_item
from bracket.sql.stage_item_inputs import (
Expand All @@ -21,6 +26,11 @@
StageItemXTeamRanking = dict[StageItemId, list[tuple[StageItemInputId, TeamStatistics]]]


class StageItemInputUpdate(BaseModel):
stage_item_input: StageItemInputTentative
team: Team


def determine_team_id(
winner_from_stage_item_id: StageItemId,
winner_position: int,
Expand All @@ -41,11 +51,11 @@ def determine_team_id(
return team_ranking[winner_position - 1][0]


async def set_team_ids_for_input(
async def get_team_update_for_input(
tournament_id: TournamentId,
stage_item_input: StageItemInputTentative,
stage_item_x_team_rankings: StageItemXTeamRanking,
) -> None:
) -> StageItemInputUpdate:
target_stage_item_input_id = determine_team_id(
stage_item_input.winner_from_stage_item_id,
stage_item_input.winner_position,
Expand All @@ -55,24 +65,11 @@ async def set_team_ids_for_input(
tournament_id, target_stage_item_input_id
)
assert isinstance(target_stage_item_input, StageItemInputFinal)
await sql_set_team_id_for_stage_item_input(
tournament_id, stage_item_input.id, target_stage_item_input.team_id
return StageItemInputUpdate(
stage_item_input=stage_item_input, team=target_stage_item_input.team
)


async def get_team_rankings_lookup_for_stage(
tournament_id: TournamentId, stage: StageWithStageItems
) -> StageItemXTeamRanking:
stage_items = {stage_item.id: stage_item for stage_item in stage.stage_items}
return {
stage_item_id: determine_team_ranking_for_stage_item(
stage_item,
assert_some(await get_ranking_for_stage_item(tournament_id, stage_item.id)),
)
for stage_item_id, stage_item in stage_items.items()
}


async def get_team_rankings_lookup_for_tournament(
tournament_id: TournamentId, stages: list[StageWithStageItems]
) -> StageItemXTeamRanking:
Expand All @@ -88,9 +85,11 @@ async def get_team_rankings_lookup_for_tournament(
}


async def update_matches_in_activated_stage(tournament_id: TournamentId, stage_id: StageId) -> None:
async def get_updates_to_inputs_in_activated_stage(
tournament_id: TournamentId, stage_id: StageId
) -> dict[StageItemId, list[StageItemInputUpdate]]:
"""
Sets the team_id for stage item inputs of the newly activated stage.
Gets the team_id updates for stage item inputs of the newly activated stage.
"""
stages = await get_full_tournament_details(tournament_id)
team_rankings_per_stage_item = await get_team_rankings_lookup_for_tournament(
Expand All @@ -99,13 +98,31 @@ async def update_matches_in_activated_stage(tournament_id: TournamentId, stage_i
activated_stage = next((stage for stage in stages if stage.id == stage_id), None)
assert activated_stage

result = defaultdict(list)

for stage_item in activated_stage.stage_items:
for stage_item_input in stage_item.inputs:
if isinstance(stage_item_input, StageItemInputTentative):
await set_team_ids_for_input(
tournament_id, stage_item_input, team_rankings_per_stage_item
result[stage_item.id].append(
await get_team_update_for_input(
tournament_id, stage_item_input, team_rankings_per_stage_item
)
)

return dict(result)


async def update_matches_in_activated_stage(tournament_id: TournamentId, stage_id: StageId) -> None:
"""
Sets the team_id for stage item inputs of the newly activated stage.
"""
updates_per_stage_item = await get_updates_to_inputs_in_activated_stage(tournament_id, stage_id)
for stage_item_updates in updates_per_stage_item.values():
for update in stage_item_updates:
await sql_set_team_id_for_stage_item_input(
tournament_id, update.stage_item_input.id, update.team.id
)


async def update_matches_in_deactivated_stage(
tournament_id: TournamentId, deactivated_stage: StageWithStageItems
Expand Down
9 changes: 2 additions & 7 deletions backend/bracket/logic/scheduling/ladder_teams.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import random
from collections import defaultdict

from fastapi import HTTPException

from bracket.logic.scheduling.shared import check_input_combination_adheres_to_filter
from bracket.models.db.match import (
MatchFilter,
Expand Down Expand Up @@ -58,15 +56,12 @@ def get_possible_upcoming_matches_for_swiss(
filter_: MatchFilter,
rounds: list[RoundWithMatches],
stage_item_inputs: list[StageItemInput],
draft_round: RoundWithMatches | None = None,
) -> list[SuggestedMatch]:
suggestions: list[SuggestedMatch] = []
scheduled_hashes: list[str] = []
draft_round = next((round_ for round_ in rounds if round_.is_draft), None)

if draft_round is None:
raise HTTPException(400, "There is no draft round, so no matches can be scheduled.")
draft_round_input_ids = get_draft_round_input_ids(draft_round) if draft_round else frozenset()

draft_round_input_ids = get_draft_round_input_ids(draft_round)
inputs_to_schedule = [
input_ for input_ in stage_item_inputs if input_.id not in draft_round_input_ids
]
Expand Down
11 changes: 6 additions & 5 deletions backend/bracket/logic/scheduling/upcoming_matches.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from bracket.logic.scheduling.ladder_teams import get_possible_upcoming_matches_for_swiss
from bracket.models.db.match import MatchFilter, SuggestedMatch
from bracket.models.db.round import Round
from bracket.models.db.stage_item import StageType
from bracket.models.db.util import RoundWithMatches, StageItemWithRounds
from bracket.sql.rounds import get_rounds_for_stage_item
Expand All @@ -29,17 +28,19 @@ async def get_draft_round_in_stage_item(
return draft_round, stage_item


async def get_upcoming_matches_for_swiss_round(
async def get_upcoming_matches_for_swiss(
match_filter: MatchFilter,
stage_item: StageItemWithRounds,
round_: Round,
tournament_id: TournamentId,
draft_round: RoundWithMatches | None = None,
) -> list[SuggestedMatch]:
if stage_item.type is not StageType.SWISS:
raise HTTPException(400, "Expected stage item to be of type SWISS.")

if not round_.is_draft:
if draft_round is not None and not draft_round.is_draft:
raise HTTPException(400, "There is no draft round, so no matches can be scheduled.")

rounds = await get_rounds_for_stage_item(tournament_id, stage_item.id)
return get_possible_upcoming_matches_for_swiss(match_filter, rounds, stage_item.inputs)
return get_possible_upcoming_matches_for_swiss(
match_filter, rounds, stage_item.inputs, draft_round
)
2 changes: 1 addition & 1 deletion backend/bracket/models/db/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class Player(PlayerInsertable):
id: PlayerId

def __hash__(self) -> int:
return self.id if self.id is not None else int(self.created.timestamp())
return self.id


class PlayerBody(BaseModelORM):
Expand Down
2 changes: 0 additions & 2 deletions backend/bracket/models/db/round.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ class RoundInsertable(BaseModelORM):
created: datetime_utc
stage_item_id: StageItemId
is_draft: bool
is_active: bool = False
name: str


Expand All @@ -19,7 +18,6 @@ class Round(RoundInsertable):
class RoundUpdateBody(BaseModelORM):
name: str
is_draft: bool
is_active: bool


class RoundCreateBody(BaseModelORM):
Expand Down
Loading

0 comments on commit 932e5a2

Please sign in to comment.