Files
ai-game-2/scripts/check_transitions.py
2026-02-21 15:36:42 -08:00

136 lines
4.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""Validates transition nodes in KQ4 room scenes."""
import re
import sys
from pathlib import Path
from dataclasses import dataclass
from typing import Optional
@dataclass
class Transition:
exit_node_name: str
target_uid: str
appear_at_node: str
label: str
source_room: str
@dataclass
class SceneInfo:
uid: str
path: Path
node_names: set[str]
def find_uid_files(root: Path) -> dict[str, Path]:
"""Build a mapping of UID -> file path from all .uid files."""
uid_map = {}
for uid_file in root.rglob("*.uid"):
uid_content = uid_file.read_text().strip()
if uid_content.startswith("uid://"):
resource_path = uid_file.with_suffix("")
if resource_path.exists():
uid_map[uid_content] = resource_path
else:
uid_map[uid_content] = uid_file.with_suffix("")
return uid_map
def parse_scene_file(tscn_path: Path) -> tuple[Optional[str], set[str], list[Transition]]:
"""Parse a .tscn file to extract UID, node names, and transitions."""
content = tscn_path.read_text()
scene_uid = None
uid_match = re.search(r'\[gd_scene[^\]]*uid="([^"]+)"', content)
if uid_match:
scene_uid = uid_match.group(1)
node_names = set(re.findall(r'^\[node name="([^"]+)"', content, re.MULTILINE))
transitions = []
room_name = tscn_path.stem
transition_pattern = re.compile(
r'\[node name="([^"]+)"[^\]]*instance=ExtResource\([^\)]+\)\]\s*\n'
r'((?:[^\[]+\n)*)',
re.MULTILINE
)
for match in transition_pattern.finditer(content):
node_name = match.group(1)
body = match.group(2)
target_match = re.search(r'^target = "([^"]+)"', body, re.MULTILINE)
appear_match = re.search(r'^appear_at_node = "([^"]+)"', body, re.MULTILINE)
label_match = re.search(r'^label = "([^"]+)"', body, re.MULTILINE)
if target_match and appear_match:
transitions.append(Transition(
exit_node_name=node_name,
target_uid=target_match.group(1),
appear_at_node=appear_match.group(1),
label=label_match.group(1) if label_match else node_name,
source_room=room_name
))
return scene_uid, node_names, transitions
def main():
root = Path(__file__).parent.parent
scenes_dir = root / "scenes"
uid_map = find_uid_files(root)
scene_files = list(scenes_dir.glob("kq4_*/kq4_*.tscn"))
scene_files = [f for f in scene_files if "placeholder_template" not in str(f)]
scenes_by_uid: dict[str, SceneInfo] = {}
all_transitions: list[Transition] = []
for scene_file in scene_files:
scene_uid, node_names, transitions = parse_scene_file(scene_file)
if scene_uid:
scenes_by_uid[scene_uid] = SceneInfo(
uid=scene_uid,
path=scene_file,
node_names=node_names
)
all_transitions.extend(transitions)
errors = 0
for t in all_transitions:
source_room = t.source_room
target_scene = scenes_by_uid.get(t.target_uid)
if target_scene is None:
print(f"ERROR: {source_room} exit '{t.exit_node_name}' -> target UID '{t.target_uid}' NOT FOUND")
errors += 1
continue
target_room_name = target_scene.path.stem
if t.appear_at_node not in target_scene.node_names:
print(f"ERROR: {source_room} exit '{t.exit_node_name}' -> {target_room_name} node '{t.appear_at_node}' NOT FOUND")
errors += 1
continue
print(f"OK: {source_room} exit '{t.exit_node_name}' -> {target_room_name} (node: {t.appear_at_node})")
print(f"\n{'='*60}")
print(f"Checked {len(all_transitions)} transitions across {len(scene_files)} rooms")
if errors:
print(f"FOUND {errors} ERROR(S)")
sys.exit(1)
else:
print("All transitions valid!")
sys.exit(0)
if __name__ == "__main__":
main()