#!/usr/bin/env python3 """Validate core romance data files.""" from __future__ import annotations import json import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] DATA = ROOT / "data" def load_json(path: Path) -> dict: with path.open("r", encoding="utf-8") as handle: return json.load(handle) def fail(message: str) -> None: print(f"ERROR: {message}", file=sys.stderr) raise SystemExit(1) def main() -> None: characters_doc = load_json(DATA / "characters.json") locations_doc = load_json(DATA / "locations.json") progression_doc = load_json(DATA / "progression.json") presence_path = DATA / "character_location_presence.json" presence_doc = load_json(presence_path) if presence_path.exists() else None minimum_age = characters_doc["content_rules"]["minimum_age"] location_docs = {location["id"]: location for location in locations_doc["locations"]} locations = set(location_docs) character_ids: set[str] = set() preferred_locations_by_character: dict[str, set[str]] = {} for character in characters_doc["characters"]: character_id = character["id"] if character_id in character_ids: fail(f"duplicate character id: {character_id}") character_ids.add(character_id) preferred_locations_by_character[character_id] = set(character["preferred_locations"]) if character["age"] < minimum_age: fail( f"{character['display_name']} is {character['age']}; " f"minimum age is {minimum_age}" ) for location_id in character["preferred_locations"]: if location_id not in locations: fail(f"{character_id} references missing preferred location {location_id}") first_location = character["first_encounter"]["location"] if first_location not in locations: fail(f"{character_id} references missing first encounter location {first_location}") event_stages = [] for event in character["date_events"]: event_stages.append(event["stage"]) event_location = event["location"] if event_location not in locations: fail(f"{character_id} references missing date location {event_location}") if event_stages != [1, 2, 3]: fail(f"{character_id} date event stages must be [1, 2, 3]") for hook in character["adult_event_hooks"]: flags = set(hook["required_flags"]) if "consent_confirmed" not in flags: fail(f"{character_id} adult event hook lacks consent_confirmed") if "lovers" not in flags: fail(f"{character_id} adult event hook lacks lovers flag") adult_requirements = progression_doc["adult_event_requirements"] if adult_requirements["minimum_character_age"] != minimum_age: fail("progression minimum age does not match character content rules") if presence_doc is not None: presence_ids: set[str] = set() valid_availability = set(presence_doc["availability_values"]) valid_roles = set(presence_doc["role_values"]) for entry in presence_doc["characters"]: character_id = entry["character_id"] if character_id not in character_ids: fail(f"presence references missing character id: {character_id}") if character_id in presence_ids: fail(f"duplicate presence entry for character id: {character_id}") presence_ids.add(character_id) seen_locations: set[str] = set() total_weight = 0 for candidate in entry["locations"]: location_id = candidate["location_id"] if location_id not in locations: fail(f"{character_id} presence references missing location {location_id}") if location_id in seen_locations: fail(f"{character_id} has duplicate presence location {location_id}") seen_locations.add(location_id) role = candidate["role"] if role not in valid_roles: fail(f"{character_id} has invalid presence role {role}") availability = candidate["availability"] if availability not in valid_availability: fail(f"{character_id} has invalid availability {availability}") weight = candidate["weight"] if not isinstance(weight, int) or weight <= 0: fail(f"{character_id} has invalid presence weight {weight}") total_weight += weight location_time_slots = set(location_docs[location_id]["time_slots"]) for time_slot in candidate["time_slots"]: if time_slot not in location_time_slots: fail( f"{character_id} uses invalid time slot {time_slot} " f"for location {location_id}" ) if not candidate["hook"].strip(): fail(f"{character_id} presence location {location_id} lacks hook text") if total_weight != 100: fail(f"{character_id} presence weights must sum to 100, got {total_weight}") missing_preferred = preferred_locations_by_character[character_id].difference( seen_locations ) if missing_preferred: fail( f"{character_id} presence lacks preferred locations: " f"{sorted(missing_preferred)}" ) if presence_ids != character_ids: fail( "presence character mismatch; " f"missing={sorted(character_ids.difference(presence_ids))}, " f"extra={sorted(presence_ids.difference(character_ids))}" ) print( "Validated " f"{len(character_ids)} characters, " f"{len(locations)} locations, " f"{len(progression_doc['relationship_stages'])} relationship stages." ) if __name__ == "__main__": main()