Spaces:
Running on Zero
Running on Zero
| """Schema validation utilities for the game pipeline.""" | |
| import json | |
| import jsonschema | |
| from pathlib import Path | |
| from typing import Any, Tuple | |
| def load_schema(schema_name: str = "game_schema.json") -> dict: | |
| """Load a JSON schema from the schemas directory. | |
| Args: | |
| schema_name: Name of the schema file (default: game_schema.json) | |
| Returns: | |
| Loaded schema as dict | |
| """ | |
| schema_path = Path("app/schemas") / schema_name | |
| with open(schema_path, 'r', encoding='utf-8') as f: | |
| return json.load(f) | |
| def validate_game_schema(game: dict) -> Tuple[bool, list[str]]: | |
| """Validate a game JSON against the game schema. | |
| Args: | |
| game: Game data to validate | |
| Returns: | |
| Tuple of (is_valid, list of error messages) | |
| """ | |
| schema = load_schema("game_schema.json") | |
| errors = [] | |
| try: | |
| jsonschema.validate(instance=game, schema=schema) | |
| return True, [] | |
| except jsonschema.ValidationError as e: | |
| errors.append(f"Validation error: {e.message}") | |
| errors.append(f"Path: {'.'.join(str(p) for p in e.absolute_path)}") | |
| return False, errors | |
| except jsonschema.SchemaError as e: | |
| errors.append(f"Schema error: {e.message}") | |
| return False, errors | |
| def validate_task_structure(task: dict) -> Tuple[bool, list[str]]: | |
| """Validate a single task object. | |
| Args: | |
| task: Task data to validate | |
| Returns: | |
| Tuple of (is_valid, list of error messages) | |
| """ | |
| required_fields = [ | |
| 'task_id', 'title', 'description', 'location_hint', | |
| 'points', 'time_limit_minutes', 'proof_type', 'hint', 'safety_note' | |
| ] | |
| errors = [] | |
| for field in required_fields: | |
| if field not in task: | |
| errors.append(f"Missing required field: {field}") | |
| # Validate proof_type enum | |
| if 'proof_type' in task: | |
| valid_types = ['photo', 'observation', 'text'] | |
| if task['proof_type'] not in valid_types: | |
| errors.append(f"Invalid proof_type: {task['proof_type']}. Must be one of {valid_types}") | |
| # Validate points and time_limit are positive | |
| if 'points' in task and task['points'] < 0: | |
| errors.append(f"Task points must be non-negative, got {task['points']}") | |
| if 'time_limit_minutes' in task and task['time_limit_minutes'] is not None: | |
| if task['time_limit_minutes'] < 0: | |
| errors.append(f"Task time_limit_minutes must be non-negative, got {task['time_limit_minutes']}") | |
| return len(errors) == 0, errors | |
| def validate_safety_structure(safety: dict) -> Tuple[bool, list[str]]: | |
| """Validate the safety object structure. | |
| Args: | |
| safety: Safety data to validate | |
| Returns: | |
| Tuple of (is_valid, list of error messages) | |
| """ | |
| required_fields = ['allowed_zone', 'forbidden_behaviors', 'adult_supervision', 'stop_conditions'] | |
| errors = [] | |
| for field in required_fields: | |
| if field not in safety: | |
| errors.append(f"Missing required safety field: {field}") | |
| # Validate field types | |
| if 'forbidden_behaviors' in safety and not isinstance(safety['forbidden_behaviors'], list): | |
| errors.append("forbidden_behaviors must be an array") | |
| if 'stop_conditions' in safety and not isinstance(safety['stop_conditions'], list): | |
| errors.append("stop_conditions must be an array") | |
| if 'adult_supervision' in safety and not isinstance(safety['adult_supervision'], bool): | |
| errors.append("adult_supervision must be a boolean") | |
| return len(errors) == 0, errors | |
| def create_minimal_game_template() -> dict: | |
| """Create a minimal valid game template for testing. | |
| Returns: | |
| Minimal valid game JSON matching the schema | |
| """ | |
| return { | |
| "game_id": "test-game-001", | |
| "title": "Test Game", | |
| "theme": "test", | |
| "setup": { | |
| "city": "Paris", | |
| "area": "Test Area", | |
| "meeting_point": "Central location", | |
| "duration_minutes": 45, | |
| "num_players": 4 | |
| }, | |
| "rules": [ | |
| "Rule 1", | |
| "Rule 2" | |
| ], | |
| "tasks": [ | |
| { | |
| "task_id": "t1", | |
| "title": "Task 1", | |
| "description": "Find something", | |
| "location_hint": "Look near the entrance", | |
| "points": 20, | |
| "time_limit_minutes": 10, | |
| "proof_type": "photo", | |
| "hint": "It's visible from the street", | |
| "safety_note": "Stay on public paths" | |
| } | |
| ], | |
| "global_hints": [ | |
| "Explore systematically" | |
| ], | |
| "score_rules": [ | |
| "1 point per second under time limit", | |
| "No penalty for hints" | |
| ], | |
| "tie_breaker": "Team with most tasks completed first", | |
| "safety": { | |
| "allowed_zone": "Public streets and parks in the designated area", | |
| "forbidden_behaviors": [ | |
| "Entering private buildings", | |
| "Crossing major roads unsafely" | |
| ], | |
| "adult_supervision": False, | |
| "stop_conditions": [ | |
| "Player injury", | |
| "Weather emergency" | |
| ] | |
| }, | |
| "story_seed": { | |
| "tone": "playful", | |
| "motifs": [ | |
| "discovery", | |
| "teamwork" | |
| ], | |
| "recap_style": "episode_recap" | |
| } | |
| } | |