| """ |
| Saved character library — name a character once, reuse it in any movie. |
| |
| Custom characters live in characters.json and resolve everywhere draw.draw_sprite |
| is used; a saved name takes precedence over the built-in library. Add them from |
| ASCII art you provide, or let the model draw one from a description. |
| |
| python characters.py list |
| python characters.py add "fox king" --desc "a fox wearing a tiny crown" --color orange |
| python characters.py add robo --art-file robo.txt --color gray |
| python characters.py show "fox king" |
| python characters.py rm robo |
| |
| A leaf module: it imports nothing from the package at module load (draw.py imports |
| THIS), so there is no cycle; add() imports draw lazily only when it must draw. |
| """ |
|
|
| import argparse |
| import json |
| import os |
|
|
| STORE = os.path.join(os.path.dirname(__file__), "characters.json") |
|
|
|
|
| def _load() -> dict: |
| if not os.path.exists(STORE): |
| return {} |
| try: |
| return json.load(open(STORE)) |
| except (OSError, json.JSONDecodeError): |
| return {} |
|
|
|
|
| def _save(d: dict) -> None: |
| json.dump(d, open(STORE, "w"), ensure_ascii=False, indent=2) |
|
|
|
|
| def library() -> dict: |
| return _load() |
|
|
|
|
| def get(name: str | None) -> list[str] | None: |
| e = _load().get((name or "").strip().lower()) |
| return e["art"] if e else None |
|
|
|
|
| def color(name: str | None) -> str | None: |
| e = _load().get((name or "").strip().lower()) |
| return e.get("color") if e else None |
|
|
|
|
| def save(name: str, art: list[str], color: str | None = None, desc: str = "") -> str: |
| key = name.strip().lower() |
| d = _load() |
| d[key] = {"art": art, "color": color, "desc": desc} |
| _save(d) |
| return key |
|
|
|
|
| def add(name: str, desc: str = "", art: list[str] | None = None, color: str | None = None) -> str: |
| """Add a character: use provided `art`, else draw it from `desc`/name.""" |
| import draw |
| art = draw._normalize(art) if art is not None else draw.draw_sprite(name, desc) |
| return save(name, art, color, desc) |
|
|
|
|
| def remove(name: str) -> None: |
| d = _load() |
| d.pop(name.strip().lower(), None) |
| _save(d) |
|
|
|
|
| def main(): |
| ap = argparse.ArgumentParser(description="Manage the saved character library.") |
| sub = ap.add_subparsers(dest="cmd", required=True) |
| sub.add_parser("list") |
| a = sub.add_parser("add") |
| a.add_argument("name") |
| a.add_argument("--desc", default="") |
| a.add_argument("--art-file", help="a text file of ASCII art (<=7 rows, <=14 cols)") |
| a.add_argument("--color", help="a palette colour name (e.g. orange, cyan, red)") |
| s = sub.add_parser("show"); s.add_argument("name") |
| r = sub.add_parser("rm"); r.add_argument("name") |
| args = ap.parse_args() |
|
|
| if args.cmd == "list": |
| lib = library() |
| if not lib: |
| print("(no saved characters yet)") |
| for k, e in sorted(lib.items()): |
| print(f" {k} [{e.get('color') or 'auto'}] {e.get('desc','')}") |
| elif args.cmd == "add": |
| art = open(args.art_file, encoding="utf-8").read().splitlines() if args.art_file else None |
| key = add(args.name, desc=args.desc, art=art, color=args.color) |
| print(f"saved '{key}':") |
| print("\n".join(get(key))) |
| elif args.cmd == "show": |
| art = get(args.name) |
| print("\n".join(art) if art else f"(no character named '{args.name}')") |
| elif args.cmd == "rm": |
| remove(args.name) |
| print(f"removed '{args.name}'") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|