From 3c0adfc06ca948344e869527dd7294275405e90d Mon Sep 17 00:00:00 2001 From: Eddie Bachle Date: Sun, 14 Apr 2024 21:15:00 -0400 Subject: [PATCH] fix: Update logic for division generation to operate based on qualified indicator (#8) --- README.md | 2 +- generate_divisions.py | 23 +++++++++++++++++++++-- models.py | 1 + util.py | 13 +++++++------ validate_divisions.py | 18 ++++++++++++++---- 5 files changed, 44 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 46c9715..a8fa516 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ python generate_divisions.py --divisions-file test_data/divisions.txt --out-file ## Validate Generated Divisions ```bash -python validate_divisions.py --out-file test_data/out.txt --season 2024 --district FIM --api-key "username:guid" +python validate_divisions.py --out-file test_data/out.txt --season 2024 --district FIM --api-key "username:guid" --num-teams 160 ``` ## File Formats diff --git a/generate_divisions.py b/generate_divisions.py index 727bf1d..887aca9 100644 --- a/generate_divisions.py +++ b/generate_divisions.py @@ -8,6 +8,7 @@ import validate_divisions import util + def main( api_key: str = typer.Option(..., help="Formatted as 'username:token'"), season: int = typer.Option(...), @@ -70,10 +71,27 @@ def print_debug(message: str): quartile_size = num_teams // 4 division_quartile_size = quartile_size // len(divisions) - rankings = util.get_rankings(season, district, num_teams, api_key) + rankings = util.get_rankings(season, district, api_key) + + qualified_teams = [team for team in rankings if team.qualified] + + if len(qualified_teams) != num_teams: + typer.echo(f"Expected {num_teams} teams to attend the championship, got {len(qualified_teams)}") + raise typer.Exit(code=1) + + # get the keys of the accomodations dictionary + accommodated_teams = list(accommodations.keys()) + # if accommodated teams are not in qualified teams, raise an error + for team in accommodated_teams: + if team not in [qualified_team.team_number for qualified_team in qualified_teams]: + typer.echo(f"Team {team} is not qualified to attend the championship") + raise typer.Exit(code=1) + + # Let's ensure this is sorted, despite what the API returns + qualified_teams = sorted(qualified_teams, key=lambda team: team.district_points, reverse=True) # Divide into quartiles and shuffle them - quartiles = [rankings[i * quartile_size:(i + 1) * quartile_size] for i in range(4)] + quartiles = [qualified_teams[i * quartile_size:(i + 1) * quartile_size] for i in range(4)] while True: breaks_rule = False @@ -109,6 +127,7 @@ def print_debug(message: str): file.write(f"{team}\n") file.write(f"\n") + if __name__ == "__main__": assert sys.version_info >= (3, 7) typer.run(main) \ No newline at end of file diff --git a/models.py b/models.py index be1e3c7..ae35bca 100644 --- a/models.py +++ b/models.py @@ -5,6 +5,7 @@ class DivisionTeam: team_number: int district_points: int + qualified: bool @dataclass class Division: diff --git a/util.py b/util.py index 2ce6ecb..edecc18 100644 --- a/util.py +++ b/util.py @@ -3,7 +3,8 @@ import models -def get_rankings(season: int, district: str, num_teams: int, api_key: str) -> list[models.DivisionTeam]: + +def get_rankings(season: int, district: str, api_key: str) -> list[models.DivisionTeam]: rankings = [] current_page = 1 total_pages = 99 @@ -13,14 +14,14 @@ def get_rankings(season: int, district: str, num_teams: int, api_key: str) -> li "Accept": "application/json" } - while len(rankings) < 160 and current_page < total_pages: + while current_page <= total_pages: resp = requests.get(f"https://frc-api.firstinspires.org/v3.0/{season}/rankings/district?districtCode={district}&page={current_page}", headers=api_headers) if not resp.ok: raise Exception("Got a bad response from the FRC API: " + str(resp.status_code) + " - " + resp.text) resp = resp.json() if current_page == 1: - total_pages = resp["pageTotal"] - rankings += [models.DivisionTeam(t["teamNumber"], t["totalPoints"]) for t in resp["districtRanks"]] + total_pages = resp["pageTotal"] + rankings += [models.DivisionTeam(t["teamNumber"], t["totalPoints"], t["qualifiedDistrictCmp"]) for t in resp["districtRanks"]] current_page += 1 - - return rankings[:num_teams] \ No newline at end of file + + return rankings diff --git a/validate_divisions.py b/validate_divisions.py index 02782db..3f1991d 100644 --- a/validate_divisions.py +++ b/validate_divisions.py @@ -10,11 +10,13 @@ CHECK_MARK = "✅" X_MARK = "❌" + def __within_tolerance(val_list: list[float], tolerance: float): return max(val_list) - min(val_list) <= tolerance + def validate_divisions(division_output: list[models.Division]) -> bool: - '''Checks that divisions conform with the tolerances set in the FRC game manual''' + """Checks that divisions conform with the tolerances set in the FRC game manual""" if not __within_tolerance([d.strength() for d in division_output], 2): return False if not __within_tolerance([d.snr() for d in division_output], 2.5): @@ -23,11 +25,13 @@ def validate_divisions(division_output: list[models.Division]) -> bool: return False return True + def main( out_file: Path = typer.Option(..., exists=True, file_okay=True, dir_okay=False, resolve_path=True, readable=True), api_key: str = typer.Option(..., help="Formatted as 'username:token'"), season: int = typer.Option(...), district: str = typer.Option(..., help="FRC District Code"), + num_teams: int = typer.Option(..., help="The total number of teams to attend the championship") ): typer.echo("NOTE: this tool assumes that all constraints from accommodations have been fulfilled") @@ -41,11 +45,17 @@ def main( name = lines[0] team_mapping[name] = [int(line) for line in lines[1:]] - rankings = util.get_rankings(season, district, sum([len(v) for v in team_mapping.values()]), api_key) + rankings = util.get_rankings(season, district, api_key) team_rankings_map = {team.team_number: team for team in rankings} + team_count = sum(len(v) for v in team_mapping.values()) + + if team_count != num_teams: + typer.echo(typer.style(f"{X_MARK} Total number of teams does not match: {team_count} != {num_teams}", fg="red")) + raise typer.Exit(code=1) + divisions = [models.Division(k, [team_rankings_map[t] for t in v]) for (k, v) in team_mapping.items()] - + # Check that the math works out is_valid = validate_divisions(divisions) if is_valid: @@ -64,4 +74,4 @@ def main( if __name__ == "__main__": assert sys.version_info >= (3, 7) - typer.run(main) \ No newline at end of file + typer.run(main)