73 lines
2.6 KiB
Python
73 lines
2.6 KiB
Python
|
|
"""Text-only coverage gate for every visual kind currently present in the game."""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import argparse
|
||
|
|
from collections import defaultdict
|
||
|
|
from pathlib import Path
|
||
|
|
import sys
|
||
|
|
|
||
|
|
import yaml
|
||
|
|
|
||
|
|
ROOT = Path(__file__).resolve().parents[1]
|
||
|
|
COVERAGE = ROOT / "assets/content_coverage.yaml"
|
||
|
|
MANIFESTS = ROOT / "assets/manifests"
|
||
|
|
|
||
|
|
|
||
|
|
def declared_assets() -> set[str]:
|
||
|
|
result: set[str] = set()
|
||
|
|
for path in sorted(MANIFESTS.glob("*.yaml")):
|
||
|
|
raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
||
|
|
result.update(key for key, value in raw.items()
|
||
|
|
if key not in {"version", "defaults"} and isinstance(value, dict))
|
||
|
|
return result
|
||
|
|
|
||
|
|
|
||
|
|
def requirements(spec: dict) -> dict[str, list[str]]:
|
||
|
|
result: dict[str, list[str]] = defaultdict(list)
|
||
|
|
agent = spec["agents"]
|
||
|
|
for slot, components in agent["components"].items():
|
||
|
|
for component in components:
|
||
|
|
for animation in agent["animations"]:
|
||
|
|
result[f"agent/{slot}"].append(f"agent_{component}_{animation}")
|
||
|
|
for kind in spec["enemies"]["kinds"]:
|
||
|
|
for animation in spec["enemies"]["animations"]:
|
||
|
|
result["enemy"].append(f"{kind}_{animation}")
|
||
|
|
result["environment"].extend(spec["environment"]["required"])
|
||
|
|
result["item_icon"].extend(spec["item_icons"]["required"])
|
||
|
|
return result
|
||
|
|
|
||
|
|
|
||
|
|
def report(release: bool) -> int:
|
||
|
|
spec = yaml.safe_load(COVERAGE.read_text(encoding="utf-8"))
|
||
|
|
if spec.get("version") != 1:
|
||
|
|
raise ValueError("assets/content_coverage.yaml: unsupported version")
|
||
|
|
have = declared_assets()
|
||
|
|
groups = requirements(spec)
|
||
|
|
missing_total = 0
|
||
|
|
print(f"{'GROUP':<20} {'HAVE':>5} {'NEED':>5} {'MISSING':>7}")
|
||
|
|
print("-" * 40)
|
||
|
|
for group, wanted in groups.items():
|
||
|
|
missing = sorted(set(wanted) - have)
|
||
|
|
missing_total += len(missing)
|
||
|
|
print(f"{group:<20} {len(wanted)-len(missing):>5} {len(wanted):>5} {len(missing):>7}")
|
||
|
|
for asset in missing:
|
||
|
|
print(f" - {asset}")
|
||
|
|
covered = sum(len(v) for v in groups.values()) - missing_total
|
||
|
|
total = sum(len(v) for v in groups.values())
|
||
|
|
print(f"coverage: {covered}/{total}; missing={missing_total}")
|
||
|
|
return 1 if release and missing_total else 0
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> int:
|
||
|
|
parser = argparse.ArgumentParser()
|
||
|
|
parser.add_argument("--release", action="store_true", help="fail if any required asset is absent")
|
||
|
|
return report(parser.parse_args().release)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
try:
|
||
|
|
raise SystemExit(main())
|
||
|
|
except (OSError, ValueError, KeyError) as error:
|
||
|
|
print(f"ERROR: {error}", file=sys.stderr)
|
||
|
|
raise SystemExit(2)
|