Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rank by points #749

Closed
wants to merge 14 commits into from
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""add multi ranking calculation support

Revision ID: 450cddf36bef
Revises: 1961954c0320
Create Date: 2024-05-27 15:04:16.583628

"""

import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import ENUM

from alembic import op

# revision identifiers, used by Alembic.
revision: str | None = "450cddf36bef"
down_revision: str | None = "1961954c0320"
branch_labels: str | None = None
depends_on: str | None = None


ranking_mode = ENUM("HIGHEST_ELO", "HIGHEST_POINTS", name="ranking_mode", create_type=True)


def upgrade() -> None:
ranking_mode.create(op.get_bind(), checkfirst=False)
op.add_column(
"stage_items", sa.Column("ranking_mode", ranking_mode, server_default=None, nullable=True)
)
op.add_column(
"players", sa.Column("game_points", sa.Integer(), server_default="0", nullable=False)
)
op.add_column(
"teams", sa.Column("game_points", sa.Integer(), server_default="0", nullable=False)
)


def downgrade() -> None:
op.drop_column("stage_items", "ranking_mode")
ranking_mode.drop(op.get_bind(), checkfirst=False)
op.drop_column("players", "game_points")
op.drop_column("teams", "game_points")
10 changes: 9 additions & 1 deletion backend/bracket/logic/ranking/elo.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from bracket.database import database
from bracket.models.db.match import MatchWithDetailsDefinitive
from bracket.models.db.players import START_ELO, PlayerStatistics
from bracket.models.db.stage_item import RankingMode
from bracket.models.db.util import StageItemWithRounds
from bracket.schema import players, teams
from bracket.sql.players import get_all_players_in_tournament, update_player_stats
Expand Down Expand Up @@ -44,6 +45,7 @@ def set_statistics_for_player_or_team(
stats[team_or_player_id].losses += 1
swiss_score_diff = Decimal("0.00")

stats[team_or_player_id].game_points += match.team1_score if is_team1 else match.team2_score
stats[team_or_player_id].swiss_score += swiss_score_diff

rating_diff = (rating_team2_before - rating_team1_before) * (1 if is_team1 else -1)
Expand Down Expand Up @@ -105,9 +107,15 @@ def determine_ranking_for_stage_items(

def determine_team_ranking_for_stage_item(
stage_item: StageItemWithRounds,
ranking_mode: RankingMode | None = None,
) -> list[tuple[TeamId, PlayerStatistics]]:
_, team_ranking = determine_ranking_for_stage_items([stage_item])
return sorted(team_ranking.items(), key=lambda x: x[1].elo_score, reverse=True)

match ranking_mode:
case RankingMode.HIGHEST_POINTS:
return sorted(team_ranking.items(), key=lambda x: x[1].game_points, reverse=True)
case _:
return sorted(team_ranking.items(), key=lambda x: x[1].elo_score, reverse=True)


async def recalculate_ranking_for_tournament_id(tournament_id: TournamentId) -> None:
Expand Down
12 changes: 9 additions & 3 deletions backend/bracket/logic/scheduling/handle_stage_activation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
determine_team_ranking_for_stage_item,
)
from bracket.models.db.match import MatchWithDetails
from bracket.models.db.stage_item import RankingMode
from bracket.sql.matches import sql_get_match, sql_update_team_ids_for_match
from bracket.sql.stage_items import get_stage_item
from bracket.sql.stages import get_full_tournament_details
Expand All @@ -14,12 +15,13 @@ async def determine_team_id(
winner_from_stage_item_id: StageItemId | None,
winner_position: int | None,
winner_from_match_id: MatchId | None,
ranking_mode: RankingMode | None,
) -> TeamId | None:
if winner_from_stage_item_id is not None and winner_position is not None:
stage_item = await get_stage_item(tournament_id, winner_from_stage_item_id)
assert stage_item is not None

team_ranking = determine_team_ranking_for_stage_item(stage_item)
team_ranking = determine_team_ranking_for_stage_item(stage_item, ranking_mode)
if len(team_ranking) >= winner_position:
return team_ranking[winner_position - 1][0]

Expand All @@ -38,18 +40,22 @@ async def determine_team_id(
raise ValueError("Unexpected match type")


async def set_team_ids_for_match(tournament_id: TournamentId, match: MatchWithDetails) -> None:
async def set_team_ids_for_match(
tournament_id: TournamentId, match: MatchWithDetails, ranking_mode: RankingMode | None
) -> None:
team1_id = await determine_team_id(
tournament_id,
match.team1_winner_from_stage_item_id,
match.team1_winner_position,
match.team1_winner_from_match_id,
ranking_mode,
)
team2_id = await determine_team_id(
tournament_id,
match.team2_winner_from_stage_item_id,
match.team2_winner_position,
match.team2_winner_from_match_id,
ranking_mode,
)

await sql_update_team_ids_for_match(assert_some(match.id), team1_id, team2_id)
Expand All @@ -62,4 +68,4 @@ async def update_matches_in_activated_stage(tournament_id: TournamentId, stage_i
for round_ in stage_item.rounds:
for match in round_.matches:
if isinstance(match, MatchWithDetails):
await set_team_ids_for_match(tournament_id, match)
await set_team_ids_for_match(tournament_id, match, stage_item.ranking_mode)
2 changes: 2 additions & 0 deletions backend/bracket/models/db/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Player(BaseModelORM):
wins: int = 0
draws: int = 0
losses: int = 0
game_points: int = 0

def __hash__(self) -> int:
return self.id if self.id is not None else int(self.created.timestamp())
Expand All @@ -41,3 +42,4 @@ class PlayerToInsert(PlayerBody):
wins: int = 0
draws: int = 0
losses: int = 0
game_points: int = 0
1 change: 1 addition & 0 deletions backend/bracket/models/db/players.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ class PlayerStatistics(BaseModel):
losses: int = 0
elo_score: int = START_ELO
swiss_score: Decimal = Decimal("0.00")
game_points: int = 0
7 changes: 7 additions & 0 deletions backend/bracket/models/db/stage_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,19 @@ def supports_dynamic_number_of_rounds(self) -> bool:
return self in [StageType.SWISS]


class RankingMode(EnumAutoStr):
HIGHEST_ELO = auto()
HIGHEST_POINTS = auto()


class StageItemToInsert(BaseModelORM):
id: StageItemId | None = None
stage_id: StageId
name: str
created: datetime_utc
type: StageType
team_count: int = Field(ge=2, le=64)
ranking_mode: RankingMode | None = None


class StageItem(StageItemToInsert):
Expand All @@ -47,6 +53,7 @@ class StageItemCreateBody(BaseModelORM):
type: StageType
team_count: int = Field(ge=2, le=64)
inputs: list[StageItemInputCreateBody]
ranking_mode: RankingMode | None = None

def get_name_or_default_name(self) -> str:
return self.name if self.name is not None else self.type.value.replace("_", " ").title()
Expand Down
2 changes: 2 additions & 0 deletions backend/bracket/models/db/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class TeamWithPlayers(BaseModel):
losses: int = 0
name: str
logo_path: str | None = None
game_points: int = 0

@property
def player_ids(self) -> list[PlayerId]:
Expand Down Expand Up @@ -100,3 +101,4 @@ class TeamToInsert(BaseModelORM):
wins: int = 0
draws: int = 0
losses: int = 0
game_points: int = 0
5 changes: 5 additions & 0 deletions backend/bracket/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
),
nullable=False,
),
Column(
"ranking_mode", Enum("HIGHEST_POINTS", "HIGHEST_ELO", name="ranking_mode"), nullable=True
),
)

stage_item_inputs = Table(
Expand Down Expand Up @@ -134,6 +137,7 @@
Column("draws", Integer, nullable=False, server_default="0"),
Column("losses", Integer, nullable=False, server_default="0"),
Column("logo_path", String, nullable=True),
Column("game_points", Integer, nullable=False, server_default="0"),
)

players = Table(
Expand All @@ -149,6 +153,7 @@
Column("draws", Integer, nullable=False),
Column("losses", Integer, nullable=False),
Column("active", Boolean, nullable=False, index=True, server_default="t"),
Column("game_points", Integer, nullable=False, server_default="0"),
)

users = Table(
Expand Down
4 changes: 3 additions & 1 deletion backend/bracket/sql/players.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ async def update_player_stats(
draws = :draws,
losses = :losses,
elo_score = :elo_score,
swiss_score = :swiss_score
swiss_score = :swiss_score,
game_points = :game_points
WHERE players.tournament_id = :tournament_id
AND players.id = :player_id
"""
Expand All @@ -101,6 +102,7 @@ async def update_player_stats(
"losses": player_statistics.losses,
"elo_score": player_statistics.elo_score,
"swiss_score": float(player_statistics.swiss_score),
"game_points": player_statistics.game_points,
},
)

