Spaces:
Running
Running
| import pulp | |
| def run_milp_model(data: dict): | |
| print("Igniting Advanced Iterative MILP Engine (Original Solver Parity)...") | |
| players = data["players"] | |
| gws = data["gws"] | |
| buy_prices = data["buy_prices"] | |
| sell_prices = data["sell_prices"] | |
| positions = data["positions"] | |
| teams = data["teams"] | |
| ev_matrix = data["ev_matrix"] | |
| raw_ev_matrix = data.get("raw_ev_matrix", ev_matrix) | |
| current_squad = data["current_squad"] | |
| start_itb = data["itb"] | |
| start_ft = data["ft"] | |
| gw_opp_teams = data.get("gw_opp_teams", {}) | |
| fh_sell_price = data.get("fh_sell_price", sell_prices) | |
| settings = data.get("settings", {}) | |
| # --- STRICT SINGLE SOURCE OF TRUTH --- | |
| # Python no longer guesses. It extracts exactly what React tells it to. | |
| time_limit_sec = int(settings.get("secs", settings.get("time_limit_sec"))) | |
| iterations = int(settings.get("iterations", 1)) | |
| iteration_diff = int(settings.get("iteration_diff", 1)) | |
| iteration_criteria = settings.get("iteration_criteria", "this_gw_transfer_in_out") | |
| banned_ids = settings.get("banned_ids", []) | |
| locked_ids = settings.get("locked_ids", []) | |
| # Directly extracting required scalar values from the React payload | |
| hit_cost = float(settings["hit_cost"]) | |
| max_ft = int(settings["max_ft"]) | |
| itb_value = float(settings["itb_value"]) | |
| vice_weight = float(settings.get("vcap_weight", settings["vice_weight"])) | |
| max_per_team = int(settings["max_per_team"]) | |
| no_transfer_last_gws = int(settings["no_transfer_last_gws"]) | |
| itb_loss_per_transfer = float(settings["itb_loss_per_transfer"]) | |
| ft_use_penalty = float(settings["ft_use_penalty"]) | |
| decay_base = float(settings.get("decay_base", 1.0)) | |
| # --- FT STATE VALUATION --- | |
| raw_ft_value = float(settings["ft_value"]) | |
| use_ftvl_flag = str(settings.get("use_ft_value_list", "false")).lower() in [ | |
| "true", | |
| "1", | |
| ] | |
| raw_ftvl = settings.get("ft_value_list", {}) if use_ftvl_flag else {} | |
| ft_states_list = list(range(max_ft + 1)) | |
| ft_state_value = {} | |
| for s in ft_states_list: | |
| val = float(raw_ftvl.get(str(s), raw_ft_value)) if raw_ftvl else raw_ft_value | |
| ft_state_value[s] = ft_state_value.get(s - 1, 0.0) + val | |
| # --- CHIP SETTINGS --- | |
| use_wc = [int(g) for g in (settings.get("use_wc") or [])] | |
| use_fh = [int(g) for g in (settings.get("use_fh") or [])] | |
| use_bb = [int(g) for g in (settings.get("use_bb") or [])] | |
| use_tc = [int(g) for g in (settings.get("use_tc") or [])] | |
| # Bench weights (Strictly trusts the payload dictionary) | |
| raw_bw = settings["bench_weights"] | |
| bench_weights = {int(k): float(v) for k, v in raw_bw.items()} | |
| gk_bench_w = max(float(bench_weights.get(0, 0.03)), 0.0001) | |
| of_bench_ws = [max(float(bench_weights.get(i, 0.0)), 0.0001) for i in [1, 2, 3]] | |
| avg_of_bench_w = sum(of_bench_ws) / len(of_bench_ws) if of_bench_ws else 0.05 | |
| chip_gws: dict[int, str] = {} | |
| for g in use_wc: | |
| if g in gws: | |
| chip_gws[g] = "wc" | |
| for g in use_fh: | |
| if g in gws: | |
| chip_gws[g] = "fh" | |
| for g in use_bb: | |
| if g in gws: | |
| chip_gws[g] = "bb" | |
| for g in use_tc: | |
| if g in gws: | |
| chip_gws[g] = "tc" | |
| all_gws = [gws[0] - 1] + gws | |
| # --- FREE HIT SQUAD REVERSION --- | |
| effective_prev: dict[int, int] = {} | |
| for w in gws: | |
| prev_w = all_gws[all_gws.index(w) - 1] | |
| if chip_gws.get(prev_w) == "fh": | |
| fh_prev_idx = all_gws.index(prev_w) - 1 | |
| effective_prev[w] = all_gws[fh_prev_idx] | |
| else: | |
| effective_prev[w] = prev_w | |
| prob = pulp.LpProblem("Luigis_Mansion_FPL_Solver", pulp.LpMaximize) | |
| # --- DECISION VARIABLES --- | |
| squad = pulp.LpVariable.dicts("squad", (players, all_gws), cat="Binary") | |
| lineup = pulp.LpVariable.dicts("lineup", (players, gws), cat="Binary") | |
| captain = pulp.LpVariable.dicts("captain", (players, gws), cat="Binary") | |
| vice_captain = pulp.LpVariable.dicts("vice_captain", (players, gws), cat="Binary") | |
| bench = pulp.LpVariable.dicts("bench", (players, gws, [0, 1, 2, 3]), cat="Binary") | |
| transfer_in = pulp.LpVariable.dicts("transfer_in", (players, gws), cat="Binary") | |
| transfer_out = pulp.LpVariable.dicts("transfer_out", (players, gws), cat="Binary") | |
| itb = pulp.LpVariable.dicts("itb", all_gws, lowBound=0, cat="Continuous") | |
| fts = pulp.LpVariable.dicts( | |
| "fts", all_gws, lowBound=1, upBound=max_ft, cat="Integer" | |
| ) | |
| hits = pulp.LpVariable.dicts("hits", gws, lowBound=0, cat="Integer") | |
| ft_below_lb = pulp.LpVariable.dicts("ft_below_lb", gws, cat="Binary") | |
| ft_above_ub = pulp.LpVariable.dicts("ft_above_ub", gws, cat="Binary") | |
| fts_state = pulp.LpVariable.dicts("fts_state", (gws, ft_states_list), cat="Binary") | |
| daux = ( | |
| pulp.LpVariable.dicts("daux", (list(set(teams.values())), gws), cat="Binary") | |
| if settings.get("double_defense_pick") | |
| else None | |
| ) | |
| gw_with_tr = ( | |
| pulp.LpVariable.dicts("gw_with_tr", gws, cat="Binary") | |
| if settings.get("transfer_itb_buffer") is not None | |
| else None | |
| ) | |
| # --- FT STATE LINKING --- | |
| for w in gws: | |
| prev_w = all_gws[all_gws.index(w) - 1] | |
| prob += fts[prev_w] == pulp.lpSum(s * fts_state[w][s] for s in ft_states_list) | |
| prob += pulp.lpSum(fts_state[w][s] for s in ft_states_list) == 1 | |
| gw_ft_value = { | |
| w: pulp.lpSum(ft_state_value[s] * fts_state[w][s] for s in ft_states_list) | |
| for w in gws | |
| } | |
| gw_ft_gain = {} | |
| for i, w in enumerate(gws): | |
| if i == 0: | |
| # MATCH solver_original.py EXACTLY: difference from 0 for the first gw | |
| gw_ft_gain[w] = gw_ft_value[w] - 0.0 | |
| else: | |
| prev_w = gws[i - 1] | |
| gw_ft_gain[w] = gw_ft_value[w] - gw_ft_value[prev_w] | |
| # --- CROSS-PLAY PENALTY LOGIC --- | |
| cp_penalty_expr = {w: 0 for w in gws} | |
| if settings.get("no_opposing_play") and gw_opp_teams: | |
| opp_group = settings.get("opposing_play_group", "all") | |
| pen_val = float(settings.get("opposing_play_penalty", 0.5)) | |
| opp_pos = [ | |
| ("G", "F"), | |
| ("G", "M"), | |
| ("D", "F"), | |
| ("D", "M"), | |
| ("F", "G"), | |
| ("M", "G"), | |
| ("F", "D"), | |
| ("M", "D"), | |
| ] | |
| cp_list = [] | |
| for w in gws: | |
| for p1 in players: | |
| for p2 in players: | |
| # Look up the pre-calculated dictionary! | |
| if p1 != p2 and (teams[p1], teams[p2]) in gw_opp_teams.get(w, []): | |
| if ( | |
| opp_group == "all" | |
| or (positions[p1], positions[p2]) in opp_pos | |
| ): | |
| cp_list.append((p1, p2, w)) | |
| if cp_list: | |
| cp_vars = pulp.LpVariable.dicts("cp", cp_list, cat="Binary") | |
| for p1, p2, w in cp_list: | |
| prob += lineup[p1][w] + lineup[p2][w] <= 1 + cp_vars[(p1, p2, w)] | |
| prob += cp_vars[(p1, p2, w)] <= lineup[p1][w] | |
| prob += cp_vars[(p1, p2, w)] <= lineup[p2][w] | |
| for w in gws: | |
| cp_penalty_expr[w] = pen_val * pulp.lpSum( | |
| cp_vars[(p1, p2, gw)] for (p1, p2, gw) in cp_list if gw == w | |
| ) | |
| # --- OBJECTIVE FUNCTION (Decayed to match original) --- | |
| obj_parts = [] | |
| for i, w in enumerate(gws): | |
| # Apply decay strictly to Hits, ITB, and FT Gains so the solver respects the horizon timing | |
| decay_factor = pow(decay_base, i) | |
| chip = chip_gws.get(w) | |
| for p in players: | |
| # Player EVs are already pre-decayed by solver_engine.py | |
| ev = ev_matrix[p].get(w, 0) | |
| cap_extra = 2.0 if chip == "tc" else 1.0 | |
| obj_parts.append(ev * lineup[p][w]) | |
| obj_parts.append(ev * cap_extra * captain[p][w]) | |
| obj_parts.append(ev * vice_weight * vice_captain[p][w]) | |
| if chip == "bb": | |
| obj_parts.append(ev * 1.0 * (squad[p][w] - lineup[p][w])) | |
| else: | |
| obj_parts.append(ev * gk_bench_w * bench[p][w][0]) | |
| if len(of_bench_ws) >= 3: | |
| obj_parts.append(ev * of_bench_ws[0] * bench[p][w][1]) | |
| obj_parts.append(ev * of_bench_ws[1] * bench[p][w][2]) | |
| obj_parts.append(ev * of_bench_ws[2] * bench[p][w][3]) | |
| if chip != "fh": | |
| obj_parts.append(gw_ft_gain[w] * decay_factor) | |
| obj_parts.append(itb[w] * itb_value * decay_factor) | |
| obj_parts.append(-hits[w] * hit_cost * decay_factor) | |
| # FT-use penalty ONLY applied outside of Wildcard/Free Hit | |
| if ft_use_penalty != 0 and chip not in ("wc", "fh"): | |
| for p in players: | |
| obj_parts.append(-transfer_in[p][w] * ft_use_penalty * decay_factor) | |
| if cp_penalty_expr[w] != 0: | |
| obj_parts.append(-cp_penalty_expr[w] * decay_factor) | |
| prob += pulp.lpSum(obj_parts), "Total_EV_Objective" | |
| # --- ADVANCED HORIZON-LEVEL CONSTRAINTS --- | |
| if settings.get("hit_limit") is not None: | |
| prob += pulp.lpSum(hits[w] for w in gws) <= settings["hit_limit"] | |
| if settings.get("future_transfer_limit") is not None and len(gws) > 1: | |
| prob += ( | |
| pulp.lpSum( | |
| transfer_in[p][w] | |
| for p in players | |
| for w in gws[1:] | |
| if chip_gws.get(w) not in ("wc", "fh") | |
| ) | |
| <= settings["future_transfer_limit"] | |
| ) | |
| if settings.get("no_gk_rotation_after") is not None: | |
| target_gw = int(settings["no_gk_rotation_after"]) | |
| if target_gw in gws: | |
| for p in players: | |
| if positions[p] == "G": | |
| for w in gws: | |
| if w > target_gw and chip_gws.get(w) != "fh": | |
| prob += lineup[p][w] >= lineup[p][target_gw] | |
| # --- INITIAL CONDITIONS --- | |
| for p in players: | |
| prob += squad[p][all_gws[0]] == (1 if p in current_squad else 0) | |
| prob += itb[all_gws[0]] == start_itb | |
| prob += fts[all_gws[0]] == start_ft | |
| # --- USER BANS & LOCKS --- | |
| for p in players: | |
| if p in banned_ids: | |
| for w in gws: | |
| prob += squad[p][w] == 0 | |
| if p in locked_ids: | |
| for w in gws: | |
| prob += squad[p][w] == 1 | |
| # --- TARGETED CHIP/PRICE CONSTRAINTS --- | |
| for gw in settings.get("no_chip_gws", []): | |
| if gw in chip_gws: | |
| raise Exception( | |
| f"Contradiction: user tried to play chip in GW {gw} but also assigned it to no_chip_gws!" | |
| ) | |
| if settings.get("pick_prices"): | |
| for pos, val in settings["pick_prices"].items(): | |
| if not val or pos not in ("G", "D", "M", "F"): | |
| continue | |
| price_pts = [float(x) for x in val.split(",")] | |
| value_dict = {i: price_pts.count(i) for i in set(price_pts)} | |
| for key, count in value_dict.items(): | |
| target_players = [ | |
| p | |
| for p in players | |
| if positions[p] == pos | |
| and buy_prices[p] >= key - 0.2 | |
| and buy_prices[p] <= key + 0.2 | |
| ] | |
| for w in gws: | |
| prob += pulp.lpSum(squad[p][w] for p in target_players) >= count | |
| # --- PER-GW CONSTRAINTS --- | |
| for w in gws: | |
| prev_w = all_gws[all_gws.index(w) - 1] | |
| eff_prev = effective_prev[w] | |
| chip = chip_gws.get(w) | |
| prob += pulp.lpSum(squad[p][w] for p in players) == 15 | |
| prob += pulp.lpSum(squad[p][w] for p in players if positions[p] == "G") == 2 | |
| prob += pulp.lpSum(squad[p][w] for p in players if positions[p] == "D") == 5 | |
| prob += pulp.lpSum(squad[p][w] for p in players if positions[p] == "M") == 5 | |
| prob += pulp.lpSum(squad[p][w] for p in players if positions[p] == "F") == 3 | |
| prob += pulp.lpSum(lineup[p][w] for p in players) == 11 | |
| prob += pulp.lpSum(lineup[p][w] for p in players if positions[p] == "G") == 1 | |
| prob += pulp.lpSum(lineup[p][w] for p in players if positions[p] == "D") >= 3 | |
| prob += pulp.lpSum(lineup[p][w] for p in players if positions[p] == "D") <= 5 | |
| prob += pulp.lpSum(lineup[p][w] for p in players if positions[p] == "M") >= 2 | |
| prob += pulp.lpSum(lineup[p][w] for p in players if positions[p] == "M") <= 5 | |
| prob += pulp.lpSum(lineup[p][w] for p in players if positions[p] == "F") >= 1 | |
| prob += pulp.lpSum(lineup[p][w] for p in players if positions[p] == "F") <= 3 | |
| prob += pulp.lpSum(captain[p][w] for p in players) == 1 | |
| prob += pulp.lpSum(vice_captain[p][w] for p in players) == 1 | |
| is_bb = 1 if chip == "bb" else 0 | |
| prob += ( | |
| pulp.lpSum(bench[p][w][0] for p in players if positions[p] == "G") | |
| == 1 - is_bb | |
| ) | |
| for o in [1, 2, 3]: | |
| prob += pulp.lpSum(bench[p][w][o] for p in players) == 1 - is_bb | |
| for p in players: | |
| prob += lineup[p][w] <= squad[p][w] | |
| prob += ( | |
| lineup[p][w] + pulp.lpSum(bench[p][w][o] for o in [0, 1, 2, 3]) | |
| <= squad[p][w] | |
| ) | |
| prob += captain[p][w] <= lineup[p][w] | |
| prob += vice_captain[p][w] <= lineup[p][w] | |
| prob += captain[p][w] + vice_captain[p][w] <= 1 | |
| prob += ( | |
| squad[p][w] | |
| == squad[p][eff_prev] + transfer_in[p][w] - transfer_out[p][w] | |
| ) | |
| for t in set(teams.values()): | |
| prob += ( | |
| pulp.lpSum(squad[p][w] for p in players if teams[p] == t) | |
| <= max_per_team | |
| ) | |
| if gws.index(w) >= len(gws) - no_transfer_last_gws and chip not in ("wc", "fh"): | |
| prob += pulp.lpSum(transfer_in[p][w] for p in players) == 0 | |
| if chip == "fh": | |
| # THE FIX: Decouple the Free Hit budget from the continuous bank! | |
| # The new squad cost must simply be <= total wealth. | |
| prob += itb[eff_prev] + pulp.lpSum( | |
| fh_sell_price[p] * squad[p][eff_prev] for p in players | |
| ) >= pulp.lpSum(fh_sell_price[p] * squad[p][w] for p in players) | |
| # The real bank is completely frozen and carries over untouched. | |
| prob += itb[w] == itb[eff_prev] | |
| else: | |
| prob += ( | |
| itb[eff_prev] | |
| + pulp.lpSum(transfer_out[p][w] * sell_prices[p] for p in players) | |
| - pulp.lpSum(transfer_in[p][w] * buy_prices[p] for p in players) | |
| - pulp.lpSum(transfer_in[p][w] * itb_loss_per_transfer for p in players) | |
| == itb[w] | |
| ) | |
| gw_transfers = pulp.lpSum(transfer_in[p][w] for p in players) | |
| # FT / Hits Engine | |
| if chip in ("wc", "fh"): | |
| prob += hits[w] == 0 | |
| raw_gw_ft = fts[prev_w] | |
| else: | |
| prob += hits[w] >= gw_transfers - fts[prev_w] | |
| raw_gw_ft = fts[prev_w] - gw_transfers + 1 | |
| m = 25 | |
| prob += raw_gw_ft <= 0 + m * (1 - ft_below_lb[w]) | |
| prob += raw_gw_ft >= 1 - m * ft_below_lb[w] | |
| prob += raw_gw_ft >= (max_ft + 1) - m * (1 - ft_above_ub[w]) | |
| prob += raw_gw_ft <= max_ft + m * ft_above_ub[w] | |
| prob += fts[w] <= 1 + m * (1 - ft_below_lb[w]) | |
| prob += fts[w] >= 1 - m * (1 - ft_below_lb[w]) | |
| prob += fts[w] <= max_ft + m * (1 - ft_above_ub[w]) | |
| prob += fts[w] >= max_ft - m * (1 - ft_above_ub[w]) | |
| prob += fts[w] - raw_gw_ft <= m * ft_below_lb[w] + m * ft_above_ub[w] | |
| prob += raw_gw_ft - fts[w] <= m * ft_below_lb[w] + m * ft_above_ub[w] | |
| # --- ADVANCED GW-LEVEL CONSTRAINTS --- | |
| for pid, tgw in settings.get("banned_next_gw", []): | |
| if pid in players and (tgw == w or tgw is None): | |
| prob += squad[pid][w] == 0 | |
| for pid, tgw in settings.get("locked_next_gw", []): | |
| if pid in players and (tgw == w or tgw is None): | |
| prob += squad[pid][w] == 1 | |
| for bt in settings.get("booked_transfers", []): | |
| if bt.get("gw") == w: | |
| if bt.get("transfer_in") in players: | |
| prob += transfer_in[bt["transfer_in"]][w] == 1 | |
| if bt.get("transfer_out") in players: | |
| prob += transfer_out[bt["transfer_out"]][w] == 1 | |
| if settings.get("only_booked_transfers") and w == gws[0]: | |
| forced_ins = [ | |
| bt["transfer_in"] | |
| for bt in settings.get("booked_transfers", []) | |
| if bt.get("gw") == w and "transfer_in" in bt | |
| ] | |
| forced_outs = [ | |
| bt["transfer_out"] | |
| for bt in settings.get("booked_transfers", []) | |
| if bt.get("gw") == w and "transfer_out" in bt | |
| ] | |
| for p in players: | |
| prob += transfer_in[p][w] == (1 if p in forced_ins else 0) | |
| prob += transfer_out[p][w] == (1 if p in forced_outs else 0) | |
| if settings.get("num_transfers") is not None and w == gws[0]: | |
| prob += gw_transfers == int(settings["num_transfers"]) | |
| if ( | |
| settings.get("no_future_transfer") | |
| and w > gws[0] | |
| and chip not in ("wc", "fh") | |
| ): | |
| prob += gw_transfers == 0 | |
| if settings.get("weekly_hit_limit") is not None: | |
| prob += hits[w] <= int(settings["weekly_hit_limit"]) | |
| if w in settings.get("no_transfer_gws", []): | |
| prob += gw_transfers == 0 | |
| if ( | |
| settings.get("no_transfer_by_position") | |
| and w > gws[0] | |
| and chip not in ("wc", "fh") | |
| ): | |
| prob += ( | |
| pulp.lpSum( | |
| transfer_in[p][w] | |
| for p in players | |
| if positions[p] in settings["no_transfer_by_position"] | |
| ) | |
| == 0 | |
| ) | |
| mdpt = settings.get("max_defenders_per_team", 3) | |
| if mdpt < 3: | |
| for t in set(teams.values()): | |
| prob += ( | |
| pulp.lpSum( | |
| squad[p][w] | |
| for p in players | |
| if teams[p] == t and positions[p] in ("G", "D") | |
| ) | |
| <= mdpt | |
| ) | |
| if settings.get("double_defense_pick") and daux is not None: | |
| for t in set(teams.values()): | |
| team_defs = pulp.lpSum( | |
| lineup[p][w] | |
| for p in players | |
| if teams[p] == t and positions[p] in ("G", "D") | |
| ) | |
| prob += team_defs <= 3 * daux[t][w] | |
| prob += team_defs >= 2 - 3 * (1 - daux[t][w]) | |
| if settings.get("transfer_itb_buffer") is not None and gw_with_tr is not None: | |
| prob += 15 * gw_with_tr[w] >= gw_transfers | |
| prob += gw_with_tr[w] <= gw_transfers | |
| prob += itb[w] >= settings["transfer_itb_buffer"] * gw_with_tr[w] | |
| for gw, ft_min in settings.get("force_ft_state_lb", []): | |
| if w == gw: | |
| prob += fts[w] >= ft_min | |
| for gw, ft_max in settings.get("force_ft_state_ub", []): | |
| if w == gw: | |
| prob += fts[w] <= ft_max | |
| if settings.get("no_trs_except_wc") and chip != "wc": | |
| prob += gw_transfers == 0 | |
| # --- ITERATION LOOP --- | |
| solutions = [] | |
| for i in range(iterations): | |
| print(f"Solving Iteration {i + 1}...") | |
| # THE FIX: Boot up the HiGHS engine instead of CBC | |
| try: | |
| # THE FIX: Force absolute perfection (0.0 gap) during Free Hits to stop lazy benching! | |
| gap = 0.0 | |
| solver = pulp.getSolver( | |
| "HiGHS", | |
| msg=False, | |
| timeLimit=time_limit_sec, | |
| options=["presolve=on", f"mip_rel_gap={gap}"], | |
| ) | |
| except pulp.PulpSolverError: | |
| print("HiGHS not found! Falling back to CBC...") | |
| solver = pulp.PULP_CBC_CMD(msg=False, timeLimit=time_limit_sec) | |
| prob.solve(solver) | |
| if pulp.LpStatus[prob.status] != "Optimal": | |
| if i == 0: | |
| raise Exception( | |
| "Solver could not find an optimal solution! Check budget, bans, or locks." | |
| ) | |
| else: | |
| print(f"No more valid alternate paths found after {i} iterations.") | |
| break | |
| plan = [] | |
| pure_ev = 0.0 | |
| active_transfers = [] | |
| objective_score = None | |
| try: | |
| objective_score = round(float(pulp.value(prob.objective)), 4) | |
| except (TypeError, ValueError): | |
| objective_score = None | |
| for w in gws: | |
| prev_w = all_gws[all_gws.index(w) - 1] | |
| chip = chip_gws.get(w) | |
| # THE FIX 1: Safely extract binary variables using > 0.5 to prevent MILP floating-point drops | |
| t_in_raw = [ | |
| p | |
| for p in players | |
| if transfer_in[p][w].varValue is not None | |
| and transfer_in[p][w].varValue > 0.5 | |
| ] | |
| t_out_raw = [ | |
| p | |
| for p in players | |
| if transfer_out[p][w].varValue is not None | |
| and transfer_out[p][w].varValue > 0.5 | |
| ] | |
| t_in = [p for p in t_in_raw if p not in t_out_raw] | |
| t_out = [p for p in t_out_raw if p not in t_in_raw] | |
| _pos_order = {"G": 0, "D": 1, "M": 2, "F": 3} | |
| t_in.sort(key=lambda x: _pos_order.get(positions.get(x, "M"), 2)) | |
| t_out.sort(key=lambda x: _pos_order.get(positions.get(x, "M"), 2)) | |
| gw_lineup = [ | |
| p | |
| for p in players | |
| if lineup[p][w].varValue is not None and lineup[p][w].varValue > 0.5 | |
| ] | |
| gw_bench_raw = [ | |
| p | |
| for p in players | |
| if squad[p][w].varValue is not None | |
| and squad[p][w].varValue > 0.5 | |
| and (lineup[p][w].varValue is None or lineup[p][w].varValue < 0.5) | |
| ] | |
| # Safe extractions with fallbacks so indexing never crashes the loop | |
| gw_cap_list = [ | |
| p | |
| for p in players | |
| if captain[p][w].varValue is not None and captain[p][w].varValue > 0.5 | |
| ] | |
| gw_cap = ( | |
| gw_cap_list[0] | |
| if gw_cap_list | |
| else (gw_lineup[0] if gw_lineup else players[0]) | |
| ) | |
| gw_vice_list = [ | |
| p | |
| for p in players | |
| if vice_captain[p][w].varValue is not None | |
| and vice_captain[p][w].varValue > 0.5 | |
| ] | |
| gw_vice = ( | |
| gw_vice_list[0] | |
| if gw_vice_list | |
| else (gw_lineup[-1] if gw_lineup else players[-1]) | |
| ) | |
| gw_hits = int(round(hits[w].varValue or 0)) | |
| ft_at_start = int(round(fts[prev_w].varValue or 0)) | |
| transfers_made = len(t_in) | |
| fts_free_used = min(transfers_made, ft_at_start) | |
| gks_b = [p for p in gw_bench_raw if positions.get(p, "M") == "G"] | |
| rest_b = sorted( | |
| [p for p in gw_bench_raw if positions.get(p, "M") != "G"], | |
| key=lambda x: raw_ev_matrix[x].get(w, 0), | |
| reverse=True, | |
| ) | |
| gw_bench_sorted = (gks_b + rest_b) if gks_b else rest_b | |
| cap_mult = 3 if chip == "tc" else 2 | |
| gw_pure_ev = sum( | |
| raw_ev_matrix[p].get(w, 0) * (cap_mult if p == gw_cap else 1) | |
| for p in gw_lineup | |
| ) | |
| if chip == "bb": | |
| gw_pure_ev += sum(raw_ev_matrix[p].get(w, 0) for p in gw_bench_sorted) | |
| else: | |
| # THE FIX 2: Stop hardcoding! Use the dynamic weights extracted from React at the top of the file! | |
| of_idx = 0 | |
| for p in gw_bench_sorted: | |
| if positions.get(p, "M") == "G": | |
| # Uses the gk_bench_w parsed at line 46 | |
| gw_pure_ev += raw_ev_matrix[p].get(w, 0) * gk_bench_w | |
| else: | |
| # Uses the of_bench_ws array parsed at line 47 | |
| bw = ( | |
| of_bench_ws[of_idx] | |
| if of_idx < len(of_bench_ws) | |
| else avg_of_bench_w | |
| ) | |
| gw_pure_ev += raw_ev_matrix[p].get(w, 0) * bw | |
| of_idx += 1 | |
| gw_pure_ev -= gw_hits * hit_cost | |
| pure_ev += gw_pure_ev | |
| # RESTORED: Track active transfers so the solver knows what to ban in the next iteration! | |
| if "this_gw" in iteration_criteria and w == gws[0]: | |
| if "in" in iteration_criteria: | |
| for p in t_in: | |
| active_transfers.append(transfer_in[p][w]) | |
| if "out" in iteration_criteria: | |
| for p in t_out: | |
| active_transfers.append(transfer_out[p][w]) | |
| elif "this_gw" not in iteration_criteria: | |
| for p in t_in: | |
| active_transfers.append(transfer_in[p][w]) | |
| for p in t_out: | |
| active_transfers.append(transfer_out[p][w]) | |
| plan.append( | |
| { | |
| "gw": w, | |
| "chip": chip, | |
| "transfers_in": t_in, | |
| "transfers_out": t_out, | |
| "lineup": gw_lineup, | |
| "bench": gw_bench_sorted, | |
| "captain": gw_cap, | |
| "vice_captain": gw_vice, | |
| "hits": gw_hits, | |
| "itb": round(itb[w].varValue, 1), | |
| "fts_remaining": int(fts[w].varValue), | |
| "ft_at_start": ft_at_start, | |
| "transfers_made": transfers_made, | |
| "fts_free_used": fts_free_used, | |
| } | |
| ) | |
| solutions.append( | |
| { | |
| "id": i + 1, | |
| "ev": round(pure_ev, 2), | |
| "objective_score": objective_score, | |
| "plan": plan, | |
| "chips_used": chip_gws, | |
| "horizon_gws": gws, | |
| } | |
| ) | |
| # THE FIX: Restored original parity logic, and fixed the 0-transfer loop properly! | |
| if len(active_transfers) > 0: | |
| prob += ( | |
| pulp.lpSum(active_transfers) <= len(active_transfers) - iteration_diff | |
| ) | |
| else: | |
| if "this_gw" in iteration_criteria: | |
| prob += pulp.lpSum(transfer_in[p][gws[0]] for p in players) >= 1 | |
| else: | |
| prob += pulp.lpSum(transfer_in[p][w] for p in players for w in gws) >= 1 | |
| def sort_key(s): | |
| obj = s.get("objective_score") | |
| ev = s.get("ev") or 0 | |
| if obj is None: | |
| return (-float(ev), -float(ev)) | |
| return (-float(obj), -float(ev)) | |
| solutions.sort(key=sort_key) | |
| return {"status": "success", "solutions": solutions} | |