from __future__ import annotations import json from collections import Counter from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] DOCS_DIR = ROOT / "docs" MANIFEST_PATH = DOCS_DIR / "unit-sprite-rework-manifest.json" AUDIT_PATH = DOCS_DIR / "unit-sprite-rework-audit.json" MARKDOWN_PATH = DOCS_DIR / "unit-sprite-batch-queue.md" JSON_PATH = DOCS_DIR / "unit-sprite-batch-queue.json" BATCH_ORDER = [ "batch1-approved-core", "batch1-approved-rebel", "batch2-shu", "batch3-wei", "batch4-wu", "batch5-rebel-other", "batch6-nanman-special", "batch7-unique-officer", ] BATCH_LABELS = { "batch1-approved-core": "Batch 1 approved core heroes", "batch1-approved-rebel": "Batch 1 approved rebel references", "batch2-shu": "Batch 2 Shu generic units", "batch3-wei": "Batch 3 Wei generic units", "batch4-wu": "Batch 4 Wu generic units", "batch5-rebel-other": "Batch 5 remaining rebel/bandit units", "batch6-nanman-special": "Batch 6 Nanman/special units", "batch7-unique-officer": "Batch 7 unique officers", } SPEC_BY_STEM = { "unit-shu-spearman": "unit-sprite-batch2-shu-spearman-spec.md", "unit-shu-archer": "unit-sprite-batch2-shu-archer-spec.md", "unit-shu-cavalry": "unit-sprite-batch2-shu-cavalry-spec.md", "unit-shu-strategist": "unit-sprite-batch2-shu-strategist-spec.md", "unit-shu-officer": "unit-sprite-batch2-shu-officer-spec.md", } def load_json(path: Path) -> dict[str, Any]: if not path.exists(): raise FileNotFoundError(f"missing required file: {path.relative_to(ROOT)}") return json.loads(path.read_text(encoding="utf-8")) def row_sort_key(row: dict[str, Any], manifest: dict[str, Any]) -> tuple[int, int, str]: stem = row["stem"] pending = manifest.get("pendingApproval", []) next_after = manifest.get("nextAfterApproval", []) if stem in pending: return (0, pending.index(stem), stem) if stem in next_after: return (1, next_after.index(stem), stem) if row.get("review_stage") == "approved-baseline": return (3, 0, stem) return (2, 0, stem) def summarize_status(row: dict[str, Any]) -> str: base = row["base"]["status"] actions = row["actions"]["status"] if base == "ok" and actions == "ok": return "technical ok" return f"base {base}; actions {actions}" def queue_data(manifest: dict[str, Any], audit: dict[str, Any]) -> dict[str, Any]: rows = audit.get("rows", []) batches: list[dict[str, Any]] = [] pending = set(manifest.get("pendingApproval", [])) approved = set(manifest.get("approvedBaseline", [])) next_after = set(manifest.get("nextAfterApproval", [])) for batch in BATCH_ORDER: batch_rows = [row for row in rows if row.get("batch") == batch] batch_rows = sorted(batch_rows, key=lambda row: row_sort_key(row, manifest)) stage_counts = Counter(row.get("review_stage", "queued-rework") for row in batch_rows) units: list[dict[str, Any]] = [] for row in batch_rows: stem = row["stem"] queue_role = "approved reference" if stem in pending: queue_role = "current approval sample" elif stem in next_after: queue_role = "next primary queue" elif stem not in approved: queue_role = "queued rework" units.append( { "stem": stem, "reviewStage": row.get("review_stage"), "queueRole": queue_role, "baseStatus": row["base"]["status"], "actionStatus": row["actions"]["status"], "battleBasePx": [ row["base"].get("battle_min_width_px"), row["base"].get("battle_max_width_px"), row["base"].get("battle_min_height_px"), row["base"].get("battle_max_height_px"), ], "spec": SPEC_BY_STEM.get(stem), } ) batches.append( { "key": batch, "label": BATCH_LABELS.get(batch, batch), "total": len(batch_rows), "stageCounts": dict(sorted(stage_counts.items())), "units": units, } ) return { "schema": 1, "manifest": MANIFEST_PATH.relative_to(ROOT).as_posix(), "audit": AUDIT_PATH.relative_to(ROOT).as_posix(), "approvedBaseline": manifest.get("approvedBaseline", []), "pendingApproval": manifest.get("pendingApproval", []), "nextAfterApproval": manifest.get("nextAfterApproval", []), "stageCounts": audit.get("stage_counts", {}), "batches": batches, } def write_markdown(data: dict[str, Any]) -> None: lines = [ "# Unit Sprite Batch Queue", "", "This file is generated by `scripts/write-unit-sprite-batch-queue.py`.", "", "Use this as the long-running queue for the full hand-painted sprite rework. It is inventory and sequencing, not approval by itself.", "", "## Current State", "", f"- Approved baseline units: {len(data['approvedBaseline'])}", f"- Pending approval units: {len(data['pendingApproval'])}", f"- Queued rework units: {data.get('stageCounts', {}).get('queued-rework', 0)}", f"- Manifest: `{data['manifest']}`", f"- Audit: `{data['audit']}`", "", ] pending = data["pendingApproval"] if pending: lines.extend( [ "## Approval Blocker", "", "Do not start the next unit, commit, push, or deploy while these units are pending:", "", ] ) for stem in pending: lines.append(f"- `{stem}`") lines.append("") lines.extend(["## Next After Approval", ""]) next_after = data["nextAfterApproval"] if next_after: for index, stem in enumerate(next_after, start=1): spec = SPEC_BY_STEM.get(stem) spec_note = f" - spec `{spec}`" if spec else "" lines.append(f"{index}. `{stem}`{spec_note}") else: lines.append("No next unit is currently defined.") lines.append("") lines.extend(["## Batch Queue", ""]) for batch in data["batches"]: lines.extend( [ f"### {batch['label']}", "", f"- Batch key: `{batch['key']}`", f"- Total units: {batch['total']}", f"- Stage counts: {', '.join(f'{key} {value}' for key, value in batch['stageCounts'].items()) or 'none'}", "", "| unit | role | stage | status | battle base px | spec |", "| --- | --- | --- | --- | --- | --- |", ] ) for unit in batch["units"]: battle = unit["battleBasePx"] battle_text = f"{battle[0]}..{battle[1]} x {battle[2]}..{battle[3]}" spec = f"`{unit['spec']}`" if unit["spec"] else "" status = ( "technical ok" if unit["baseStatus"] == "ok" and unit["actionStatus"] == "ok" else f"base {unit['baseStatus']}; actions {unit['actionStatus']}" ) lines.append( f"| `{unit['stem']}` | {unit['queueRole']} | {unit['reviewStage']} | {status} | {battle_text} | {spec} |" ) lines.append("") MARKDOWN_PATH.write_text("\n".join(lines), encoding="utf-8") def main() -> None: manifest = load_json(MANIFEST_PATH) audit = load_json(AUDIT_PATH) data = queue_data(manifest, audit) JSON_PATH.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") write_markdown(data) print(f"Wrote {JSON_PATH}") print(f"Wrote {MARKDOWN_PATH}") if __name__ == "__main__": main()