#!/usr/bin/env python3 """Canonical FIFA 17 kit map, joined from the extracted client tables. Authority for every kit question that a table can answer, so nobody has to reverse a binary for a fact that is sitting in a JSON row. Reads only: fifa17-recon/data/tables/fcc_kitcards.json the FUT KIT CARD definitions fifa17-recon/data/tables/teamkits.json the ENGINE kit rows Everything printed is TABLE_PROVEN unless the line says otherwise: it is a direct count over the full table, not a sample. Usage: python3 audit_fifa17_kits.py human report python3 audit_fifa17_kits.py --json machine-readable, for tests/tools python3 audit_fifa17_kits.py --team 21 drill into one team """ from __future__ import annotations import argparse import json import os import sys from collections import Counter, defaultdict TABLES = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "data", "tables") # TABLE_PROVEN, established by this script's own discriminating test (see # category_type_evidence): a kit CARD's `category` selects the engine kit ROW's # `teamkittypetechid` at the same (team, year). CATEGORY_TO_KIT_TYPE = {2: 0, 3: 1, 5: 2} KIT_TYPE_NAME = {0: "HOME", 1: "AWAY", 2: "THIRD", 3: "FOURTH", 5: "GK", 6: "SPECIAL6", 7: "SPECIAL7"} def load(name): with open(os.path.join(TABLES, name), "r", encoding="utf-8") as fh: data = json.load(fh) return data if isinstance(data, list) else data.get("rows", data) def band(carddbid: int) -> int: """The 6_300_000 / 6_400_000 id band.""" return (carddbid // 100_000) * 100_000 def category_type_evidence(cards, kits): """The DISCRIMINATING test behind CATEGORY_TO_KIT_TYPE. Asserting "category 3 means away" because away kits usually exist is not evidence -- types 0/1/2 are present for most teams, so the claim is true by construction. What discriminates is the teams that LACK a type: if category 3 really means type 1, then no category-3 card may exist for a (team, year) that has no type-1 row. Same for category 5 and type 2. """ kits_by = defaultdict(set) for r in kits: kits_by[(r["teamtechid"], r["year"])].add(r["teamkittypetechid"]) cards_by = defaultdict(list) for r in cards: cards_by[(r["teamid"], r["year"])].append(r) out = {} for cat, want in CATEGORY_TO_KIT_TYPE.items(): # keys that HAVE teamkits rows but not the wanted type lacking = [k for k, t in kits_by.items() if t and want not in t] counterexamples = [ r["carddbid"] for k in lacking for r in cards_by.get(k, []) if r["category"] == cat ] out[cat] = { "kit_type": want, "name": KIT_TYPE_NAME[want], "keys_lacking_type": len(lacking), "counterexamples": counterexamples, } return out def audit(): cards = load("fcc_kitcards.json") kits = load("teamkits.json") kits_by = defaultdict(list) for r in kits: kits_by[(r["teamtechid"], r["year"])].append(r) rows = [] for c in cards: key = (c["teamid"], c["year"]) want = CATEGORY_TO_KIT_TYPE.get(c["category"]) match = next((k for k in kits_by.get(key, []) if k["teamkittypetechid"] == want), None) rows.append( { "carddbid": c["carddbid"], "band": band(c["carddbid"]), "teamid": c["teamid"], "year": c["year"], "category": c["category"], "kit_type": want, "kit_type_name": KIT_TYPE_NAME.get(want, "?"), "assetid": c["assetid"], "cardassetid": c["cardassetid"], "value": c["value"], "weightrare": c["weightrare"], # These are BYTE OFFSETS into the table's string blob, not ids. # The blob is not among the extracted tables, so a kit's own # name string is NOT recoverable from data/tables alone. "name_offset": c["name"], "header_offset": c["header"], "description_offset": c["description"], "teamkitid": match["teamkitid"] if match else None, "teamkit_islocked": match["islocked"] if match else None, "teamkit_embargoed": match["isembargoed"] if match else None, } ) dupes = [k for k, n in Counter((r["teamid"], r["year"], r["category"]) for r in rows).items() if n > 1] return { "counts": {"fcc_kitcards": len(cards), "teamkits": len(kits)}, "bands": dict(sorted(Counter(r["band"] for r in rows).items())), "band_x_assetid": {f"{b}/{a}": n for (b, a), n in sorted(Counter((r["band"], r["assetid"]) for r in rows).items())}, "band_x_category": {f"{b}/{c}": n for (b, c), n in sorted(Counter((r["band"], r["category"]) for r in rows).items())}, "category_counts": dict(sorted(Counter(r["category"] for r in rows).items())), "cardassetid": sorted({r["cardassetid"] for r in rows}), "category_type_evidence": category_type_evidence(cards, kits), "unmatched": [r["carddbid"] for r in rows if r["teamkitid"] is None], "duplicate_team_year_category": dupes, "teamkits_islocked": dict(Counter(r["islocked"] for r in kits)), "teamkits_embargoed": dict(Counter(r["isembargoed"] for r in kits)), "teamkits_types": dict(sorted(Counter(r["teamkittypetechid"] for r in kits).items())), "rows": rows, } def main(): ap = argparse.ArgumentParser() ap.add_argument("--json", action="store_true") ap.add_argument("--team", type=int) args = ap.parse_args() a = audit() if args.json: json.dump(a, sys.stdout, indent=2) return print("FIFA 17 kit map — TABLE_PROVEN from the extracted client tables") print(f" fcc_kitcards rows : {a['counts']['fcc_kitcards']}") print(f" teamkits rows : {a['counts']['teamkits']}") print("\nid bands") for b, n in a["bands"].items(): print(f" {b}: {n}") print("\nband/assetid (assetid is fully determined by band)") for k, n in a["band_x_assetid"].items(): print(f" {k}: {n}") print("\nband/category") for k, n in a["band_x_category"].items(): print(f" {k}: {n}") print(f"\ncardassetid values: {a['cardassetid']} (the FUT card frame, not the kit art)") print("\ncategory -> engine kit type, with the discriminating test") for cat, ev in a["category_type_evidence"].items(): verdict = "HOLDS" if not ev["counterexamples"] else f"FAILS ({len(ev['counterexamples'])})" print(f" category {cat} -> type {ev['kit_type']} {ev['name']:6s} " f"| {ev['keys_lacking_type']:4d} (team,year) keys lack that type, " f"{len(ev['counterexamples'])} counterexample(s) -> {verdict}") print("\nengine kit types present in teamkits") for t, n in a["teamkits_types"].items(): print(f" type {t} {KIT_TYPE_NAME.get(t,'?'):8s}: {n}") print(f"\nteamkits islocked : {a['teamkits_islocked']} <- every row, so NOT the selector lock") print(f"teamkits embargoed : {a['teamkits_embargoed']}") print(f"\nanomalies") print(f" cards with no matching teamkits row : {len(a['unmatched'])}") print(f" duplicate (team,year,category) : {len(a['duplicate_team_year_category'])}") if args.team is not None: print(f"\n=== team {args.team} ===") print(f" {'carddbid':10s} {'cat':4s} {'type':7s} {'year':6s} {'assetid':8s} {'teamkitid':10s} locked") for r in sorted((r for r in a["rows"] if r["teamid"] == args.team), key=lambda r: r["carddbid"]): print(f" {r['carddbid']:<10} {r['category']:<4} {r['kit_type_name']:<7} {r['year']:<6} " f"{r['assetid']:<8} {str(r['teamkitid']):<10} {r['teamkit_islocked']}") if __name__ == "__main__": main()