Spaces:
Running
Running
| import requests | |
| from typing import Any | |
| _FIXTURE_CACHE = [] | |
| def _norm_id_list(raw) -> list[int]: | |
| if not raw: | |
| return [] | |
| out = [] | |
| for x in raw: | |
| # THE FIX: Safely extract the ID from React's dictionary payload | |
| if isinstance(x, dict): | |
| pid = x.get("ID") or x.get("id", 0) | |
| out.append(int(pid)) | |
| else: | |
| out.append(int(x)) | |
| return out | |
| def _norm_gw_list(raw) -> list[int]: | |
| """Normalise a chip GW list to a list of ints, handling None gracefully.""" | |
| if not raw: | |
| return [] | |
| return [int(g) for g in raw if g is not None] | |
| def prep_solver_data(payload_data: dict): | |
| """ | |
| Translates the React JSON payload into the exact mathematical dictionaries | |
| the MILP engine requires, including chips and advanced constraints. | |
| """ | |
| print("Prepping data for the MILP Engine...") | |
| horizon_gws = [int(g) for g in payload_data["horizon_gws"]] | |
| raw_squad = payload_data["current_squad_ids"] | |
| current_squad_ids = [ | |
| int(pid) | |
| for pid in raw_squad | |
| if isinstance(pid, (int, float)) or str(pid).isdigit() | |
| ] | |
| settings_payload: dict[str, Any] = dict(payload_data.get("settings") or {}) | |
| comp_payload = dict(payload_data.get("comprehensive_settings") or {}) | |
| # Start with an empty dictionary. | |
| # Python no longer guesses defaults. It completely trusts the React payload. | |
| settings = {} | |
| # 1. Apply basic settings | |
| settings.update({k: v for k, v in settings_payload.items() if v not in (None, "")}) | |
| # 2. Apply comprehensive settings (Filters out None AND empty strings from UI) | |
| settings.update({k: v for k, v in comp_payload.items() if v not in (None, "")}) | |
| # --- BAN / LOCK --- | |
| banned_ids = _norm_id_list(settings.get("banned") or settings.get("banned_ids")) | |
| locked_ids = _norm_id_list(settings.get("locked") or settings.get("locked_ids")) | |
| settings["banned_ids"] = banned_ids | |
| settings["locked_ids"] = locked_ids | |
| # --- SCALAR SETTINGS --- | |
| settings["iterations"] = max( | |
| 1, | |
| min(5, int(settings.get("iterations", settings.get("num_iterations", 1)))), | |
| ) | |
| settings["iteration_diff"] = int(settings.get("iteration_diff", 1)) | |
| settings["iteration_criteria"] = settings.get( | |
| "iteration_criteria", "this_gw_transfer_in_out" | |
| ) | |
| settings["hit_cost"] = int(settings.get("hit_cost", 4)) | |
| settings["max_ft"] = int(settings.get("max_ft", 5)) | |
| settings["itb_value"] = float(settings.get("itb_value", 0.08)) | |
| settings["time_limit_sec"] = int(settings.get("time_limit_sec", 3000)) | |
| settings["vice_weight"] = float(settings.get("vice_weight", 0.05)) | |
| settings["max_per_team"] = int(settings.get("max_per_team", 3)) | |
| settings["no_transfer_last_gws"] = int(settings.get("no_transfer_last_gws", 0)) | |
| settings["itb_loss_per_transfer"] = float(settings.get("itb_loss_per_transfer", 0)) | |
| settings["ft_value"] = float( | |
| settings.get("ft_value_base", settings.get("ft_value", 0)) or 0 | |
| ) | |
| settings["ft_use_penalty"] = float(settings.get("ft_use_penalty", 0) or 0) | |
| settings["future_transfer_limit"] = settings.get("future_transfer_limit") | |
| if settings["future_transfer_limit"] is not None: | |
| settings["future_transfer_limit"] = int(settings["future_transfer_limit"]) | |
| settings["hit_limit"] = settings.get("hit_limit") | |
| if settings["hit_limit"] is not None: | |
| settings["hit_limit"] = int(settings["hit_limit"]) | |
| settings["weekly_hit_limit"] = settings.get("weekly_hit_limit") | |
| if settings["weekly_hit_limit"] is not None: | |
| settings["weekly_hit_limit"] = int(settings["weekly_hit_limit"]) | |
| settings["no_transfer_gws"] = _norm_gw_list(settings.get("no_transfer_gws")) | |
| settings["no_transfer_by_position"] = settings.get("no_transfer_by_position") or [] | |
| settings["no_trs_except_wc"] = bool(settings.get("no_trs_except_wc", False)) | |
| settings["max_defenders_per_team"] = int(settings.get("max_defenders_per_team", 3)) | |
| settings["double_defense_pick"] = bool(settings.get("double_defense_pick", False)) | |
| settings["transfer_itb_buffer"] = settings.get("transfer_itb_buffer") | |
| if settings["transfer_itb_buffer"] is not None: | |
| settings["transfer_itb_buffer"] = float(settings["transfer_itb_buffer"]) | |
| settings["no_gk_rotation_after"] = settings.get("no_gk_rotation_after") | |
| settings["no_opposing_play"] = settings.get("no_opposing_play", False) | |
| settings["opposing_play_group"] = settings.get("opposing_play_group", "all") | |
| settings["opposing_play_penalty"] = float( | |
| settings.get("opposing_play_penalty", 0.5) | |
| ) | |
| settings["force_ft_state_lb"] = settings.get("force_ft_state_lb") or [] | |
| settings["force_ft_state_ub"] = settings.get("force_ft_state_ub") or [] | |
| settings["no_chip_gws"] = _norm_gw_list(settings.get("no_chip_gws")) | |
| # Parse gw-specific lock/bans | |
| def norm_temporal_list(raw): | |
| if not raw: | |
| return [] | |
| out = [] | |
| for x in raw: | |
| if isinstance(x, list) and len(x) == 2: | |
| out.append((int(x[0]), int(x[1]))) | |
| elif isinstance(x, dict): | |
| # THE FIX: Extract the ID and strictly bind it to the FIRST gameweek of the horizon! | |
| pid = x.get("ID") or x.get("id", 0) | |
| out.append((int(pid), horizon_gws[0])) | |
| else: | |
| out.append((int(x), horizon_gws[0])) | |
| return out | |
| settings["banned_next_gw"] = norm_temporal_list( | |
| settings.get("ban_this_gw") or settings.get("banned_next_gw") | |
| ) | |
| settings["locked_next_gw"] = norm_temporal_list( | |
| settings.get("lock_this_gw") or settings.get("locked_next_gw") | |
| ) | |
| settings["booked_transfers"] = settings.get("booked_transfers") or [] | |
| settings["only_booked_transfers"] = bool( | |
| settings.get("only_booked_transfers", False) | |
| ) | |
| settings["no_future_transfer"] = bool(settings.get("no_future_transfer", False)) | |
| settings["num_transfers"] = settings.get("num_transfers") | |
| settings["pick_prices"] = settings.get("pick_prices", {}) | |
| # --- CHIP GW LISTS --- | |
| settings["use_wc"] = _norm_gw_list(settings.get("use_wc")) | |
| settings["use_fh"] = _norm_gw_list(settings.get("use_fh")) | |
| settings["use_bb"] = _norm_gw_list(settings.get("use_bb")) | |
| settings["use_tc"] = _norm_gw_list(settings.get("use_tc")) | |
| # --- BENCH WEIGHTS (dict with string keys "0"-"3") --- | |
| raw_bw = settings.get("bench_weights") | |
| if raw_bw and isinstance(raw_bw, dict): | |
| settings["bench_weights"] = {str(k): float(v) for k, v in raw_bw.items()} | |
| else: | |
| settings["bench_weights"] = {"0": 0.03, "1": 0.18, "2": 0.06, "3": 0.002} | |
| # --- FT VALUE LIST (dict with string keys "2"-"5") --- | |
| raw_ftvl = settings.get("ft_value_list") | |
| if raw_ftvl and isinstance(raw_ftvl, dict): | |
| settings["ft_value_list"] = {str(k): float(v) for k, v in raw_ftvl.items()} | |
| else: | |
| settings["ft_value_list"] = {} | |
| # --- DECAY --- | |
| decay = float(settings.get("decay_base", settings.get("decay", 1.0)) or 1.0) | |
| # --- BUILD PLAYER DICTS --- | |
| buy_prices: dict[int, float] = {} | |
| sell_prices: dict[int, float] = {} | |
| positions: dict[int, str] = {} | |
| teams: dict[int, str] = {} | |
| ev_matrix: dict[int, dict[int, float]] = {} | |
| raw_ev_matrix: dict[int, dict[int, float]] = {} | |
| for idx, gw in enumerate(horizon_gws): | |
| decay_factor = decay**idx if decay != 1.0 else 1.0 | |
| for p in payload_data["market_players"]: | |
| pid = int(p["id"]) | |
| if pid not in buy_prices: | |
| positions[pid] = p["pos"] | |
| teams[pid] = p["team"] | |
| buy_prices[pid] = float(p["now_cost"]) | |
| sell_prices[pid] = float(p.get("sell_price", p["now_cost"])) | |
| ev_matrix[pid] = {} | |
| raw_ev_matrix[pid] = {} | |
| raw_ev = p["evs"].get(gw, p["evs"].get(str(gw), 0)) | |
| ev_matrix[pid][gw] = float(raw_ev) * decay_factor | |
| raw_ev_matrix[pid][gw] = float(raw_ev) | |
| fh_sell_price = { | |
| pid: sell_prices[pid] if pid in current_squad_ids else buy_prices[pid] | |
| for pid in buy_prices | |
| } | |
| # --- CROSS-PLAY FIXTURE PREP --- | |
| global _FIXTURE_CACHE | |
| gw_opp_teams = {w: [] for w in horizon_gws} | |
| if settings.get("no_opposing_play"): | |
| if not _FIXTURE_CACHE: | |
| try: | |
| # Same fast cache logic as solver_the_real_one | |
| _FIXTURE_CACHE = requests.get( | |
| "https://fantasy.premierleague.com/api/fixtures/" | |
| ).json() | |
| except: # noqa: E722 | |
| pass | |
| # Map the opponent matchups for the solver | |
| team_mapping = {v: k for k, v in teams.items()} # Reverse lookup | |
| for f in _FIXTURE_CACHE: | |
| w = f.get("event") | |
| if w in horizon_gws: | |
| home_team = team_mapping.get(f.get("team_h")) | |
| away_team = team_mapping.get(f.get("team_a")) | |
| if home_team and away_team: | |
| gw_opp_teams[w].extend( | |
| [(home_team, away_team), (away_team, home_team)] | |
| ) | |
| return { | |
| "players": list(buy_prices.keys()), | |
| "gws": horizon_gws, | |
| "buy_prices": buy_prices, | |
| "sell_prices": sell_prices, | |
| "fh_sell_price": fh_sell_price, | |
| "positions": positions, | |
| "teams": teams, | |
| "ev_matrix": ev_matrix, | |
| "current_squad": current_squad_ids, | |
| "itb": float(payload_data["in_the_bank"]), | |
| "ft": int(payload_data["free_transfers"]), | |
| "settings": settings, | |
| "gw_opp_teams": gw_opp_teams, | |
| } | |