feat(fifa17): deterministic base-card seed generator + full identity catalog
scripts/seed_fifa17_cards.py: deterministic pipeline from committed FIFA17 data (pool.json + roster.json + leagues/nations/teams tables) -> the card-definition identity catalog. CardDefinitionId is opaque + deterministic (fifa17_<asset>), version 0 (base cards only; resource_id == asset_id). --check mode diffs against committed output (drift-detection mutation-proven). Provenance embedded. Generated openfut-adapter-fifa17/data/fifa17-card-identities.json: all 17,563 base assets. Semantic definition coverage (to /tmp, not committed here): 17,547 resolvable; 16 skipped for missing roster name (reported, never fabricated). Adapter loads the committed catalog (test: 17,563 entries, Ronaldo fifa17_20801 -> asset 20801 v0). Phase commit 3/5. NOT owned inventory: this is 'which cards exist', not 'which the user owns'. Core content seeding + dev-owned set next.
This commit is contained in:
Executable
+171
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Deterministic FIFA 17 base-card seed generator.
|
||||
|
||||
Reads the committed, evidenced FIFA 17 data and produces:
|
||||
|
||||
1. the FIFA 17 card-definition *identity catalog*
|
||||
(openfut-adapter-fifa17 data): CardDefinitionId -> {asset_id, version=0}.
|
||||
`resource_id == asset_id` for version 0.
|
||||
2. (optional) semantic base CardDefinitions for OpenFUT Core content.
|
||||
|
||||
Base cards only (version 0). No specials — pool.json proves only base identities.
|
||||
CardDefinitionId is opaque + deterministic: `fifa17_<asset_id>` (per-game
|
||||
namespace). Core treats it as opaque; the FIFA meaning stays in the catalog.
|
||||
|
||||
Sources (all committed, non-sensitive):
|
||||
fifa17-recon/data/pool.json real player assets (id, rating, pos, nation,
|
||||
league, team, attrs[pace,sho,pas,dri,def,phy], gk)
|
||||
fifa17-recon/data/roster.json names (id -> first/last/common)
|
||||
fifa17-recon/data/tables/{leagues,nations,teams}.json entity id -> name
|
||||
|
||||
Determinism: output is sorted by asset_id and JSON-stable; running twice on the
|
||||
same source yields identical bytes. `--check` regenerates and diffs against the
|
||||
committed files, exiting non-zero on drift.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
GENERATOR_VERSION = 1
|
||||
SCHEMA_VERSION = 1
|
||||
GAME = "fifa17"
|
||||
RECON = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "fifa17-recon")
|
||||
|
||||
|
||||
def _load(path):
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def _table_map(path, id_key, name_key):
|
||||
doc = _load(path)
|
||||
return {row[id_key]: row[name_key] for row in doc["rows"]}
|
||||
|
||||
|
||||
def tier(overall):
|
||||
return "gold" if overall >= 75 else "silver" if overall >= 65 else "bronze"
|
||||
|
||||
|
||||
def build(recon):
|
||||
pool = _load(os.path.join(recon, "data", "pool.json"))
|
||||
roster = {r["id"]: r for r in _load(os.path.join(recon, "data", "roster.json"))}
|
||||
leagues = _table_map(os.path.join(recon, "data", "tables", "leagues.json"), "leagueid", "leaguename")
|
||||
nations = _table_map(os.path.join(recon, "data", "tables", "nations.json"), "nationid", "nationname")
|
||||
teams = _table_map(os.path.join(recon, "data", "tables", "teams.json"), "teamid", "teamname")
|
||||
|
||||
catalog_cards = {}
|
||||
definitions = []
|
||||
skipped = []
|
||||
seen_assets = set()
|
||||
|
||||
for p in sorted(pool, key=lambda x: x["id"]):
|
||||
asset = p["id"]
|
||||
if asset in seen_assets:
|
||||
skipped.append((asset, "duplicate_asset_id"))
|
||||
continue
|
||||
seen_assets.add(asset)
|
||||
if asset > 0x00FFFFFF:
|
||||
skipped.append((asset, "asset_id_exceeds_24_bits"))
|
||||
continue
|
||||
card_id = "fifa17_%d" % asset
|
||||
# Catalog entry needs only the asset id (identity); always emitted.
|
||||
catalog_cards[card_id] = {"asset_id": asset, "version": 0}
|
||||
|
||||
# Semantic definition needs a name + resolvable entities; skip+report if not.
|
||||
r = roster.get(asset)
|
||||
name = None
|
||||
if r:
|
||||
common = (r.get("common") or "").strip()
|
||||
name = common or (" ".join(x for x in [r.get("first", ""), r.get("last", "")] if x)).strip()
|
||||
if not name:
|
||||
skipped.append((asset, "no_roster_name"))
|
||||
continue
|
||||
nation = nations.get(p["nation"])
|
||||
league = leagues.get(p["league"])
|
||||
club = teams.get(p["team"])
|
||||
if not nation or not league or not club:
|
||||
missing = [k for k, v in (("nation", nation), ("league", league), ("club", club)) if not v]
|
||||
skipped.append((asset, "unresolved_entity:" + ",".join(missing)))
|
||||
continue
|
||||
a = p["attrs"] # [pace, shooting, passing, dribbling, defending, physical]
|
||||
definitions.append({
|
||||
"id": card_id,
|
||||
"name": name,
|
||||
"overall": p["rating"],
|
||||
"position": p["pos"],
|
||||
"nation": nation,
|
||||
"league": league,
|
||||
"club": club,
|
||||
"pace": a[0], "shooting": a[1], "passing": a[2],
|
||||
"dribbling": a[3], "defending": a[4], "physical": a[5],
|
||||
"rarity": tier(p["rating"]),
|
||||
"image_path": None,
|
||||
})
|
||||
|
||||
catalog = {
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"game": GAME,
|
||||
"_provenance": {
|
||||
"generator": "scripts/seed_fifa17_cards.py",
|
||||
"generator_version": GENERATOR_VERSION,
|
||||
"sources": ["fifa17-recon/data/pool.json"],
|
||||
"base_cards_only": True,
|
||||
},
|
||||
"cards": {cid: catalog_cards[cid] for cid in sorted(catalog_cards)},
|
||||
}
|
||||
definitions.sort(key=lambda d: d["id"])
|
||||
return catalog, definitions, skipped, len(pool)
|
||||
|
||||
|
||||
def dumps(obj):
|
||||
return json.dumps(obj, indent=2, ensure_ascii=False, sort_keys=False) + "\n"
|
||||
|
||||
|
||||
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("--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)
|
||||
|
||||
reasons = {}
|
||||
for _, why in skipped:
|
||||
reasons[why.split(":")[0]] = reasons.get(why.split(":")[0], 0) + 1
|
||||
report = {
|
||||
"pool_rows": pool_rows,
|
||||
"catalog_entries": len(catalog["cards"]),
|
||||
"definitions_seeded": len(definitions),
|
||||
"skipped_definitions": len(skipped),
|
||||
"skip_reasons": reasons,
|
||||
"catalog_sha256": hashlib.sha256(catalog_bytes.encode()).hexdigest()[:16],
|
||||
}
|
||||
print(json.dumps(report, indent=2), file=sys.stderr)
|
||||
|
||||
if args.check:
|
||||
drift = False
|
||||
for path, want in [(args.catalog_out, catalog_bytes)] + (
|
||||
[(args.defs_out, defs_bytes)] if args.defs_out else []
|
||||
):
|
||||
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)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user