feat(fifa17): curated dev content pack generator + Core dev seed (submodule 36abd4b)
This commit is contained in:
+108
-10
@@ -118,6 +118,85 @@ def build(recon):
|
||||
definitions.sort(key=lambda d: d["id"])
|
||||
return catalog, definitions, skipped, len(pool)
|
||||
|
||||
# ── Curated development subset ───────────────────────────────────────────────
|
||||
# The dev pack is a small, deterministic slice of the full base-card set, chosen
|
||||
# to exercise the retail /club UI (quality/position/nation/league/team filters +
|
||||
# pagination) without loading all ~17.5K definitions. Selection is reproducible
|
||||
# from the sorted source; no asset is hand-picked.
|
||||
MY_SQUAD_PAGE = 11 # evidence: the client requests count=11 per page
|
||||
DEV_PRIMARY_GOLD_CAP = 18 # > one page → quality=gold and league=<primary> span 2+ pages
|
||||
DEV_SECONDARY_GOLD = 4
|
||||
DEV_SILVER = 6
|
||||
DEV_BRONZE = 4
|
||||
|
||||
|
||||
def select_dev(definitions):
|
||||
"""Deterministically curate a dev slice from the full definition set.
|
||||
|
||||
Criteria (all reproducible): the two leagues with the most Gold cards are the
|
||||
primary/secondary leagues (tie-break by name); take the primary league's top
|
||||
Gold cards (capped), guaranteeing >1 page for both `quality=gold` and
|
||||
`league=<primary>`; ensure a GK is present; add the secondary league's top
|
||||
Golds (multiple leagues); add the top Silvers and Bronzes (quality spread).
|
||||
"""
|
||||
by_league = {}
|
||||
for d in definitions:
|
||||
by_league.setdefault(d["league"], []).append(d)
|
||||
golds = lambda ds: [d for d in ds if d["overall"] >= 75]
|
||||
ranked = sorted(by_league, key=lambda L: (-len(golds(by_league[L])), L))
|
||||
primary, secondary = ranked[0], ranked[1]
|
||||
key = lambda d: (-d["overall"], d["id"])
|
||||
chosen = {}
|
||||
for d in sorted(golds(by_league[primary]), key=key)[:DEV_PRIMARY_GOLD_CAP]:
|
||||
chosen[d["id"]] = d
|
||||
if not any(d["position"] == "GK" for d in chosen.values()):
|
||||
gks = sorted((d for d in definitions if d["position"] == "GK" and d["overall"] >= 75), key=key)
|
||||
if gks:
|
||||
chosen[gks[0]["id"]] = gks[0]
|
||||
for d in sorted(golds(by_league[secondary]), key=key)[:DEV_SECONDARY_GOLD]:
|
||||
chosen[d["id"]] = d
|
||||
for d in sorted((d for d in definitions if 65 <= d["overall"] < 75), key=key)[:DEV_SILVER]:
|
||||
chosen[d["id"]] = d
|
||||
for d in sorted((d for d in definitions if d["overall"] < 65), key=key)[:DEV_BRONZE]:
|
||||
chosen[d["id"]] = d
|
||||
return sorted(chosen.values(), key=lambda d: d["id"]), primary
|
||||
|
||||
|
||||
def dev_coverage(dev):
|
||||
"""Coverage stats + the self-check gate the dev pack must pass."""
|
||||
q = lambda lo, hi=256: sum(1 for d in dev if lo <= d["overall"] < hi)
|
||||
positions = sorted({d["position"] for d in dev})
|
||||
teams = {}
|
||||
for d in dev:
|
||||
teams[d["club"]] = teams.get(d["club"], 0) + 1
|
||||
cov = {
|
||||
"count": len(dev),
|
||||
"gold": q(75),
|
||||
"silver": q(65, 75),
|
||||
"bronze": q(0, 65),
|
||||
"positions": positions,
|
||||
"has_gk": "GK" in positions,
|
||||
"gold_st": sum(1 for d in dev if d["position"] == "ST" and d["overall"] >= 75),
|
||||
"leagues": sorted({d["league"] for d in dev}),
|
||||
"nations": sorted({d["nation"] for d in dev}),
|
||||
"max_same_team": max(teams.values()) if teams else 0,
|
||||
"pages_gold": (q(75) + MY_SQUAD_PAGE - 1) // MY_SQUAD_PAGE,
|
||||
}
|
||||
# The pack is only useful if it can exercise the filters + pagination.
|
||||
checks = {
|
||||
"gold_over_one_page": cov["gold"] > MY_SQUAD_PAGE,
|
||||
"has_gk": cov["has_gk"],
|
||||
"has_st": any(d["position"] == "ST" for d in dev),
|
||||
"multi_league": len(cov["leagues"]) >= 2,
|
||||
"multi_nation": len(cov["nations"]) >= 2,
|
||||
"has_silver": cov["silver"] >= 1,
|
||||
"has_bronze": cov["bronze"] >= 1,
|
||||
"same_team_group": cov["max_same_team"] >= 2,
|
||||
}
|
||||
cov["checks"] = checks
|
||||
cov["ok"] = all(checks.values())
|
||||
return cov
|
||||
|
||||
|
||||
def dumps(obj):
|
||||
return json.dumps(obj, indent=2, ensure_ascii=False, sort_keys=False) + "\n"
|
||||
@@ -127,13 +206,18 @@ def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--recon", default=os.path.normpath(RECON))
|
||||
ap.add_argument("--catalog-out", required=True)
|
||||
ap.add_argument("--defs-out", help="optional semantic CardDefinition JSON (Core content)")
|
||||
ap.add_argument("--defs-out", help="optional semantic CardDefinition JSON (full Core content)")
|
||||
ap.add_argument("--dev-out", help="curated game-scoped dev CardDefinition pack")
|
||||
ap.add_argument("--check", action="store_true", help="fail if committed output would change")
|
||||
args = ap.parse_args()
|
||||
|
||||
catalog, definitions, skipped, pool_rows = build(args.recon)
|
||||
catalog_bytes = dumps(catalog)
|
||||
defs_bytes = dumps(definitions)
|
||||
dev, primary = select_dev(definitions)
|
||||
dev_bytes = dumps(dev)
|
||||
cov = dev_coverage(dev)
|
||||
cov["primary_league"] = primary
|
||||
|
||||
reasons = {}
|
||||
for _, why in skipped:
|
||||
@@ -145,26 +229,40 @@ def main():
|
||||
"skipped_definitions": len(skipped),
|
||||
"skip_reasons": reasons,
|
||||
"catalog_sha256": hashlib.sha256(catalog_bytes.encode()).hexdigest()[:16],
|
||||
"dev_coverage": cov,
|
||||
}
|
||||
print(json.dumps(report, indent=2), file=sys.stderr)
|
||||
|
||||
# The dev pack is worthless if it cannot exercise the filters + pagination.
|
||||
if args.dev_out and not cov["ok"]:
|
||||
print("DEV PACK FAILED COVERAGE GATE: %s" % cov["checks"], file=sys.stderr)
|
||||
sys.exit(2)
|
||||
# Every dev definition must have a catalog (FIFA render) identity.
|
||||
missing = [d["id"] for d in dev if d["id"] not in catalog["cards"]]
|
||||
if missing:
|
||||
print("DEV DEFS WITHOUT CATALOG IDENTITY: %s" % missing, file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
outputs = [(args.catalog_out, catalog_bytes)]
|
||||
if args.defs_out:
|
||||
outputs.append((args.defs_out, defs_bytes))
|
||||
if args.dev_out:
|
||||
outputs.append((args.dev_out, dev_bytes))
|
||||
|
||||
if args.check:
|
||||
drift = False
|
||||
for path, want in [(args.catalog_out, catalog_bytes)] + (
|
||||
[(args.defs_out, defs_bytes)] if args.defs_out else []
|
||||
):
|
||||
for path, want in outputs:
|
||||
have = open(path, encoding="utf-8").read() if os.path.exists(path) else None
|
||||
if have != want:
|
||||
print("DRIFT: %s is stale (regenerate)" % path, file=sys.stderr)
|
||||
drift = True
|
||||
sys.exit(1 if drift else 0)
|
||||
|
||||
with open(args.catalog_out, "w", encoding="utf-8") as f:
|
||||
f.write(catalog_bytes)
|
||||
if args.defs_out:
|
||||
with open(args.defs_out, "w", encoding="utf-8") as f:
|
||||
f.write(defs_bytes)
|
||||
print("wrote %s (%d cards)" % (args.catalog_out, len(catalog["cards"])), file=sys.stderr)
|
||||
for path, data in outputs:
|
||||
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(data)
|
||||
print("wrote catalog=%d dev=%d cards" % (len(catalog["cards"]), len(dev)), file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user