#!/usr/bin/env python3 """One-shot fixer for the Game 1 resource graph (task 1 from plans/what-are-all-the-gleaming-flurry.md). Three operations: 1. Rewrite public/games/age-of-dwarves/data/deposits/manifest.json to the curated Game 1 dwarf-mundane set (drop magic, rename dwarf-flavor names to existing generic files, create missing-but-mundane files). 2. Create missing deposit files for entries we want to keep (coral_reef, spices, horses-in-manifest needs no creation since horses.json already exists). 3. Regenerate `gates_units` and `gates_buildings` on every deposit by scanning the units/ and buildings/ directories for the `requires_resource` field. Units/buildings are the source of truth; deposit back-pointers are derived. Run with --dry-run to see what would change without writing files. """ from __future__ import annotations import argparse import json from collections import defaultdict from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent DEPOSITS_DIR = REPO_ROOT / "public" / "resources" / "deposits" UNITS_DIR = REPO_ROOT / "public" / "resources" / "units" BUILDINGS_DIR = REPO_ROOT / "public" / "resources" / "buildings" MANIFEST = ( REPO_ROOT / "public" / "games" / "age-of-dwarves" / "data" / "deposits" / "manifest.json" ) # Curated Game 1 manifest. Order is presentation order. GAME1_MANIFEST = [ # Strategic "iron_ore", "coal_seam", "saltpeter_deposit", "obsidian", "mithril_vein", # kept: referenced by ~10 dwarf units; dwarf-mithril is canon "horses", "stone_deposit", # Luxury — minerals "gold_vein", "ancient_marble", "chalk", "gypsum", "selenite", "calcite", "pyrite", "quartz", "fluorite", "tourmaline", "optical_calcite", "glimmer_salt", "pressure_crystal", # Luxury — gems "agate", "malachite", "garnet", "amethyst", "turquoise", "opal", "jade", "topaz", "aquamarine", "ruby", "sapphire", "emerald", "alexandrite", "diamond", # Luxury — flora / fauna "amber", "fur", "ivory", "dyes", "silk", "wine", "incense", "spices", # to create "cotton", # Bonus food "wheat", "cattle", "sheep", "deer", "fish", "crab", # Luxury — marine "pearls", "coral_reef", # to create ] # Placer-class assignment for the Game 1 manifest (Gap D / task 2). One of: # metal, gem, mineral, coal, volcanic, marine, flora_luxury, food. # Drives DepositPlacer dispatch in mc-mapgen (see plan §4 Gap D). PLACER_CLASS: dict[str, str] = { # metal "iron_ore": "metal", "gold_vein": "metal", "mithril_vein": "metal", "pyrite": "metal", # gem "agate": "gem", "alexandrite": "gem", "amber": "gem", "amethyst": "gem", "aquamarine": "gem", "diamond": "gem", "emerald": "gem", "garnet": "gem", "jade": "gem", "malachite": "gem", "opal": "gem", "ruby": "gem", "sapphire": "gem", "topaz": "gem", "turquoise": "gem", "tourmaline": "gem", # mineral (industrial non-gem) "chalk": "mineral", "gypsum": "mineral", "selenite": "mineral", "calcite": "mineral", "fluorite": "mineral", "optical_calcite": "mineral", "quartz": "mineral", "glimmer_salt": "mineral", "pressure_crystal": "mineral", "ancient_marble": "mineral", "stone_deposit": "mineral", # coal — needs its own placer because it forms in-epoch during the swamp biome stage "coal_seam": "coal", # volcanic "obsidian": "volcanic", "saltpeter_deposit": "volcanic", # marine "pearls": "marine", "coral_reef": "marine", "fish": "marine", "crab": "marine", # flora_luxury "dyes": "flora_luxury", "silk": "flora_luxury", "wine": "flora_luxury", "incense": "flora_luxury", "spices": "flora_luxury", "cotton": "flora_luxury", "ivory": "flora_luxury", "fur": "flora_luxury", # food "wheat": "food", "cattle": "food", "sheep": "food", "deer": "food", "horses": "food", } # Files to author if absent. Schema lifted from amber.json + adjusted. DEPOSITS_TO_CREATE: dict[str, dict] = { "spices": { "id": "spices", "category": "luxury", "name": "Spice Groves", "tier": 3, "terrains": ["jungle", "savanna", "grassland", "plains"], "near_start": False, "min_per_player": 0, "food_bonus": 1, "production_bonus": 0, "trade_bonus": 4, "culture_bonus": 1, "sprite": "sprites/resources/spices.png", "description": "Aromatic spice plants cultivated for trade and preservation.", "encyclopedia": { "category": "world", "entry_type": "deposit", "detail_route": "/map/resources", "tags": [], }, "quality_scale": { "1": {"multiplier": 0.5, "label": "Marginal"}, "2": {"multiplier": 0.75, "label": "Poor"}, "3": {"multiplier": 1.0, "label": "Standard"}, "4": {"multiplier": 1.25, "label": "Rich"}, "5": {"multiplier": 1.5, "label": "Exceptional"}, }, "gates_units": [], "gates_buildings": [], "placer_class": "flora_luxury", "concept_resource": "spices", "visibility": "always", "yield_gate": "herbalism", "improvement_gate": "herbalism", "indicator_decorations": [], }, "coral_reef": { "id": "coral_reef", "category": "luxury", "name": "Coral Reef", "tier": 3, "terrains": ["coast", "ocean"], "near_start": False, "min_per_player": 0, "food_bonus": 1, "production_bonus": 0, "trade_bonus": 3, "culture_bonus": 1, "sprite": "sprites/resources/coral_reef.png", "description": "Living reef formations rich in food and trade goods harvested by coastal divers.", "encyclopedia": { "category": "world", "entry_type": "deposit", "detail_route": "/map/resources", "tags": [], }, "quality_scale": { "1": {"multiplier": 0.5, "label": "Marginal"}, "2": {"multiplier": 0.75, "label": "Poor"}, "3": {"multiplier": 1.0, "label": "Standard"}, "4": {"multiplier": 1.25, "label": "Rich"}, "5": {"multiplier": 1.5, "label": "Exceptional"}, }, "gates_units": [], "gates_buildings": [], "placer_class": "marine", "concept_resource": "coral", "visibility": "always", "yield_gate": "fishing", "improvement_gate": "fishing", "indicator_decorations": [], }, } def _load_array(path: Path) -> list[dict]: with path.open() as f: data = json.load(f) if isinstance(data, list): return data if isinstance(data, dict): return [data] return [] def _scan_requires_resource(directory: Path) -> dict[str, list[str]]: """Return resource_id -> sorted list of item ids that require it.""" rev: dict[str, list[str]] = defaultdict(list) for path in sorted(directory.iterdir()): if path.suffix != ".json" or path.name.endswith(".schema.json"): continue try: for entry in _load_array(path): if not isinstance(entry, dict): continue rres = entry.get("requires_resource") if rres: rev[rres].append(entry["id"]) except Exception: continue return {k: sorted(set(v)) for k, v in rev.items()} def main() -> int: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--dry-run", action="store_true") args = ap.parse_args() changes: list[str] = [] # Step 1: rewrite manifest. new_manifest = { "source": "resources/deposits", "includes": list(GAME1_MANIFEST), } if not args.dry_run: MANIFEST.write_text(json.dumps(new_manifest, indent=2) + "\n") changes.append(f"manifest → {len(GAME1_MANIFEST)} entries (was 60)") # Step 2: create missing deposit files. for did, spec in DEPOSITS_TO_CREATE.items(): path = DEPOSITS_DIR / f"{did}.json" if path.exists(): continue if not args.dry_run: path.write_text(json.dumps(spec, indent=2) + "\n") changes.append(f"created deposits/{did}.json") # Step 3: regenerate gates_units / gates_buildings on every deposit from # the units/buildings source of truth. rev_units = _scan_requires_resource(UNITS_DIR) rev_bldgs = _scan_requires_resource(BUILDINGS_DIR) rewritten = 0 for path in sorted(DEPOSITS_DIR.iterdir()): if path.suffix != ".json" or path.name.endswith(".schema.json"): continue if path.name in {"deposit_categories.json"}: continue try: with path.open() as f: dep = json.load(f) except Exception: continue if not isinstance(dep, dict) or "id" not in dep: continue did = dep["id"] new_units = rev_units.get(did, []) new_bldgs = rev_bldgs.get(did, []) new_class = PLACER_CLASS.get(did) unchanged = ( dep.get("gates_units") == new_units and dep.get("gates_buildings") == new_bldgs and (new_class is None or dep.get("placer_class") == new_class) ) if unchanged: continue dep["gates_units"] = new_units dep["gates_buildings"] = new_bldgs if new_class is not None: dep["placer_class"] = new_class if not args.dry_run: path.write_text(json.dumps(dep, indent=2) + "\n") rewritten += 1 changes.append(f"regenerated gates_* on {rewritten} deposit files") for line in changes: print(("[dry-run] " if args.dry_run else "[applied] ") + line) return 0 if __name__ == "__main__": raise SystemExit(main())