| """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}") |
| |
| |
| 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}") |
| |
| |
| 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}") |
| |
| |
| 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" |
| } |
| } |
|
|