Files
heros_web/scripts/write-unit-sprite-batch-queue.py

220 lines
7.8 KiB
Python

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()