Skip to content

Points calculation #77

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

Merged
merged 7 commits into from
Feb 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions algobattle/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,28 +79,28 @@ def run(
result.notify("match")
return result

def calculate_points(self, achievable_points: int) -> dict[Team, float]:
def calculate_points(self, achievable_points: int) -> dict[str, float]:
"""Calculate the number of points each team scored.

Each pair of teams fights for the achievable points among one another.
These achievable points are split over all rounds.
"""
if len(self.teams) == 0:
if len(self.teams.active) == 0:
return {}
if len(self.teams) == 1:
return {self.teams[0]: achievable_points}
if len(self.teams.active) == 1:
return {self.teams.active[0].name: achievable_points}

if any(not 0 <= len(results) <= self.config.rounds for results in self.results.values()):
raise ValueError

points = {team: 0.0 for team in self.teams}
points = {team.name: 0.0 for team in self.teams.active + self.teams.excluded}
if self.config.rounds == 0:
return points
points_per_round = round(achievable_points / ((len(self.teams) - 1) * self.config.rounds), 1)
points_per_battle = round(achievable_points / ((len(self.teams.active) - 1) * self.config.rounds), 1)

for home_matchup, away_matchup in self.teams.grouped_matchups:
home_team = getattr(home_matchup, self.config.battle_type.scoring_team)
away_team = getattr(away_matchup, self.config.battle_type.scoring_team)
home_team: Team = getattr(home_matchup, self.config.battle_type.scoring_team)
away_team: Team = getattr(away_matchup, self.config.battle_type.scoring_team)
for home_res, away_res in zip(self.results[home_matchup], self.results[away_matchup]):
total_score = home_res.score() + away_res.score()
if total_score == 0:
Expand All @@ -111,8 +111,13 @@ def calculate_points(self, achievable_points: int) -> dict[Team, float]:
home_ratio = home_res.score() / total_score
away_ratio = away_res.score() / total_score

points[home_team] += round(points_per_round * home_ratio, 1)
points[away_team] += round(points_per_round * away_ratio, 1)
points[home_team.name] += round(points_per_battle * home_ratio, 1)
points[away_team.name] += round(points_per_battle * away_ratio, 1)

# we need to also add the points each team would have gotten fighting the excluded teams
# each active team would have had one set of battles against each excluded team
for team in self.teams.active:
points[team.name] += points_per_battle * len(self.teams.excluded) * self.config.rounds

return points

Expand Down
33 changes: 24 additions & 9 deletions algobattle/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from dataclasses import dataclass
from itertools import combinations
from pathlib import Path
from typing import Iterator
from typing import Iterable, Iterator, Self
import logging

from algobattle.docker_util import DockerConfig, DockerError, ArchivedImage, Generator, Solver
Expand Down Expand Up @@ -131,12 +131,24 @@ def __iter__(self) -> Iterator[Team]:
yield self.solver


class TeamHandler(list[Team]):
class TeamHandler:
"""Handles building teams and cleaning them up."""

def __init__(self, teams: Iterable[Team] | None = None, excluded: Iterable[TeamInfo] | None = None) -> None:
if teams is not None:
self.active = list(teams)
else:
self.active = []
if excluded is not None:
self.excluded = list(excluded)
else:
self.excluded = []
super().__init__()

@staticmethod
def build(infos: list[TeamInfo], problem: type[Problem], config: DockerConfig, safe_build: bool = False) -> TeamHandler:
"""Builds the specified team objects."""
excluded: list[TeamInfo] = []
if safe_build:
with TempDir() as folder:
archives: list[_ArchivedTeam] = []
Expand All @@ -147,7 +159,9 @@ def build(infos: list[TeamInfo], problem: type[Problem], config: DockerConfig, s
archives.append(team)
except Exception:
logger.warning(f"Building generators and solvers for team {info.name} failed, they will be excluded!")
return TeamHandler([team.restore() for team in archives])
excluded.append(info)

return TeamHandler([team.restore() for team in archives], excluded)
else:
teams: list[Team] = []
for info in infos:
Expand All @@ -156,13 +170,14 @@ def build(infos: list[TeamInfo], problem: type[Problem], config: DockerConfig, s
teams.append(team)
except (ValueError, DockerError):
logger.warning(f"Building generators and solvers for team {info.name} failed, they will be excluded!")
return TeamHandler(teams)
excluded.append(info)
return TeamHandler(teams, excluded)

def __enter__(self):
def __enter__(self) -> Self:
return self

def __exit__(self, _type, _value_, _traceback):
for team in self:
for team in self.active:
team.cleanup()

@property
Expand All @@ -171,12 +186,12 @@ def grouped_matchups(self) -> list[tuple[Matchup, Matchup]]:

Each tuple's first matchup has the first team in the group generating, the second has it solving.
"""
return [(Matchup(*g), Matchup(*g[::-1])) for g in combinations(self, 2)]
return [(Matchup(*g), Matchup(*g[::-1])) for g in combinations(self.active, 2)]

@property
def matchups(self) -> list[Matchup]:
"""All matchups that will be fought."""
if len(self) == 1:
return [Matchup(self[0], self[0])]
if len(self.active) == 1:
return [Matchup(self.active[0], self.active[0])]
else:
return [m for pair in self.grouped_matchups for m in pair]
18 changes: 9 additions & 9 deletions tests/test_match.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def test_all_battle_pairs_single_player(self):
def test_calculate_points_zero_rounds(self):
"""All teams get 0 points if no rounds have been fought."""
match = Match(MatchConfig(rounds=0), Iterated.Config(), TestProblem, self.teams)
self.assertEqual(match.calculate_points(100), {self.team0: 0, self.team1: 0})
self.assertEqual(match.calculate_points(100), {self.team0.name: 0, self.team1.name: 0})

def test_calculate_points_iterated_no_successful_round(self):
"""Two teams should get an equal amount of points if nobody solved anything."""
Expand All @@ -54,7 +54,7 @@ def test_calculate_points_iterated_no_successful_round(self):
battle.reached = 0
match.results[self.matchup0] = [battle, battle]
match.results[self.matchup1] = [battle, battle]
self.assertEqual(match.calculate_points(100), {self.team0: 50, self.team1: 50})
self.assertEqual(match.calculate_points(100), {self.team0.name: 50, self.team1.name: 50})

def test_calculate_points_iterated_draw(self):
"""Two teams should get an equal amount of points if both solved a problem equally well."""
Expand All @@ -65,7 +65,7 @@ def test_calculate_points_iterated_draw(self):
battle2.reached = 10
match.results[self.matchup0] = [battle, battle2]
match.results[self.matchup1] = [battle2, battle]
self.assertEqual(match.calculate_points(100), {self.team0: 50, self.team1: 50})
self.assertEqual(match.calculate_points(100), {self.team0.name: 50, self.team1.name: 50})

def test_calculate_points_iterated_domination(self):
"""One team should get all points if it solved anything and the other team nothing."""
Expand All @@ -76,7 +76,7 @@ def test_calculate_points_iterated_domination(self):
battle2.reached = 0
match.results[self.matchup0] = [battle, battle]
match.results[self.matchup1] = [battle2, battle2]
self.assertEqual(match.calculate_points(100), {self.team0: 0, self.team1: 100})
self.assertEqual(match.calculate_points(100), {self.team0.name: 0, self.team1.name: 100})

def test_calculate_points_iterated_one_team_better(self):
"""One team should get more points than the other if it performed better."""
Expand All @@ -87,7 +87,7 @@ def test_calculate_points_iterated_one_team_better(self):
battle2.reached = 20
match.results[self.matchup0] = [battle, battle]
match.results[self.matchup1] = [battle2, battle2]
self.assertEqual(match.calculate_points(100), {self.team0: 66.6, self.team1: 33.4})
self.assertEqual(match.calculate_points(100), {self.team0.name: 66.6, self.team1.name: 33.4})

def test_calculate_points_averaged_no_successful_round(self):
"""Two teams should get an equal amount of points if nobody solved anything."""
Expand All @@ -96,7 +96,7 @@ def test_calculate_points_averaged_no_successful_round(self):
battle.scores = [0, 0, 0]
match.results[self.matchup0] = [battle, battle]
match.results[self.matchup1] = [battle, battle]
self.assertEqual(match.calculate_points(100), {self.team0: 50, self.team1: 50})
self.assertEqual(match.calculate_points(100), {self.team0.name: 50, self.team1.name: 50})

def test_calculate_points_averaged_draw(self):
"""Two teams should get an equal amount of points if both solved a problem equally well."""
Expand All @@ -105,7 +105,7 @@ def test_calculate_points_averaged_draw(self):
battle.scores = [.5, .5, .5]
match.results[self.matchup0] = [battle, battle]
match.results[self.matchup1] = [battle, battle]
self.assertEqual(match.calculate_points(100), {self.team0: 50, self.team1: 50})
self.assertEqual(match.calculate_points(100), {self.team0.name: 50, self.team1.name: 50})

def test_calculate_points_averaged_domination(self):
"""One team should get all points if it solved anything and the other team nothing."""
Expand All @@ -116,7 +116,7 @@ def test_calculate_points_averaged_domination(self):
battle2.scores = [1, 1, 1]
match.results[self.matchup0] = [battle, battle]
match.results[self.matchup1] = [battle2, battle2]
self.assertEqual(match.calculate_points(100), {self.team0: 100, self.team1: 0})
self.assertEqual(match.calculate_points(100), {self.team0.name: 100, self.team1.name: 0})

def test_calculate_points_averaged_one_team_better(self):
"""One team should get more points than the other if it performed better."""
Expand All @@ -127,7 +127,7 @@ def test_calculate_points_averaged_one_team_better(self):
battle2.scores = [.4, .4, .4]
match.results[self.matchup0] = [battle, battle]
match.results[self.matchup1] = [battle2, battle2]
self.assertEqual(match.calculate_points(100), {self.team0: 60, self.team1: 40})
self.assertEqual(match.calculate_points(100), {self.team0.name: 60, self.team1.name: 40})

# TODO: Add tests for remaining functions

Expand Down