Files
date_game/scripts/validate_content.py

156 lines
6.1 KiB
Python

#!/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()