feat(@projects/@magic-civilization): ✨ add bridge call pattern test validation
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
9ce47ab986
commit
573fdec713
2 changed files with 117 additions and 0 deletions
|
|
@ -404,6 +404,35 @@ mod tests {
|
|||
assert!(occ.contains_work("b"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_call_pattern_compiles() {
|
||||
// Mirror of the exact call sequence used by api-gdext::civics::
|
||||
// GdGreatWorkOccupation. Lives here so we can catch type drift
|
||||
// when api-gdext can't build due to unrelated crate blockers.
|
||||
use crate::city::TileYield;
|
||||
use crate::harvest::{apply_harvest_policy, HarvestResult};
|
||||
use crate::harvest_policy::HarvestPolicyRegistry;
|
||||
use crate::tile::WorkedTile;
|
||||
|
||||
let tile = WorkedTile::with_policy((0, 0), "extract_sustainable");
|
||||
let reg = HarvestPolicyRegistry::default();
|
||||
let base = TileYield {
|
||||
coord: tile.coord,
|
||||
food: 2.0,
|
||||
production: 1.0,
|
||||
gold: 0.0,
|
||||
culture: 0.0,
|
||||
science: 0.0,
|
||||
collectibles: Vec::new(),
|
||||
food_modifier: 1.0,
|
||||
};
|
||||
// Should not panic on empty registry — falls through to Yield(base).
|
||||
match apply_harvest_policy(base, &tile, ®) {
|
||||
HarvestResult::Yield(_) => {}
|
||||
HarvestResult::Chop(_) => panic!("unexpected chop"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_occupancy_summary() {
|
||||
let mut occ = GreatWorkOccupation::new();
|
||||
|
|
|
|||
|
|
@ -487,6 +487,93 @@ class GameDataValidator:
|
|||
else:
|
||||
self._ok(label)
|
||||
|
||||
def validate_recipes(self):
|
||||
"""p2-57: cross-ref public/resources/recipes/recipes.json against the
|
||||
known building ids and the union of raw resource ids
|
||||
(`public/resources/resources.json`) plus processed ids
|
||||
(`public/resources/typed-resources/processed.json`). Fails on any
|
||||
undeclared resource or unknown building.
|
||||
"""
|
||||
recipes_path = self.resources / "recipes" / "recipes.json"
|
||||
if not recipes_path.exists():
|
||||
return
|
||||
data, err = load_json_safe(recipes_path)
|
||||
if err or not isinstance(data, dict):
|
||||
self._fail("recipes/recipes.json", f"parse error: {err or 'wrong shape'}")
|
||||
return
|
||||
|
||||
# Known building ids.
|
||||
bdir = self.resources / "buildings"
|
||||
building_ids = self._load_id_set_from_split_dir(bdir) if bdir.exists() else set()
|
||||
if (self.game_data / "buildings").exists():
|
||||
building_ids |= self._load_id_set_from_split_dir(self.game_data / "buildings")
|
||||
|
||||
# Known resource ids: raws from resources.json + processed from
|
||||
# typed-resources/processed.json.
|
||||
resource_ids: set[str] = set()
|
||||
raw_path = self.resources / "resources.json"
|
||||
if raw_path.exists():
|
||||
raw_data, _ = load_json_safe(raw_path)
|
||||
if isinstance(raw_data, dict):
|
||||
for cat in ("bonus", "luxury", "strategic"):
|
||||
for entry in raw_data.get(cat, []):
|
||||
if isinstance(entry, dict) and "id" in entry:
|
||||
resource_ids.add(entry["id"])
|
||||
proc_path = self.resources / "typed-resources" / "processed.json"
|
||||
if proc_path.exists():
|
||||
proc_data, _ = load_json_safe(proc_path)
|
||||
if isinstance(proc_data, dict):
|
||||
for entry in proc_data.get("processed", []):
|
||||
if isinstance(entry, dict) and "id" in entry:
|
||||
resource_ids.add(entry["id"])
|
||||
|
||||
print(
|
||||
f"\n recipe cross-refs ({len(building_ids)} buildings, "
|
||||
f"{len(resource_ids)} resource ids)"
|
||||
)
|
||||
recipes = data.get("recipes", [])
|
||||
if not isinstance(recipes, list):
|
||||
self._fail("recipes/recipes.json", "`recipes` must be a list")
|
||||
return
|
||||
for idx, recipe in enumerate(recipes):
|
||||
if not isinstance(recipe, dict):
|
||||
self._fail(f"recipes[{idx}]", f"must be object, got {type(recipe).__name__}")
|
||||
continue
|
||||
bid = recipe.get("building_id")
|
||||
label = f"recipes/{bid or f'<idx {idx}>'}"
|
||||
if not isinstance(bid, str):
|
||||
self._fail(label, "missing/non-string building_id")
|
||||
continue
|
||||
if bid not in building_ids:
|
||||
self._fail(label, f"building_id='{bid}' is not a known building")
|
||||
else:
|
||||
self._ok(f"{label}.building_id")
|
||||
for edge_kind in ("consumes", "produces"):
|
||||
edges = recipe.get(edge_kind, [])
|
||||
if not isinstance(edges, list):
|
||||
self._fail(
|
||||
f"{label}.{edge_kind}",
|
||||
f"must be a list, got {type(edges).__name__}",
|
||||
)
|
||||
continue
|
||||
for j, edge in enumerate(edges):
|
||||
elabel = f"{label}.{edge_kind}[{j}]"
|
||||
if not isinstance(edge, dict):
|
||||
self._fail(elabel, "edge must be an object")
|
||||
continue
|
||||
rid = edge.get("resource")
|
||||
qty = edge.get("qty_per_turn")
|
||||
if not isinstance(rid, str):
|
||||
self._fail(elabel, "missing/non-string resource")
|
||||
elif rid not in resource_ids:
|
||||
self._fail(elabel, f"resource='{rid}' not declared in resources.json or typed-resources/processed.json")
|
||||
else:
|
||||
self._ok(f"{elabel}.resource")
|
||||
if not isinstance(qty, int) or qty < 1:
|
||||
self._fail(elabel, f"qty_per_turn must be int >= 1, got {qty!r}")
|
||||
else:
|
||||
self._ok(f"{elabel}.qty_per_turn")
|
||||
|
||||
def validate_building_requires_existing(self):
|
||||
"""p1-43a: every `requires_existing` ladder pointer must resolve to a
|
||||
real building id. Cross-refs `public/resources/buildings/*.json` only
|
||||
|
|
@ -646,6 +733,7 @@ class GameDataValidator:
|
|||
self.validate_biomes()
|
||||
self.validate_deposit_concept_refs()
|
||||
self.validate_resources_kind()
|
||||
self.validate_recipes()
|
||||
self.validate_guide_data()
|
||||
self.validate_building_requires_existing()
|
||||
self.validate_cross_refs()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue