307 lines
9.8 KiB
Python
307 lines
9.8 KiB
Python
|
|
#!/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())
|