Expand Down
5 changes: 3 additions & 2 deletions backend/bracket/sql/stage_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ async def sql_create_stage_item(
) -> StageItem:
async with database.transaction():
query = """
INSERT INTO stage_items (type, stage_id, name, team_count)
VALUES (:stage_item_type, :stage_id, :name, :team_count)
INSERT INTO stage_items (type, stage_id, name, team_count, ranking_mode)
VALUES (:stage_item_type, :stage_id, :name, :team_count, :ranking_mode)
RETURNING *
"""
result = await database.fetch_one(
Expand All @@ -22,6 +22,7 @@ async def sql_create_stage_item(
"stage_id": stage_item.stage_id,
"name": stage_item.get_name_or_default_name(),
"team_count": stage_item.team_count,
"ranking_mode": stage_item.ranking_mode.value if stage_item.ranking_mode else None,
},
)

Expand Down
4 changes: 3 additions & 1 deletion backend/bracket/sql/teams.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ async def update_team_stats(
draws = :draws,
losses = :losses,
elo_score = :elo_score,
swiss_score = :swiss_score
swiss_score = :swiss_score,
game_points = :game_points
WHERE teams.tournament_id = :tournament_id
AND teams.id = :team_id
"""
Expand All @@ -132,6 +133,7 @@ async def update_team_stats(
"losses": team_statistics.losses,
"elo_score": team_statistics.elo_score,
"swiss_score": float(team_statistics.swiss_score),
"game_points": team_statistics.game_points,
},
)

Expand Down
20 changes: 18 additions & 2 deletions backend/bracket/utils/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,28 @@ class Pagination:
@dataclass
class PaginationPlayers(Pagination):
sort_by: Literal[
"name", "elo_score", "swiss_score", "wins", "draws", "losses", "active", "created"
"name",
"elo_score",
"swiss_score",
"wins",
"draws",
"losses",
"active",
"created",
"game_points",
] = "name"


@dataclass
class PaginationTeams(Pagination):
sort_by: Literal[
"name", "elo_score", "swiss_score", "wins", "draws", "losses", "active", "created"
"name",
"elo_score",
"swiss_score",
"wins",
"draws",
"losses",
"active",
"created",
"game_points",
] = "name"
6 changes: 6 additions & 0 deletions backend/tests/integration_tests/api/matches_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ async def test_upcoming_matches_endpoint(
"wins": 0,
"draws": 0,
"losses": 0,
"game_points": 0,
},
{
"id": player_inserted_3.id,
Expand All @@ -267,6 +268,7 @@ async def test_upcoming_matches_endpoint(
"wins": 0,
"draws": 0,
"losses": 0,
"game_points": 0,
},
],
"swiss_score": "0.0",
Expand All @@ -275,6 +277,7 @@ async def test_upcoming_matches_endpoint(
"draws": 0,
"losses": 0,
"logo_path": None,
"game_points": 0,
},
"team2": {
"id": team2_inserted.id,
Expand All @@ -291,6 +294,7 @@ async def test_upcoming_matches_endpoint(
"wins": 0,
"draws": 0,
"losses": 0,
"game_points": 0,
},
{
"id": player_inserted_4.id,
Expand All @@ -303,6 +307,7 @@ async def test_upcoming_matches_endpoint(
"wins": 0,
"draws": 0,
"losses": 0,
"game_points": 0,
},
],
"swiss_score": "0.0",
Expand All @@ -311,6 +316,7 @@ async def test_upcoming_matches_endpoint(
"draws": 0,
"losses": 0,
"logo_path": None,
"game_points": 0,
},
"elo_diff": "200",
"swiss_diff": "0",
Expand Down
1 change: 1 addition & 0 deletions backend/tests/integration_tests/api/players_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ async def test_players_endpoint(
"losses": 0,
"name": "Player 01",
"tournament_id": auth_context.tournament.id,
"game_points": 0,
}
],
"count": 1,
Expand Down
1 change: 1 addition & 0 deletions backend/tests/integration_tests/api/stages_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ async def test_stages_endpoint(
"created": DUMMY_MOCK_TIME.isoformat().replace("+00:00", "Z"),
"type": "ROUND_ROBIN",
"team_count": 4,
"ranking_mode": None,
"rounds": [
{
"id": round_inserted.id,
Expand Down
1 change: 1 addition & 0 deletions backend/tests/integration_tests/api/teams_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ async def test_teams_endpoint(
"draws": 0,
"losses": 0,
"logo_path": None,
"game_points": 0,
}
],
"count": 1,
Expand Down
Loading