diff --git a/airsenal/framework/optimization_transfers.py b/airsenal/framework/optimization_transfers.py index ac754359..0e8e8700 100644 --- a/airsenal/framework/optimization_transfers.py +++ b/airsenal/framework/optimization_transfers.py @@ -388,6 +388,21 @@ def make_best_transfers( players_out = [p for p in _out if p not in _in] # remove duplicates transfer_dict = {"in": players_in, "out": players_out} + elif isinstance(num_transfers, int) and num_transfers > 2: + new_squad, players_out, players_in = make_random_transfers( + squad, + tag, + nsubs=num_transfers, + gw_range=gameweeks, + root_gw=root_gw, + num_iter=num_iter, + update_func_and_args=update_func_and_args, + season=season, + bench_boost_gw=bench_boost_gw, + triple_captain_gw=triple_captain_gw, + ) + transfer_dict = {"in": players_in, "out": players_out} + else: raise RuntimeError(f"Unrecognized value for num_transfers: {num_transfers}") diff --git a/airsenal/framework/optimization_utils.py b/airsenal/framework/optimization_utils.py index b4ee1e6b..bfbb63e8 100644 --- a/airsenal/framework/optimization_utils.py +++ b/airsenal/framework/optimization_utils.py @@ -29,6 +29,7 @@ positions = ["FWD", "MID", "DEF", "GK"] # front-to-back DEFAULT_SUB_WEIGHTS = {"GK": 0.03, "Outfield": (0.65, 0.3, 0.1)} +MAX_FREE_TRANSFERS = 5 # changed in 24/25 season (not accounted for in replay season) def check_tag_valid(pred_tag, gameweek_range, season=CURRENT_SEASON, dbsession=session): @@ -63,35 +64,41 @@ def calc_points_hit(num_transfers, free_transfers): """ if num_transfers in ["W", "F"]: return 0 - elif isinstance(num_transfers, int): - return max(0, 4 * (num_transfers - free_transfers)) - elif (num_transfers.startswith("B") or num_transfers.startswith("T")) and len( - num_transfers - ) == 2: + if ( + isinstance(num_transfers, str) + and num_transfers.startswith(("B", "T")) + and len(num_transfers) == 2 + ): num_transfers = int(num_transfers[-1]) - return max(0, 4 * (num_transfers - free_transfers)) - else: + if not isinstance(num_transfers, int): raise RuntimeError(f"Unexpected argument for num_transfers {num_transfers}") + return max(0, 4 * (num_transfers - free_transfers)) + -def calc_free_transfers(num_transfers, prev_free_transfers): +def calc_free_transfers( + num_transfers, prev_free_transfers, max_free_transfers=MAX_FREE_TRANSFERS +): """ We get one extra free transfer per week, unless we use a wildcard or - free hit, but we can't have more than 2. So we should only be able - to return 1 or 2. + free hit, but we can't have more than 5. So we should only be able + to return 1 to 5. """ if num_transfers in ["W", "F"]: - return 1 - elif isinstance(num_transfers, int): - return max(1, min(2, 1 + prev_free_transfers - num_transfers)) - elif (num_transfers.startswith("B") or num_transfers.startswith("T")) and len( - num_transfers - ) == 2: - # take the 'x' out of Bx or Tx + return prev_free_transfers # changed in 24/25 season, previously 1 + + if ( + isinstance(num_transfers, str) + and num_transfers.startswith(("B", "T")) + and len(num_transfers) == 2 + ): + # take the 'x' out of Bx or Tx (bench boost or triple captain with x transfers) num_transfers = int(num_transfers[-1]) - return max(1, min(2, 1 + prev_free_transfers - num_transfers)) - else: - raise RuntimeError(f"Unexpected argument for num_transfers {num_transfers}") + + if not isinstance(num_transfers, int): + raise ValueError(f"Unexpected input for num_transfers {num_transfers}") + + return max(1, min(max_free_transfers, 1 + prev_free_transfers - num_transfers)) def get_starting_squad( @@ -429,8 +436,9 @@ def next_week_transfers( strat, max_total_hit=None, allow_unused_transfers=True, - max_transfers=2, + max_opt_transfers=2, chips={"chips_allowed": [], "chip_to_play": None}, + max_free_transfers=MAX_FREE_TRANSFERS, ): """Given a previous strategy and some optimisation constraints, determine the valid options for the number of transfers (or chip played) in the following gameweek. @@ -438,6 +446,14 @@ def next_week_transfers( strat is a tuple (free_transfers, hit_so_far, strat_dict) strat_dict must have key chips_played, which is a dict indexed by gameweek with possible values None, "wildcard", "free_hit", "bench_boost" or triple_captain" + + max_opt_transfers - maximum number of transfers to play each week as part of + strategy in optimisation + + max_free_transfers - maximum number of free transfers saved in the game rules + (2 before 2024/25, 5 from 2024/25 season) + + Returns (new_transfers, new_ft_available, new_points_hits) tuples. """ # check that the 'chips' dict we are given makes sense: if ( @@ -453,13 +469,13 @@ def next_week_transfers( ft_available, hit_so_far, strat_dict = strat chip_history = strat_dict["chips_played"] - if not allow_unused_transfers and ft_available == 2: - # Force at least 1 free transfer. - # NOTE: This will exclude the baseline strategy when allow_unused_transfers - # is False. Re-add it outside this function in that case. - ft_choices = list(range(1, max_transfers + 1)) + if not allow_unused_transfers and ft_available == max_free_transfers: + # Force at least 1 free transfer if a free transfer will be lost otherwise. + # NOTE: This can cause the baseline strategy to be excluded. Re-add it outside + # this function in that case. + ft_choices = list(range(1, max_opt_transfers + 1)) else: - ft_choices = list(range(max_transfers + 1)) + ft_choices = list(range(max_opt_transfers + 1)) if max_total_hit is not None: ft_choices = [ @@ -515,7 +531,10 @@ def next_week_transfers( new_points_hits = [ hit_so_far + calc_points_hit(nt, ft_available) for nt in new_transfers ] - new_ft_available = [calc_free_transfers(nt, ft_available) for nt in new_transfers] + new_ft_available = [ + calc_free_transfers(nt, ft_available, max_free_transfers) + for nt in new_transfers + ] # return list of (num_transfers, free_transfers, hit_so_far) tuples for each new # strategy @@ -523,14 +542,15 @@ def next_week_transfers( def count_expected_outputs( - gw_ahead, - next_gw=NEXT_GAMEWEEK, - free_transfers=1, - max_total_hit=None, - allow_unused_transfers=True, - max_transfers=2, - chip_gw_dict={}, -): + gw_ahead: int, + next_gw: int = NEXT_GAMEWEEK, + free_transfers: int = 1, + max_total_hit: Optional[int] = None, + allow_unused_transfers: bool = True, + max_opt_transfers: int = 2, + chip_gw_dict: dict = {}, + max_free_transfers: int = MAX_FREE_TRANSFERS, +) -> tuple[int, bool]: """ Count the number of possible transfer and chip strategies for gw_ahead gameweeks ahead, subject to: @@ -540,8 +560,15 @@ def count_expected_outputs( * Allow playing the chips which have their allow_xxx argument set True * Exclude strategies that waste free transfers (make 0 transfers if 2 free tramsfers are available), if allow_unused_transfers is False. - * Make a maximum of max_transfers transfers each gameweek. + * Make a maximum of max_opt_transfers transfers each gameweek. * Each chip only allowed once. + + Returns + ------- + Tuple of int: number of strategies that will be computed, and bool: whether the + baseline strategy will be excluded from the main optimization tree and will need + to be computed separately (this can be the case if allow_unused_transfers is + False). Either way, the total count of strategies will include the baseline. """ init_strat_dict = { @@ -559,9 +586,10 @@ def count_expected_outputs( possibilities = next_week_transfers( s, max_total_hit=max_total_hit, - max_transfers=max_transfers, + max_opt_transfers=max_opt_transfers, allow_unused_transfers=allow_unused_transfers, chips=chips_for_gw, + max_free_transfers=max_free_transfers, ) for n_transfers, new_free_transfers, new_hit in possibilities: @@ -593,18 +621,20 @@ def count_expected_outputs( strategies = new_strategies - # if allow_unused_transfers is False baseline of no transfers will be removed above, - # add it back in here, apart from edge cases where it's already included. - if not allow_unused_transfers and ( - gw_ahead > 1 or (gw_ahead == 1 and init_free_transfers == 2) - ): - baseline_strat_dict = { - "players_in": {gw: [] for gw in range(next_gw, next_gw + gw_ahead)}, - "chips_played": {}, - } - baseline_dict = (2, 0, baseline_strat_dict) + # if allow_unused_transfers is False baseline of no transfers can be removed above. + # Check whether 1st strategy is the baseline and if not add it back in here + baseline_strat_dict = { + "players_in": {gw: [] for gw in range(next_gw, next_gw + gw_ahead)}, + "chips_played": {}, + } + if strategies[0][2] != baseline_strat_dict: + baseline_dict = (max_free_transfers, 0, baseline_strat_dict) strategies.insert(0, baseline_dict) - return len(strategies) + baseline_excluded = True + else: + baseline_excluded = False + + return len(strategies), baseline_excluded def get_discount_factor(next_gw, pred_gw, discount_type="exp", discount=14 / 15): diff --git a/airsenal/scripts/airsenal_run_pipeline.py b/airsenal/scripts/airsenal_run_pipeline.py index 48dda293..f777a720 100644 --- a/airsenal/scripts/airsenal_run_pipeline.py +++ b/airsenal/scripts/airsenal_run_pipeline.py @@ -110,8 +110,11 @@ ) @click.option( "--max_transfers", - help="specify maximum number of transfers to be made each gameweek (defaults to 2)", - type=click.IntRange(min=0, max=2), + help=( + "specify maximum number of transfers to consider each gameweek [EXPERIMENTAL: " + "increasing this value above 2 may make the optimisation very slow!]" + ), + type=click.IntRange(min=0, max=5), default=2, ) @click.option( @@ -361,7 +364,7 @@ def run_optimize_squad( fpl_team_id=fpl_team_id, num_thread=num_thread, chip_gameweeks=chips_played, - max_transfers=max_transfers, + max_opt_transfers=max_transfers, max_total_hit=max_hit, allow_unused_transfers=allow_unused, ) diff --git a/airsenal/scripts/fill_transfersuggestion_table.py b/airsenal/scripts/fill_transfersuggestion_table.py index b5912bdb..94644a83 100644 --- a/airsenal/scripts/fill_transfersuggestion_table.py +++ b/airsenal/scripts/fill_transfersuggestion_table.py @@ -39,7 +39,7 @@ ) from airsenal.framework.optimization_transfers import make_best_transfers from airsenal.framework.optimization_utils import ( - calc_free_transfers, + MAX_FREE_TRANSFERS, calc_points_hit, check_tag_valid, count_expected_outputs, @@ -95,6 +95,7 @@ def optimize( updater: Optional[Callable] = None, resetter: Optional[Callable] = None, profile: bool = False, + max_free_transfers: int = MAX_FREE_TRANSFERS, ) -> None: """ Queue is the multiprocessing queue, @@ -213,7 +214,6 @@ def optimize( strat_dict["discount_factor"][gw] = discount_factor strat_dict["players_in"][gw] = transfers["in"] strat_dict["players_out"][gw] = transfers["out"] - free_transfers = calc_free_transfers(num_transfers, free_transfers) depth += 1 @@ -235,8 +235,9 @@ def optimize( (free_transfers, hit_so_far, strat_dict), max_total_hit=max_total_hit, allow_unused_transfers=allow_unused_transfers, - max_transfers=max_transfers, + max_opt_transfers=max_transfers, chips=chips_gw_dict[gw + 1], + max_free_transfers=max_free_transfers, ) for strat in strategies: @@ -407,11 +408,12 @@ def run_optimization( num_free_transfers: Optional[int] = None, max_total_hit: Optional[int] = None, allow_unused_transfers: bool = False, - max_transfers: int = 2, + max_opt_transfers: int = 2, num_iterations: int = 100, num_thread: int = 4, profile: bool = False, is_replay: bool = False, # for replaying seasons + max_free_transfers: int = MAX_FREE_TRANSFERS, ): """ This is the actual main function that sets up the multiprocessing @@ -467,7 +469,7 @@ def run_optimization( # if we got to here, we can assume we are optimizing an existing squad. # How many free transfers are we starting with? - if not num_free_transfers: + if num_free_transfers is None: num_free_transfers = get_free_transfers( fpl_team_id, gameweeks[0], @@ -505,14 +507,15 @@ def run_optimization( # number of nodes in tree will be something like 3^num_weeks unless we allow # a "chip" such as wildcard or free hit, in which case it gets complicated num_weeks = len(gameweeks) - num_expected_outputs = count_expected_outputs( + num_expected_outputs, baseline_excluded = count_expected_outputs( num_weeks, next_gw=gameweeks[0], free_transfers=num_free_transfers, max_total_hit=max_total_hit, allow_unused_transfers=allow_unused_transfers, - max_transfers=max_transfers, + max_opt_transfers=max_opt_transfers, chip_gw_dict=chip_gw_dict, + max_free_transfers=max_free_transfers, ) total_progress = tqdm(total=num_expected_outputs, desc="Total progress") @@ -539,9 +542,7 @@ def update_progress(increment=1, index=None): progress_bars[index].update(increment) progress_bars[index].refresh() - if not allow_unused_transfers and ( - num_weeks > 1 or (num_weeks == 1 and num_free_transfers == 2) - ): + if baseline_excluded: # if we are excluding unused transfers the tree may not include the baseline # strategy. In those cases quickly calculate and save it here first. save_baseline_score(starting_squad, gameweeks, tag) @@ -568,7 +569,7 @@ def update_progress(increment=1, index=None): chip_gw_dict, max_total_hit, allow_unused_transfers, - max_transfers, + max_opt_transfers, num_iterations, update_progress, reset_progress, @@ -697,8 +698,8 @@ def sanity_check_args(args: argparse.Namespace) -> bool: args.gameweek_end and not args.gameweek_start ): raise RuntimeError("Need to specify both gameweek_start and gameweek_end") - if args.num_free_transfers and args.num_free_transfers not in range(1, 3): - raise RuntimeError("Number of free transfers must be 1 or 2") + if args.num_free_transfers and args.num_free_transfers not in range(6): + raise RuntimeError("Number of free transfers must be 0 to 5") return True @@ -751,6 +752,15 @@ def main(): help="if set, include strategies that waste free transfers", action="store_true", ) + parser.add_argument( + "--max_transfers", + help=( + "maximum number of transfers to consider each gameweek [EXPERIMENTAL: " + "increasing this value above 2 make the optimisation very slow!]" + ), + type=int, + default=2, + ) parser.add_argument( "--num_iterations", help="how many iterations to use for Wildcard/Free Hit optimization", @@ -835,7 +845,7 @@ def main(): num_free_transfers, max_total_hit, allow_unused_transfers, - 2, + args.max_transfers, num_iterations, num_thread, profile, diff --git a/airsenal/scripts/replay_season.py b/airsenal/scripts/replay_season.py index aabb39a3..881809cd 100644 --- a/airsenal/scripts/replay_season.py +++ b/airsenal/scripts/replay_season.py @@ -64,6 +64,7 @@ def replay_season( team_model: str = "extended", team_model_args: dict = {"epsilon": 0.0}, fpl_team_id: Optional[int] = None, + max_opt_transfers: int = 2, ) -> None: start = datetime.now() if gameweek_end is None: @@ -130,6 +131,7 @@ def replay_season( fpl_team_id=fpl_team_id, num_thread=num_thread, is_replay=True, + max_opt_transfers=max_opt_transfers, ) gw_result["starting_11"] = [] gw_result["subs"] = [] @@ -227,6 +229,15 @@ def main(): type=float, default=0.0, ) + parser.add_argument( + "--max_transfers", + help=( + "maximum number of transfers to consider each gameweek [EXPERIMENTAL: " + "increasing this value above 2 may make the optimisation very slow!]" + ), + type=int, + default=2, + ) args = parser.parse_args() if args.resume and not args.fpl_team_id: @@ -251,6 +262,7 @@ def main(): fpl_team_id=args.fpl_team_id, team_model=args.team_model, team_model_args={"epsilon": args.epsilon}, + max_opt_transfers=args.max_transfers, ) n_completed += 1 diff --git a/airsenal/tests/test_optimization.py b/airsenal/tests/test_optimization.py index 8ec24758..39388e4b 100644 --- a/airsenal/tests/test_optimization.py +++ b/airsenal/tests/test_optimization.py @@ -282,20 +282,63 @@ def test_next_week_transfers_no_chips_no_constraints(): strat, max_total_hit=None, allow_unused_transfers=True, - max_transfers=2, + max_opt_transfers=2, ) # (no. transfers, free transfers following week, points hit) expected = [(0, 2, 0), (1, 1, 0), (2, 1, 4)] assert actual == expected +def test_next_week_transfers_no_chips_no_constraints_max5(): + # First week (blank starting strat with 1 free transfer available) + strat = (1, 0, {"players_in": {}, "chips_played": {}}) + # No chips or constraints + actual = next_week_transfers( + strat, + max_total_hit=None, + allow_unused_transfers=True, + max_opt_transfers=5, + ) + # (no. transfers, free transfers following week, points hit) + expected = [(0, 2, 0), (1, 1, 0), (2, 1, 4), (3, 1, 8), (4, 1, 12), (5, 1, 16)] + assert actual == expected + + def test_next_week_transfers_any_chip_no_constraints(): # All chips, no constraints strat = (1, 0, {"players_in": {}, "chips_played": {}}) actual = next_week_transfers( strat, max_total_hit=None, - max_transfers=2, + max_opt_transfers=2, + chips={ + "chips_allowed": ["wildcard", "free_hit", "bench_boost", "triple_captain"], + "chip_to_play": None, + }, + ) + expected = [ + (0, 2, 0), + (1, 1, 0), + (2, 1, 4), + ("W", 1, 0), + ("F", 1, 0), + ("B0", 2, 0), + ("B1", 1, 0), + ("B2", 1, 4), + ("T0", 2, 0), + ("T1", 1, 0), + ("T2", 1, 4), + ] + assert actual == expected + + +def test_next_week_transfers_any_chip_no_constraints_max5(): + # All chips, no constraints + strat = (1, 0, {"players_in": {}, "chips_played": {}}) + actual = next_week_transfers( + strat, + max_total_hit=None, + max_opt_transfers=5, chips={ "chips_allowed": ["wildcard", "free_hit", "bench_boost", "triple_captain"], "chip_to_play": None, @@ -305,14 +348,23 @@ def test_next_week_transfers_any_chip_no_constraints(): (0, 2, 0), (1, 1, 0), (2, 1, 4), + (3, 1, 8), + (4, 1, 12), + (5, 1, 16), ("W", 1, 0), ("F", 1, 0), ("B0", 2, 0), ("B1", 1, 0), ("B2", 1, 4), + ("B3", 1, 8), + ("B4", 1, 12), + ("B5", 1, 16), ("T0", 2, 0), ("T1", 1, 0), ("T2", 1, 4), + ("T3", 1, 8), + ("T4", 1, 12), + ("T5", 1, 16), ] assert actual == expected @@ -324,7 +376,20 @@ def test_next_week_transfers_no_chips_zero_hit(): strat, max_total_hit=0, allow_unused_transfers=True, - max_transfers=2, + max_opt_transfers=2, + ) + expected = [(0, 2, 0), (1, 1, 0)] + assert actual == expected + + +def test_next_week_transfers_no_chips_zero_hit_max5(): + # No points hits + strat = (1, 0, {"players_in": {}, "chips_played": {}}) + actual = next_week_transfers( + strat, + max_total_hit=0, + allow_unused_transfers=True, + max_opt_transfers=5, ) expected = [(0, 2, 0), (1, 1, 0)] assert actual == expected @@ -337,12 +402,41 @@ def test_next_week_transfers_2ft_no_unused(): strat, max_total_hit=None, allow_unused_transfers=False, - max_transfers=2, + max_opt_transfers=2, + max_free_transfers=2, ) expected = [(1, 2, 0), (2, 1, 0)] assert actual == expected +def test_next_week_transfers_5ft_no_unused_max5(): + # 2 free transfers available, no wasted transfers + strat = (5, 0, {"players_in": {}, "chips_played": {}}) + actual = next_week_transfers( + strat, + max_total_hit=None, + allow_unused_transfers=False, + max_opt_transfers=5, + max_free_transfers=5, + ) + expected = [(1, 5, 0), (2, 4, 0), (3, 3, 0), (4, 2, 0), (5, 1, 0)] + assert actual == expected + + +def test_next_week_transfers_3ft_no_hit_max5(): + # 2 free transfers available, no wasted transfers + strat = (3, 0, {"players_in": {}, "chips_played": {}}) + actual = next_week_transfers( + strat, + max_total_hit=0, + allow_unused_transfers=False, + max_opt_transfers=5, + max_free_transfers=5, + ) + expected = [(0, 4, 0), (1, 3, 0), (2, 2, 0), (3, 1, 0)] + assert actual == expected + + def test_next_week_transfers_chips_already_used(): # Chips allowed but previously used strat = ( @@ -361,7 +455,7 @@ def test_next_week_transfers_chips_already_used(): actual = next_week_transfers( strat, max_total_hit=None, - max_transfers=2, + max_opt_transfers=2, ) expected = [(0, 2, 0), (1, 1, 0), (2, 1, 4)] assert actual == expected @@ -372,7 +466,7 @@ def test_next_week_transfers_play_wildcard(): actual = next_week_transfers( strat, max_total_hit=None, - max_transfers=2, + max_opt_transfers=2, chips={"chips_allowed": [], "chip_to_play": "wildcard"}, ) expected = [("W", 1, 0)] @@ -384,10 +478,32 @@ def test_next_week_transfers_2ft_allow_wildcard(): actual = next_week_transfers( strat, max_total_hit=None, - max_transfers=2, + max_opt_transfers=2, + chips={"chips_allowed": ["wildcard"], "chip_to_play": None}, + max_free_transfers=2, + ) + expected = [(0, 2, 0), (1, 2, 0), (2, 1, 0), ("W", 2, 0)] + assert actual == expected + + +def test_next_week_transfers_5ft_allow_wildcard(): + strat = (5, 0, {"players_in": {}, "chips_played": {}}) + actual = next_week_transfers( + strat, + max_total_hit=None, + max_opt_transfers=5, chips={"chips_allowed": ["wildcard"], "chip_to_play": None}, + max_free_transfers=5, ) - expected = [(0, 2, 0), (1, 2, 0), (2, 1, 0), ("W", 1, 0)] + expected = [ + (0, 5, 0), + (1, 5, 0), + (2, 4, 0), + (3, 3, 0), + (4, 2, 0), + (5, 1, 0), + ("W", 5, 0), + ] assert actual == expected @@ -397,10 +513,11 @@ def test_next_week_transfers_2ft_allow_wildcard_no_unused(): strat, max_total_hit=None, allow_unused_transfers=False, - max_transfers=2, + max_opt_transfers=2, chips={"chips_allowed": ["wildcard"], "chip_to_play": None}, + max_free_transfers=2, ) - expected = [(1, 2, 0), (2, 1, 0), ("W", 1, 0)] + expected = [(1, 2, 0), (2, 1, 0), ("W", 2, 0)] assert actual == expected @@ -409,10 +526,10 @@ def test_next_week_transfers_2ft_play_wildcard(): actual = next_week_transfers( strat, max_total_hit=None, - max_transfers=2, + max_opt_transfers=2, chips={"chips_allowed": [], "chip_to_play": "wildcard"}, ) - expected = [("W", 1, 0)] + expected = [("W", 2, 0)] assert actual == expected @@ -422,8 +539,9 @@ def test_next_week_transfers_2ft_play_bench_boost_no_unused(): strat, max_total_hit=None, allow_unused_transfers=False, - max_transfers=2, + max_opt_transfers=2, chips={"chips_allowed": [], "chip_to_play": "bench_boost"}, + max_free_transfers=2, ) expected = [("B1", 2, 0), ("B2", 1, 0)] assert actual == expected @@ -435,7 +553,7 @@ def test_next_week_transfers_play_triple_captain_max_transfers_3(): strat, max_total_hit=None, allow_unused_transfers=True, - max_transfers=3, + max_opt_transfers=3, chips={"chips_allowed": [], "chip_to_play": "triple_captain"}, ) expected = [("T0", 2, 0), ("T1", 1, 0), ("T2", 1, 4), ("T3", 1, 8)] @@ -444,71 +562,131 @@ def test_next_week_transfers_play_triple_captain_max_transfers_3(): def test_count_expected_outputs_no_chips_no_constraints(): # No constraints or chips, expect 3**num_gameweeks strategies - count = count_expected_outputs( + count, _ = count_expected_outputs( 3, free_transfers=1, max_total_hit=None, allow_unused_transfers=True, next_gw=1, - max_transfers=2, + max_opt_transfers=2, chip_gw_dict={}, ) assert count == 3**3 - # Max hit 0 - # Include: - # (0, 0, 0), (0, 0, 1), (0, 0, 2), (0, 1, 0), (0, 1, 1), (0, 1, 2), - # (0, 2, 0), (0, 2, 1), (1, 0, 0), (1, 0, 1), (1, 0, 2), (1, 1, 0), (1, 1, 1) - # Exclude: - # (0, 2, 2), (1, 1, 2), (1, 2, 0), (1, 2, 1), (1, 2, 2), (2, 0, 0), (2, 0, 1), - # (2, 0, 2), (2, 1, 0), (2, 1, 1), (2, 1, 2), (2, 2, 0), (2, 2, 1), (2, 2, 2) + +def test_count_expected_outputs_no_chips_no_constraints_max5(): + # No constraints or chips, expect 6**num_gameweeks strategies (0 to 5 transfers + # each week) + count, _ = count_expected_outputs( + 3, + free_transfers=1, + max_total_hit=None, + allow_unused_transfers=True, + next_gw=1, + max_opt_transfers=5, + chip_gw_dict={}, + ) + assert count == 6**3 def test_count_expected_outputs_no_chips_zero_hit(): - count = count_expected_outputs( + """ + Max hit 0 + Include: + (0, 0, 0), (0, 0, 1), (0, 0, 2), (0, 1, 0), (0, 1, 1), (0, 1, 2), + (0, 2, 0), (0, 2, 1), (1, 0, 0), (1, 0, 1), (1, 0, 2), (1, 1, 0), (1, 1, 1) + Exclude: + (0, 2, 2), (1, 1, 2), (1, 2, 0), (1, 2, 1), (1, 2, 2), (2, 0, 0), (2, 0, 1), + (2, 0, 2), (2, 1, 0), (2, 1, 1), (2, 1, 2), (2, 2, 0), (2, 2, 1), (2, 2, 2) + """ + count, _ = count_expected_outputs( 3, free_transfers=1, max_total_hit=0, next_gw=1, - max_transfers=2, + max_opt_transfers=2, chip_gw_dict={}, ) assert count == 13 - # Start with 2 FT and no unused - # Include: - # (0, 0, 0), (1, 1, 1), (1, 1, 2), (1, 2, 0), (1, 2, 1), (1, 2, 2), (2, 0, 1), - # (2, 0, 2), (2, 1, 0), (2, 1, 1), (2, 1, 2), (2, 2, 0), (2, 2, 1), (2, 2, 2) - # Exclude: - # (0, 0, 1), (0, 0, 2), (0, 1, 0), (0, 1, 1), (0, 1, 2), (0, 2, 0), (0, 2, 1), - # (0, 2, 2), (1, 0, 0), (1, 0, 1), (1, 0, 2), (1, 1, 0), (2, 0, 0) + +def test_count_expected_outputs_no_chips_zero_hit_max5(): + """ + Max hit 0 + Max 5 transfers + Adds (0, 0, 3) to valid strategies compared to + test_count_expected_outputs_no_chips_zero_hit above + """ + count, _ = count_expected_outputs( + 3, + free_transfers=1, + max_total_hit=0, + next_gw=1, + max_opt_transfers=5, + chip_gw_dict={}, + ) + assert count == 14 def test_count_expected_outputs_no_chips_2ft_no_unused(): - count = count_expected_outputs( + """ + Start with 2 FT and no unused + Include: + (0, 0, 0), (1, 1, 1), (1, 1, 2), (1, 2, 0), (1, 2, 1), (1, 2, 2), (2, 0, 1), + (2, 0, 2), (2, 1, 0), (2, 1, 1), (2, 1, 2), (2, 2, 0), (2, 2, 1), (2, 2, 2) + Exclude: + (0, 0, 1), (0, 0, 2), (0, 1, 0), (0, 1, 1), (0, 1, 2), (0, 2, 0), (0, 2, 1), + (0, 2, 2), (1, 0, 0), (1, 0, 1), (1, 0, 2), (1, 1, 0), (2, 0, 0) + """ + count, _ = count_expected_outputs( 3, free_transfers=2, max_total_hit=None, allow_unused_transfers=False, next_gw=1, - max_transfers=2, + max_opt_transfers=2, + max_free_transfers=2, ) assert count == 14 - # Wildcard, 2 weeks, no constraints - # Strategies: - # (0, 0), (0, 1), (0, 2), (0, 'W'), (1, 0), (1, 1), (1, 2), (1, 'W'), (2, 0), - # (2, 1), (2, 2), (2, 'W'), ('W', 0), ('W', 1), ('W', 2) + +def test_count_expected_outputs_no_chips_5ft_no_unused_max5(): + """ + Start with 5 FT and no unused over 2 weeks + Include: + (0, 0), + (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), + (2, 0), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), + (3, 0), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), + (4, 0), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5), + (5, 0), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5), + """ + count, _ = count_expected_outputs( + 2, + free_transfers=5, + max_total_hit=None, + allow_unused_transfers=False, + next_gw=1, + max_opt_transfers=5, + max_free_transfers=5, + ) + assert count == 30 def test_count_expected_wildcard_allowed_no_constraints(): - count = count_expected_outputs( + """ + Wildcard, 2 weeks, no constraints + Strategies: + (0, 0), (0, 1), (0, 2), (0, 'W'), (1, 0), (1, 1), (1, 2), (1, 'W'), (2, 0), + (2, 1), (2, 2), (2, 'W'), ('W', 0), ('W', 1), ('W', 2) + """ + count, _ = count_expected_outputs( 2, free_transfers=1, max_total_hit=None, allow_unused_transfers=True, next_gw=1, - max_transfers=2, + max_opt_transfers=2, chip_gw_dict={ 1: {"chips_allowed": ["wildcard"]}, 2: {"chips_allowed": ["wildcard"]}, @@ -517,22 +695,23 @@ def test_count_expected_wildcard_allowed_no_constraints(): ) assert count == 15 - # Bench boost, 2 weeks, no constraints - # Strategies: - # (0, 0), (0, 1), (0, 2), (0, 'B0'), (0, 'B1'), (0, 'B2'), (1, 0), (1, 1), (1, 2), - # (1, 'B0'), (1, 'B1'), (1, 'B2'), (2, 0), (2, 1), (2, 2), (2, 'B0'), (2, 'B1'), - # (2, 'B2'), ('B0', 0), ('B0', 1), ('B0', 2), ('B1', 0), ('B1', 1), ('B1', 2), - # ('B2', 0), ('B2', 1), ('B2', 2), - def count_expected_bench_boost_allowed_no_constraints(): - count = count_expected_outputs( + """ + Bench boost, 2 weeks, no constraints + Strategies: + (0, 0), (0, 1), (0, 2), (0, 'B0'), (0, 'B1'), (0, 'B2'), (1, 0), (1, 1), (1, 2), + (1, 'B0'), (1, 'B1'), (1, 'B2'), (2, 0), (2, 1), (2, 2), (2, 'B0'), (2, 'B1'), + (2, 'B2'), ('B0', 0), ('B0', 1), ('B0', 2), ('B1', 0), ('B1', 1), ('B1', 2), + ('B2', 0), ('B2', 1), ('B2', 2), + """ + count, _ = count_expected_outputs( 2, free_transfers=1, max_total_hit=None, allow_unused_transfers=True, next_gw=1, - max_transfers=2, + max_opt_transfers=2, chip_gw_dict={ 1: {"chips_allowed": ["bench_boost"]}, 2: {"chips_allowed": ["bench_boost"]}, @@ -541,19 +720,20 @@ def count_expected_bench_boost_allowed_no_constraints(): ) assert count == 27 - # Force playing wildcard in first week - # Strategies: - # ("W",0), ("W,1), ("W",2) - def count_expected_play_wildcard_no_constraints(): - count = count_expected_outputs( + """ + Force playing wildcard in first week + Strategies: + ("W",0), ("W,1), ("W",2) + """ + count, _ = count_expected_outputs( 2, free_transfers=1, max_total_hit=None, allow_unused_transfers=True, next_gw=1, - max_transfers=2, + max_opt_transfers=2, chip_gw_dict={ 1: {"chip_to_play": "wildcard", "chips_allowed": []}, 2: {"chip_to_play": None, "chips_allowed": []}, @@ -561,19 +741,20 @@ def count_expected_play_wildcard_no_constraints(): ) assert count == 3 - # Force playing free hit in first week, 2FT, don't allow unused - # Strategies: - # (0,0), ("F",1), ("F",2) - def count_expected_play_free_hit_no_unused(): - count = count_expected_outputs( + """ + Force playing free hit in first week, 2FT, don't allow unused + Strategies: + (0,0), ("F",1), ("F",2) + """ + count, _ = count_expected_outputs( 2, free_transfers=2, max_total_hit=None, allow_unused_transfers=False, next_gw=1, - max_transfers=2, + max_opt_transfers=2, chip_gw_dict={ 1: {"chip_to_play": "free_hit", "chips_allowed": []}, 2: {"chip_to_play": None, "chips_allowed": []}, diff --git a/pyproject.toml b/pyproject.toml index bfcad539..8f380b5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "airsenal" -version = "1.8.0" +version = "1.9.0" description = "AI manager for Fantasy Premier League" authors = [ "Angus Williams ",