diff --git a/scripts/sold-staging-up.py b/scripts/sold-staging-up.py index b90653e..160f688 100755 --- a/scripts/sold-staging-up.py +++ b/scripts/sold-staging-up.py @@ -748,6 +748,78 @@ def verify_blaze_patch(lay: Layout) -> None: # --- seeding ------------------------------------------------------------------------ +# FIFA refuses to kick off unless the squad has 11 starters AND 7 substitutes: +# "your squad must have at least 11 players and 7 subs … currently below the +# minimum number of players (18)". The imported club's squad has only its 11 +# starters, so a freshly installed real club is UNPLAYABLE until the bench is +# filled. The 23 slots are 0..10 pitch, 11..22 bench/reserves, derived from the +# index alone. +CLIENT_MIN_SQUAD = 18 +SQUAD_SLOTS = 23 + + +def fill_bench_to_minimum(conn, club_id: str, squad_id: str, lay: Layout) -> int: + """Top the squad up to the client's minimum with the club's best spare + players, filling empty bench slots from index 11 upward. + + Only EMPTY slots are written, so the starting XI — and any bench the + operator has already chosen — is never touched, and a re-run is a no-op. + Only real PLAYER definitions are eligible: a kit, a manager or a consumable + in a squad slot would be nonsense, and the client would drop it anyway. + """ + taken = { + row[0] + for row in conn.execute( + "SELECT owned_card_id FROM squad_players WHERE squad_id = ?", (squad_id,) + ) + } + used_slots = { + row[0] + for row in conn.execute( + "SELECT position_index FROM squad_players WHERE squad_id = ?", (squad_id,) + ) + } + if len(taken) >= CLIENT_MIN_SQUAD: + return 0 + + with open(safe_path(lay.catalog)) as fh: + catalog = json.load(fh)["cards"] + with open(safe_path(lay.cards)) as fh: + overall = {c["id"]: c.get("overall", 0) for c in json.load(fh)} + + # Best first, then a stable id tiebreak so a re-run picks the same bench. + candidates = [ + (overall.get(card_id, 0), owned_id, card_id) + for owned_id, card_id in conn.execute( + "SELECT id, card_id FROM owned_cards WHERE club_id = ? AND is_loan = 0", + (club_id,), + ) + if owned_id not in taken + and catalog.get(card_id, {}).get("kind", "player") == "player" + ] + candidates.sort(key=lambda c: (-c[0], c[1])) + + free_slots = [i for i in range(11, SQUAD_SLOTS) if i not in used_slots] + need = CLIENT_MIN_SQUAD - len(taken) + if need > len(free_slots) or need > len(candidates): + raise Fatal( + f"cannot reach the client's {CLIENT_MIN_SQUAD}-player minimum: need " + f"{need} more, but {len(free_slots)} free bench slots and " + f"{len(candidates)} eligible spare players" + ) + + rows = [ + (f"sp-bench-{slot}", squad_id, owned_id, slot, 0, 1) + for slot, (_ovr, owned_id, _card) in zip(free_slots, candidates[:need]) + ] + conn.executemany( + "INSERT INTO squad_players (id, squad_id, owned_card_id, position_index, " + "is_captain, is_on_bench) VALUES (?, ?, ?, ?, ?, ?)", + rows, + ) + return len(rows) + + def seed_core_db(lay: Layout, real_club: dict | None) -> None: @@ -847,6 +919,15 @@ def seed_core_db(lay: Layout, real_club: dict | None) -> None: "updated_at) VALUES (?, ?, ?)", (squad_row[0], STAGING_MANAGER["owned_id"], TS), ) + + # Only the REAL club is meant to be played. The 14-item fixture is a + # market test bed with a single spare player, so demanding 18 there + # would abort a bring-up that never needed to kick off. + filled = ( + fill_bench_to_minimum(conn, seller_club, squad_row[0], lay) + if real_club is not None + else 0 + ) finally: conn.close() if real_club is None: @@ -863,6 +944,12 @@ def seed_core_db(lay: Layout, real_club: dict | None) -> None: f"{len(STAGING_KITS)} active kits, the manager " f"{STAGING_MANAGER['label']} and Buyer B ({BUYER_COINS} coins)" ) + if filled: + ok( + f"filled {filled} empty bench slot(s) with the club's best spare " + f"players: FIFA refuses to kick off below {CLIENT_MIN_SQUAD} " + "(11 starters + 7 subs), and the imported squad carries only its XI" + ) def db_summary(lay: Layout) -> str: