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, }