Commit Β·
eaf7eca
1
Parent(s): ec2e649
Phase 1: establish the baseline
Browse files- .gitignore +104 -0
- app/__init__.py +1 -0
- app/data/games_dataset.json +0 -0
- app/main.py +14 -0
- app/prompts/game_generation.txt +33 -0
- app/prompts/game_repair.txt +18 -0
- app/prompts/story_recap.txt +20 -0
- app/schemas/event_schema.json +50 -0
- app/schemas/game_schema.json +178 -0
- app/schemas/journal_schema.json +57 -0
- app/schemas/story_packet_schema.json +65 -0
- app/services/__init__.py +1 -0
- app/services/generator.py +41 -0
- app/services/journal.py +34 -0
- app/services/retrieval.py +200 -0
- app/services/schema_validator.py +174 -0
- app/services/scoring.py +21 -0
- app/services/story.py +22 -0
- app/services/tracing.py +29 -0
- app/services/validator.py +54 -0
- inspect_dataset.py +123 -0
- requirements.txt +2 -0
- test_retrieval.py +135 -0
- test_schema.py +265 -0
.gitignore
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
build/
|
| 8 |
+
develop-eggs/
|
| 9 |
+
dist/
|
| 10 |
+
downloads/
|
| 11 |
+
eggs/
|
| 12 |
+
.eggs/
|
| 13 |
+
lib/
|
| 14 |
+
lib64/
|
| 15 |
+
parts/
|
| 16 |
+
sdist/
|
| 17 |
+
var/
|
| 18 |
+
wheels/
|
| 19 |
+
pip-wheel-metadata/
|
| 20 |
+
share/python-wheels/
|
| 21 |
+
*.egg-info/
|
| 22 |
+
.installed.cfg
|
| 23 |
+
*.egg
|
| 24 |
+
MANIFEST
|
| 25 |
+
|
| 26 |
+
# Virtual Environment
|
| 27 |
+
.venv/
|
| 28 |
+
venv/
|
| 29 |
+
ENV/
|
| 30 |
+
env/
|
| 31 |
+
.env
|
| 32 |
+
|
| 33 |
+
# IDE and Editor
|
| 34 |
+
.vscode/
|
| 35 |
+
.idea/
|
| 36 |
+
*.swp
|
| 37 |
+
*.swo
|
| 38 |
+
*~
|
| 39 |
+
.DS_Store
|
| 40 |
+
*.sublime-project
|
| 41 |
+
*.sublime-workspace
|
| 42 |
+
.project
|
| 43 |
+
.pydevproject
|
| 44 |
+
.settings/
|
| 45 |
+
*.code-workspace
|
| 46 |
+
|
| 47 |
+
# OS
|
| 48 |
+
Thumbs.db
|
| 49 |
+
.DS_Store
|
| 50 |
+
.AppleDouble
|
| 51 |
+
.LSOverride
|
| 52 |
+
|
| 53 |
+
# Logs and temporary files
|
| 54 |
+
app/logs/*.jsonl
|
| 55 |
+
app/logs/*.log
|
| 56 |
+
*.log
|
| 57 |
+
*.pot
|
| 58 |
+
|
| 59 |
+
# Runtime data
|
| 60 |
+
.coverage
|
| 61 |
+
.pytest_cache/
|
| 62 |
+
htmlcov/
|
| 63 |
+
|
| 64 |
+
# Jupyter Notebook
|
| 65 |
+
.ipynb_checkpoints
|
| 66 |
+
*.ipynb
|
| 67 |
+
|
| 68 |
+
# Unit test / coverage reports
|
| 69 |
+
.tox/
|
| 70 |
+
.hypothesis/
|
| 71 |
+
.coverage
|
| 72 |
+
.coverage.*
|
| 73 |
+
.cache
|
| 74 |
+
|
| 75 |
+
# mypy
|
| 76 |
+
.mypy_cache/
|
| 77 |
+
.dmypy.json
|
| 78 |
+
dmypy.json
|
| 79 |
+
|
| 80 |
+
# Pyre type checker
|
| 81 |
+
.pyre/
|
| 82 |
+
|
| 83 |
+
# Model files and large data
|
| 84 |
+
*.model
|
| 85 |
+
*.bin
|
| 86 |
+
*.pt
|
| 87 |
+
*.pth
|
| 88 |
+
*.ckpt
|
| 89 |
+
|
| 90 |
+
# Temporary and backup files
|
| 91 |
+
*.tmp
|
| 92 |
+
*.bak
|
| 93 |
+
*.swp
|
| 94 |
+
*.swo
|
| 95 |
+
*~
|
| 96 |
+
.~*
|
| 97 |
+
|
| 98 |
+
# Generated files
|
| 99 |
+
normalized_games.json
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
# other files to ignore
|
| 103 |
+
Geo_Chase_Project_Specification.md
|
| 104 |
+
ai_pipeline_source_of_truth.md
|
app/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""AI Pipeline for location-based game generation and management."""
|
app/data/games_dataset.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
app/main.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import spaces
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
zero = torch.Tensor([0]).cuda()
|
| 6 |
+
print(zero.device) # <-- 'cpu' π€
|
| 7 |
+
|
| 8 |
+
@spaces.GPU
|
| 9 |
+
def greet(n):
|
| 10 |
+
print(zero.device) # <-- 'cuda:0' π€
|
| 11 |
+
return f"Hello {zero + n} Tensor"
|
| 12 |
+
|
| 13 |
+
demo = gr.Interface(fn=greet, inputs=gr.Number(), outputs=gr.Text())
|
| 14 |
+
demo.launch()
|
app/prompts/game_generation.txt
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You are an expert urban real-world game designer. Your task is to create an engaging, safe, and playable location-based game.
|
| 2 |
+
|
| 3 |
+
## Context
|
| 4 |
+
- City: {city}
|
| 5 |
+
- Area: {area}
|
| 6 |
+
- Game Type: {game_type}
|
| 7 |
+
- Duration: {duration_minutes} minutes
|
| 8 |
+
- Number of Players: {num_players}
|
| 9 |
+
- Difficulty: {difficulty}
|
| 10 |
+
- Age Group: {age_group}
|
| 11 |
+
|
| 12 |
+
## Retrieved Examples
|
| 13 |
+
{retrieved_examples}
|
| 14 |
+
|
| 15 |
+
## Hard Safety Constraints
|
| 16 |
+
1. NO entering buildings, shops, private courtyards, rooftops, or fenced areas
|
| 17 |
+
2. NO proximity to river edges, canal edges, traffic, rail lines without explicit restrictions
|
| 18 |
+
3. NO direct interaction with strangers or staff
|
| 19 |
+
4. NO requiring purchases
|
| 20 |
+
5. All locations must be public, accessible, and safe
|
| 21 |
+
6. Include supervision requirements for mixed-age groups
|
| 22 |
+
|
| 23 |
+
## Output Requirements
|
| 24 |
+
- Return ONLY valid JSON that matches the provided schema
|
| 25 |
+
- Each task must have clear location hints, proof type, and safety notes
|
| 26 |
+
- Include global hints to help teams navigate
|
| 27 |
+
- Define clear win conditions
|
| 28 |
+
- Avoid invented private or inaccessible locations
|
| 29 |
+
|
| 30 |
+
## Output Schema
|
| 31 |
+
{output_schema}
|
| 32 |
+
|
| 33 |
+
Generate the game JSON:
|
app/prompts/game_repair.txt
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You are an expert editor specializing in game design quality assurance.
|
| 2 |
+
|
| 3 |
+
## Task
|
| 4 |
+
Fix the following failed validation checks in this game JSON. Make minimal changesβonly fix the specific failures listed.
|
| 5 |
+
|
| 6 |
+
## Failures
|
| 7 |
+
{failures}
|
| 8 |
+
|
| 9 |
+
## Original Game
|
| 10 |
+
{game_json}
|
| 11 |
+
|
| 12 |
+
## Requirements
|
| 13 |
+
- Return ONLY valid JSON matching the schema
|
| 14 |
+
- Keep all other fields unchanged
|
| 15 |
+
- Ensure no new validation failures are introduced
|
| 16 |
+
|
| 17 |
+
## Output
|
| 18 |
+
Repaired game JSON:
|
app/prompts/story_recap.txt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You are a talented narrative writer creating engaging recaps of location-based games.
|
| 2 |
+
|
| 3 |
+
## Game Data
|
| 4 |
+
{story_packet}
|
| 5 |
+
|
| 6 |
+
## Instructions
|
| 7 |
+
- Use ONLY facts from logs, tasks, journals, scores, and photos
|
| 8 |
+
- Do NOT invent locations or quotes
|
| 9 |
+
- Mention 2-4 concrete moments from player journals
|
| 10 |
+
- Highlight the decisive turning point
|
| 11 |
+
- Maintain a lively, memorable tone
|
| 12 |
+
- Keep recaps between 150-250 words for short recap, 400-600 words for long summary
|
| 13 |
+
|
| 14 |
+
## Output
|
| 15 |
+
Generate:
|
| 16 |
+
1. short_recap: Brief highlight for the result screen
|
| 17 |
+
2. long_summary: Full episode recap
|
| 18 |
+
3. poster_prompt: Visual prompt for image generation
|
| 19 |
+
|
| 20 |
+
Return as JSON with these three keys.
|
app/schemas/event_schema.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"title": "Event Schema",
|
| 4 |
+
"type": "object",
|
| 5 |
+
"required": [
|
| 6 |
+
"event_id",
|
| 7 |
+
"timestamp",
|
| 8 |
+
"session_id",
|
| 9 |
+
"team_id",
|
| 10 |
+
"event_type",
|
| 11 |
+
"payload"
|
| 12 |
+
],
|
| 13 |
+
"properties": {
|
| 14 |
+
"event_id": {
|
| 15 |
+
"type": "string",
|
| 16 |
+
"description": "Unique event identifier"
|
| 17 |
+
},
|
| 18 |
+
"timestamp": {
|
| 19 |
+
"type": "string",
|
| 20 |
+
"format": "date-time",
|
| 21 |
+
"description": "ISO-8601 timestamp"
|
| 22 |
+
},
|
| 23 |
+
"session_id": {
|
| 24 |
+
"type": "string",
|
| 25 |
+
"description": "Game session identifier"
|
| 26 |
+
},
|
| 27 |
+
"team_id": {
|
| 28 |
+
"type": "string",
|
| 29 |
+
"description": "Team identifier"
|
| 30 |
+
},
|
| 31 |
+
"event_type": {
|
| 32 |
+
"type": "string",
|
| 33 |
+
"enum": [
|
| 34 |
+
"task_revealed",
|
| 35 |
+
"task_completed",
|
| 36 |
+
"hint_used",
|
| 37 |
+
"task_skipped",
|
| 38 |
+
"photo_uploaded",
|
| 39 |
+
"journal_recorded",
|
| 40 |
+
"score_updated",
|
| 41 |
+
"game_finished"
|
| 42 |
+
],
|
| 43 |
+
"description": "Type of gameplay event"
|
| 44 |
+
},
|
| 45 |
+
"payload": {
|
| 46 |
+
"type": "object",
|
| 47 |
+
"description": "Event-specific data"
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
}
|
app/schemas/game_schema.json
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"title": "Game Schema",
|
| 4 |
+
"type": "object",
|
| 5 |
+
"required": [
|
| 6 |
+
"game_id",
|
| 7 |
+
"title",
|
| 8 |
+
"theme",
|
| 9 |
+
"setup",
|
| 10 |
+
"rules",
|
| 11 |
+
"tasks",
|
| 12 |
+
"global_hints",
|
| 13 |
+
"score_rules",
|
| 14 |
+
"tie_breaker",
|
| 15 |
+
"safety",
|
| 16 |
+
"story_seed"
|
| 17 |
+
],
|
| 18 |
+
"properties": {
|
| 19 |
+
"game_id": {
|
| 20 |
+
"type": "string",
|
| 21 |
+
"description": "Unique game identifier"
|
| 22 |
+
},
|
| 23 |
+
"title": {
|
| 24 |
+
"type": "string",
|
| 25 |
+
"description": "Game title"
|
| 26 |
+
},
|
| 27 |
+
"theme": {
|
| 28 |
+
"type": "string",
|
| 29 |
+
"description": "Game theme"
|
| 30 |
+
},
|
| 31 |
+
"setup": {
|
| 32 |
+
"type": "object",
|
| 33 |
+
"required": ["city", "area", "meeting_point", "duration_minutes", "num_players"],
|
| 34 |
+
"properties": {
|
| 35 |
+
"city": {
|
| 36 |
+
"type": "string"
|
| 37 |
+
},
|
| 38 |
+
"area": {
|
| 39 |
+
"type": "string"
|
| 40 |
+
},
|
| 41 |
+
"meeting_point": {
|
| 42 |
+
"type": "string"
|
| 43 |
+
},
|
| 44 |
+
"duration_minutes": {
|
| 45 |
+
"type": "integer"
|
| 46 |
+
},
|
| 47 |
+
"num_players": {
|
| 48 |
+
"type": "integer"
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
},
|
| 52 |
+
"rules": {
|
| 53 |
+
"type": "array",
|
| 54 |
+
"items": {
|
| 55 |
+
"type": "string"
|
| 56 |
+
},
|
| 57 |
+
"description": "Game rules"
|
| 58 |
+
},
|
| 59 |
+
"tasks": {
|
| 60 |
+
"type": "array",
|
| 61 |
+
"items": {
|
| 62 |
+
"type": "object",
|
| 63 |
+
"required": [
|
| 64 |
+
"task_id",
|
| 65 |
+
"title",
|
| 66 |
+
"description",
|
| 67 |
+
"location_hint",
|
| 68 |
+
"points",
|
| 69 |
+
"time_limit_minutes",
|
| 70 |
+
"proof_type",
|
| 71 |
+
"hint",
|
| 72 |
+
"safety_note"
|
| 73 |
+
],
|
| 74 |
+
"properties": {
|
| 75 |
+
"task_id": {
|
| 76 |
+
"type": "string"
|
| 77 |
+
},
|
| 78 |
+
"title": {
|
| 79 |
+
"type": "string"
|
| 80 |
+
},
|
| 81 |
+
"description": {
|
| 82 |
+
"type": "string"
|
| 83 |
+
},
|
| 84 |
+
"location_hint": {
|
| 85 |
+
"type": "string"
|
| 86 |
+
},
|
| 87 |
+
"points": {
|
| 88 |
+
"type": "integer"
|
| 89 |
+
},
|
| 90 |
+
"time_limit_minutes": {
|
| 91 |
+
"oneOf": [
|
| 92 |
+
{
|
| 93 |
+
"type": "integer"
|
| 94 |
+
},
|
| 95 |
+
{
|
| 96 |
+
"type": "null"
|
| 97 |
+
}
|
| 98 |
+
]
|
| 99 |
+
},
|
| 100 |
+
"proof_type": {
|
| 101 |
+
"type": "string",
|
| 102 |
+
"enum": ["photo", "observation", "text"]
|
| 103 |
+
},
|
| 104 |
+
"hint": {
|
| 105 |
+
"type": "string"
|
| 106 |
+
},
|
| 107 |
+
"safety_note": {
|
| 108 |
+
"type": "string"
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
},
|
| 112 |
+
"description": "List of game tasks"
|
| 113 |
+
},
|
| 114 |
+
"global_hints": {
|
| 115 |
+
"type": "array",
|
| 116 |
+
"items": {
|
| 117 |
+
"type": "string"
|
| 118 |
+
}
|
| 119 |
+
},
|
| 120 |
+
"score_rules": {
|
| 121 |
+
"type": "array",
|
| 122 |
+
"items": {
|
| 123 |
+
"type": "string"
|
| 124 |
+
}
|
| 125 |
+
},
|
| 126 |
+
"tie_breaker": {
|
| 127 |
+
"type": "string"
|
| 128 |
+
},
|
| 129 |
+
"safety": {
|
| 130 |
+
"type": "object",
|
| 131 |
+
"required": [
|
| 132 |
+
"allowed_zone",
|
| 133 |
+
"forbidden_behaviors",
|
| 134 |
+
"adult_supervision",
|
| 135 |
+
"stop_conditions"
|
| 136 |
+
],
|
| 137 |
+
"properties": {
|
| 138 |
+
"allowed_zone": {
|
| 139 |
+
"type": "string"
|
| 140 |
+
},
|
| 141 |
+
"forbidden_behaviors": {
|
| 142 |
+
"type": "array",
|
| 143 |
+
"items": {
|
| 144 |
+
"type": "string"
|
| 145 |
+
}
|
| 146 |
+
},
|
| 147 |
+
"adult_supervision": {
|
| 148 |
+
"type": "boolean"
|
| 149 |
+
},
|
| 150 |
+
"stop_conditions": {
|
| 151 |
+
"type": "array",
|
| 152 |
+
"items": {
|
| 153 |
+
"type": "string"
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
},
|
| 158 |
+
"story_seed": {
|
| 159 |
+
"type": "object",
|
| 160 |
+
"required": ["tone", "motifs", "recap_style"],
|
| 161 |
+
"properties": {
|
| 162 |
+
"tone": {
|
| 163 |
+
"type": "string",
|
| 164 |
+
"enum": ["playful", "cinematic", "chaotic", "wholesome"]
|
| 165 |
+
},
|
| 166 |
+
"motifs": {
|
| 167 |
+
"type": "array",
|
| 168 |
+
"items": {
|
| 169 |
+
"type": "string"
|
| 170 |
+
}
|
| 171 |
+
},
|
| 172 |
+
"recap_style": {
|
| 173 |
+
"type": "string"
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
}
|
app/schemas/journal_schema.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"title": "Journal Entry Schema",
|
| 4 |
+
"type": "object",
|
| 5 |
+
"required": [
|
| 6 |
+
"journal_id",
|
| 7 |
+
"timestamp",
|
| 8 |
+
"session_id",
|
| 9 |
+
"team_id",
|
| 10 |
+
"transcript",
|
| 11 |
+
"mood",
|
| 12 |
+
"location_note"
|
| 13 |
+
],
|
| 14 |
+
"properties": {
|
| 15 |
+
"journal_id": {
|
| 16 |
+
"type": "string",
|
| 17 |
+
"description": "Unique journal entry identifier"
|
| 18 |
+
},
|
| 19 |
+
"timestamp": {
|
| 20 |
+
"type": "string",
|
| 21 |
+
"format": "date-time",
|
| 22 |
+
"description": "ISO-8601 timestamp"
|
| 23 |
+
},
|
| 24 |
+
"session_id": {
|
| 25 |
+
"type": "string",
|
| 26 |
+
"description": "Game session identifier"
|
| 27 |
+
},
|
| 28 |
+
"team_id": {
|
| 29 |
+
"type": "string",
|
| 30 |
+
"description": "Team identifier"
|
| 31 |
+
},
|
| 32 |
+
"task_id": {
|
| 33 |
+
"type": "string",
|
| 34 |
+
"description": "Optional associated task"
|
| 35 |
+
},
|
| 36 |
+
"transcript": {
|
| 37 |
+
"type": "string",
|
| 38 |
+
"description": "Transcribed voice note"
|
| 39 |
+
},
|
| 40 |
+
"mood": {
|
| 41 |
+
"type": "string",
|
| 42 |
+
"enum": ["funny", "confused", "excited", "tense", "lucky", "chaotic"],
|
| 43 |
+
"description": "Emotional tone of the journal entry"
|
| 44 |
+
},
|
| 45 |
+
"location_note": {
|
| 46 |
+
"type": "string",
|
| 47 |
+
"description": "Where the entry was recorded"
|
| 48 |
+
},
|
| 49 |
+
"photo_refs": {
|
| 50 |
+
"type": "array",
|
| 51 |
+
"items": {
|
| 52 |
+
"type": "string"
|
| 53 |
+
},
|
| 54 |
+
"description": "References to associated photos"
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
}
|
app/schemas/story_packet_schema.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"title": "Story Packet Schema",
|
| 4 |
+
"type": "object",
|
| 5 |
+
"required": [
|
| 6 |
+
"game_info",
|
| 7 |
+
"winner",
|
| 8 |
+
"final_scores",
|
| 9 |
+
"task_outcomes",
|
| 10 |
+
"journal_moments",
|
| 11 |
+
"photo_captions",
|
| 12 |
+
"notable_events",
|
| 13 |
+
"story_style"
|
| 14 |
+
],
|
| 15 |
+
"properties": {
|
| 16 |
+
"game_info": {
|
| 17 |
+
"type": "object",
|
| 18 |
+
"description": "Game metadata and setup"
|
| 19 |
+
},
|
| 20 |
+
"winner": {
|
| 21 |
+
"type": "string",
|
| 22 |
+
"description": "Winning team identifier"
|
| 23 |
+
},
|
| 24 |
+
"final_scores": {
|
| 25 |
+
"type": "array",
|
| 26 |
+
"items": {
|
| 27 |
+
"type": "object"
|
| 28 |
+
},
|
| 29 |
+
"description": "Final team scores"
|
| 30 |
+
},
|
| 31 |
+
"task_outcomes": {
|
| 32 |
+
"type": "array",
|
| 33 |
+
"items": {
|
| 34 |
+
"type": "object"
|
| 35 |
+
},
|
| 36 |
+
"description": "Outcomes of all tasks"
|
| 37 |
+
},
|
| 38 |
+
"journal_moments": {
|
| 39 |
+
"type": "array",
|
| 40 |
+
"items": {
|
| 41 |
+
"type": "object"
|
| 42 |
+
},
|
| 43 |
+
"description": "Selected journal entries for story"
|
| 44 |
+
},
|
| 45 |
+
"photo_captions": {
|
| 46 |
+
"type": "array",
|
| 47 |
+
"items": {
|
| 48 |
+
"type": "object"
|
| 49 |
+
},
|
| 50 |
+
"description": "Photos with captions"
|
| 51 |
+
},
|
| 52 |
+
"notable_events": {
|
| 53 |
+
"type": "array",
|
| 54 |
+
"items": {
|
| 55 |
+
"type": "object"
|
| 56 |
+
},
|
| 57 |
+
"description": "Significant gameplay moments"
|
| 58 |
+
},
|
| 59 |
+
"story_style": {
|
| 60 |
+
"type": "string",
|
| 61 |
+
"enum": ["episode_recap"],
|
| 62 |
+
"description": "Style of story to generate"
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
}
|
app/services/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""AI services for the game pipeline."""
|
app/services/generator.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Game generation module using Nemotron or similar models."""
|
| 2 |
+
|
| 3 |
+
from typing import Any
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def generate_game(config: dict, retrieved_examples: list[dict]) -> dict:
|
| 7 |
+
"""Generate a game from user config and retrieved examples.
|
| 8 |
+
|
| 9 |
+
Uses NVIDIA Nemotron Nano 4B as the primary generator.
|
| 10 |
+
|
| 11 |
+
Args:
|
| 12 |
+
config: Game configuration (game_type, city, duration, etc.)
|
| 13 |
+
retrieved_examples: List of similar example games for grounding
|
| 14 |
+
|
| 15 |
+
Returns:
|
| 16 |
+
Generated game JSON matching the game schema
|
| 17 |
+
"""
|
| 18 |
+
# TODO: Implement with Nemotron or mock for testing
|
| 19 |
+
mock_game = {
|
| 20 |
+
"game_id": "mock-001",
|
| 21 |
+
"title": "Mock Game",
|
| 22 |
+
"theme": "test",
|
| 23 |
+
"setup": config,
|
| 24 |
+
"rules": [],
|
| 25 |
+
"tasks": [],
|
| 26 |
+
"global_hints": [],
|
| 27 |
+
"score_rules": [],
|
| 28 |
+
"tie_breaker": "",
|
| 29 |
+
"safety": {
|
| 30 |
+
"allowed_zone": "",
|
| 31 |
+
"forbidden_behaviors": [],
|
| 32 |
+
"adult_supervision": False,
|
| 33 |
+
"stop_conditions": []
|
| 34 |
+
},
|
| 35 |
+
"story_seed": {
|
| 36 |
+
"tone": "playful",
|
| 37 |
+
"motifs": [],
|
| 38 |
+
"recap_style": "episode_recap"
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
return mock_game
|
app/services/journal.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Voice journal capture and summarization module."""
|
| 2 |
+
|
| 3 |
+
from typing import Any
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def transcribe_journal(audio_path: str) -> str:
|
| 7 |
+
"""Transcribe voice journal audio to text.
|
| 8 |
+
|
| 9 |
+
Args:
|
| 10 |
+
audio_path: Path to recorded audio file
|
| 11 |
+
|
| 12 |
+
Returns:
|
| 13 |
+
Transcribed text
|
| 14 |
+
"""
|
| 15 |
+
# TODO: Implement with speech-to-text service
|
| 16 |
+
return ""
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def summarize_journal(transcript: str, task_id: str | None = None) -> dict:
|
| 20 |
+
"""Summarize journal entry using OpenBMB model.
|
| 21 |
+
|
| 22 |
+
Args:
|
| 23 |
+
transcript: Journal transcript text
|
| 24 |
+
task_id: Optional associated task ID
|
| 25 |
+
|
| 26 |
+
Returns:
|
| 27 |
+
Journal summary with tags and story value
|
| 28 |
+
"""
|
| 29 |
+
# TODO: Implement with MiniCPM or similar model
|
| 30 |
+
return {
|
| 31 |
+
"moment_summary": "",
|
| 32 |
+
"tags": [],
|
| 33 |
+
"story_value": "low"
|
| 34 |
+
}
|
app/services/retrieval.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Retrieval grounding module for fetching similar game examples."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def load_games_dataset(path: str) -> list[dict]:
|
| 9 |
+
"""Load the games dataset from JSON file.
|
| 10 |
+
|
| 11 |
+
Args:
|
| 12 |
+
path: Path to games_dataset.json
|
| 13 |
+
|
| 14 |
+
Returns:
|
| 15 |
+
List of game records
|
| 16 |
+
"""
|
| 17 |
+
with open(path, 'r', encoding='utf-8') as f:
|
| 18 |
+
return json.load(f)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def normalize_game_record(record: dict) -> dict:
|
| 22 |
+
"""Normalize a raw game record into structured format.
|
| 23 |
+
|
| 24 |
+
Extracts features from the nested dataset structure and creates
|
| 25 |
+
a flat normalized record suitable for retrieval and logging.
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
record: Raw game record from dataset
|
| 29 |
+
|
| 30 |
+
Returns:
|
| 31 |
+
Normalized game record with extracted features
|
| 32 |
+
"""
|
| 33 |
+
# Extract input fields
|
| 34 |
+
input_data = record.get('input', {})
|
| 35 |
+
game_type = input_data.get('game_type', '')
|
| 36 |
+
location = input_data.get('location', {})
|
| 37 |
+
preferences = input_data.get('preferences', {})
|
| 38 |
+
|
| 39 |
+
# Extract output fields
|
| 40 |
+
output_data = record.get('expected_output', {})
|
| 41 |
+
tasks = output_data.get('tasks', [])
|
| 42 |
+
rules = output_data.get('rules', [])
|
| 43 |
+
hints = output_data.get('hints', [])
|
| 44 |
+
safety_flags = output_data.get('safety_flags', {})
|
| 45 |
+
|
| 46 |
+
# Extract metadata
|
| 47 |
+
metadata = record.get('metadata', {})
|
| 48 |
+
|
| 49 |
+
# Build normalized record
|
| 50 |
+
normalized = {
|
| 51 |
+
'id': record.get('id'),
|
| 52 |
+
'game_type': game_type,
|
| 53 |
+
'city': location.get('city', ''),
|
| 54 |
+
'area': location.get('area', ''),
|
| 55 |
+
'location_type': location.get('location_type', ''),
|
| 56 |
+
'duration_minutes': preferences.get('duration_minutes'),
|
| 57 |
+
'num_players': preferences.get('num_players'),
|
| 58 |
+
'difficulty': preferences.get('difficulty', ''),
|
| 59 |
+
'age_group': preferences.get('age_group', ''),
|
| 60 |
+
'num_tasks': len(tasks),
|
| 61 |
+
'task_ids': [t.get('task_id') for t in tasks],
|
| 62 |
+
'num_rules': len(rules),
|
| 63 |
+
'num_hints': len(hints),
|
| 64 |
+
'is_safe': safety_flags.get('is_safe', True),
|
| 65 |
+
'safety_flags_list': safety_flags.get('flags', []),
|
| 66 |
+
'quality_score': metadata.get('quality_score'),
|
| 67 |
+
'source': metadata.get('source'),
|
| 68 |
+
'notes': metadata.get('notes', ''),
|
| 69 |
+
# Store full objects for reference
|
| 70 |
+
'rules': rules,
|
| 71 |
+
'tasks': tasks,
|
| 72 |
+
'hints': hints,
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
return normalized
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def retrieve_examples(config: dict, dataset: list[dict], k: int = 5) -> list[dict]:
|
| 79 |
+
"""Retrieve top k closest examples from dataset.
|
| 80 |
+
|
| 81 |
+
Uses game type, duration, age group, difficulty, location type,
|
| 82 |
+
and area similarity for retrieval.
|
| 83 |
+
|
| 84 |
+
Args:
|
| 85 |
+
config: Game configuration from user input
|
| 86 |
+
dataset: Loaded games dataset
|
| 87 |
+
k: Number of examples to retrieve
|
| 88 |
+
|
| 89 |
+
Returns:
|
| 90 |
+
List of retrieved example games (compressed exemplar bundles)
|
| 91 |
+
"""
|
| 92 |
+
if not dataset:
|
| 93 |
+
return []
|
| 94 |
+
|
| 95 |
+
# Scoring function for retrieval
|
| 96 |
+
def compute_similarity_score(config: dict, record: dict) -> float:
|
| 97 |
+
"""Compute similarity score between config and record."""
|
| 98 |
+
score = 0.0
|
| 99 |
+
|
| 100 |
+
# Game type exact match (highest weight)
|
| 101 |
+
if config.get('game_type', '').lower() == record.get('game_type', '').lower():
|
| 102 |
+
score += 50
|
| 103 |
+
|
| 104 |
+
# Age group match (high weight)
|
| 105 |
+
config_age = config.get('age_group', '').lower()
|
| 106 |
+
record_age = record.get('age_group', '').lower()
|
| 107 |
+
if config_age and record_age:
|
| 108 |
+
# Exact match
|
| 109 |
+
if config_age == record_age:
|
| 110 |
+
score += 25
|
| 111 |
+
# Mixed age groups can match with any specific age
|
| 112 |
+
elif 'mixed' in [config_age, record_age]:
|
| 113 |
+
score += 15
|
| 114 |
+
|
| 115 |
+
# Difficulty match (medium weight)
|
| 116 |
+
if config.get('difficulty', '').lower() == record.get('difficulty', '').lower():
|
| 117 |
+
score += 20
|
| 118 |
+
|
| 119 |
+
# Location type match (medium weight)
|
| 120 |
+
if config.get('location_type', '').lower() == record.get('location_type', '').lower():
|
| 121 |
+
score += 15
|
| 122 |
+
|
| 123 |
+
# Duration proximity (lower weight, prefer closer durations)
|
| 124 |
+
config_duration = config.get('duration_minutes')
|
| 125 |
+
record_duration = record.get('duration_minutes')
|
| 126 |
+
if config_duration and record_duration:
|
| 127 |
+
duration_diff = abs(config_duration - record_duration)
|
| 128 |
+
# Prefer matches within 15 minutes
|
| 129 |
+
if duration_diff <= 15:
|
| 130 |
+
score += 10
|
| 131 |
+
elif duration_diff <= 30:
|
| 132 |
+
score += 5
|
| 133 |
+
|
| 134 |
+
# Area name similarity (lightweight fuzzy matching)
|
| 135 |
+
config_area = config.get('area', '').lower()
|
| 136 |
+
record_area = record.get('area', '').lower()
|
| 137 |
+
if config_area and record_area:
|
| 138 |
+
# Exact area match
|
| 139 |
+
if config_area == record_area:
|
| 140 |
+
score += 10
|
| 141 |
+
# Partial area match (e.g., "Parc" matches park names)
|
| 142 |
+
elif any(word in record_area for word in config_area.split()):
|
| 143 |
+
score += 3
|
| 144 |
+
|
| 145 |
+
# Quality bonus (prefer higher quality examples)
|
| 146 |
+
quality = record.get('quality_score', 0)
|
| 147 |
+
if quality:
|
| 148 |
+
score += quality * 0.5
|
| 149 |
+
|
| 150 |
+
# Safety bonus (prefer safe games)
|
| 151 |
+
if record.get('is_safe', True):
|
| 152 |
+
score += 5
|
| 153 |
+
|
| 154 |
+
return score
|
| 155 |
+
|
| 156 |
+
# Score all records
|
| 157 |
+
scored_records = []
|
| 158 |
+
for record in dataset:
|
| 159 |
+
score = compute_similarity_score(config, record)
|
| 160 |
+
scored_records.append({
|
| 161 |
+
'record': record,
|
| 162 |
+
'score': score
|
| 163 |
+
})
|
| 164 |
+
|
| 165 |
+
# Sort by score descending
|
| 166 |
+
scored_records.sort(key=lambda x: x['score'], reverse=True)
|
| 167 |
+
|
| 168 |
+
# Extract top k and compress into exemplar bundles
|
| 169 |
+
top_k = scored_records[:k]
|
| 170 |
+
retrieved = []
|
| 171 |
+
|
| 172 |
+
for item in top_k:
|
| 173 |
+
record = item['record']
|
| 174 |
+
# Create compressed exemplar bundle
|
| 175 |
+
exemplar = {
|
| 176 |
+
'id': record['id'],
|
| 177 |
+
'game_type': record['game_type'],
|
| 178 |
+
'area': record['area'],
|
| 179 |
+
'city': record['city'],
|
| 180 |
+
'difficulty': record['difficulty'],
|
| 181 |
+
'age_group': record['age_group'],
|
| 182 |
+
'duration_minutes': record['duration_minutes'],
|
| 183 |
+
'location_type': record['location_type'],
|
| 184 |
+
'rules_summary': record.get('rules', [])[:3], # Top 3 rules
|
| 185 |
+
'task_patterns': [
|
| 186 |
+
{
|
| 187 |
+
'task_id': t.get('task_id'),
|
| 188 |
+
'proof_type': t.get('proof_type') if 'proof_type' in t else 'observation',
|
| 189 |
+
'points': t.get('points'),
|
| 190 |
+
'time_limit': t.get('time_limit_minutes')
|
| 191 |
+
}
|
| 192 |
+
for t in record.get('tasks', [])[:3] # Top 3 tasks
|
| 193 |
+
],
|
| 194 |
+
'safety_patterns': record.get('safety_flags_list', []),
|
| 195 |
+
'quality_score': record.get('quality_score'),
|
| 196 |
+
'retrieval_score': item['score'],
|
| 197 |
+
}
|
| 198 |
+
retrieved.append(exemplar)
|
| 199 |
+
|
| 200 |
+
return retrieved
|
app/services/schema_validator.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Schema validation utilities for the game pipeline."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import jsonschema
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any, Tuple
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def load_schema(schema_name: str = "game_schema.json") -> dict:
|
| 10 |
+
"""Load a JSON schema from the schemas directory.
|
| 11 |
+
|
| 12 |
+
Args:
|
| 13 |
+
schema_name: Name of the schema file (default: game_schema.json)
|
| 14 |
+
|
| 15 |
+
Returns:
|
| 16 |
+
Loaded schema as dict
|
| 17 |
+
"""
|
| 18 |
+
schema_path = Path("app/schemas") / schema_name
|
| 19 |
+
with open(schema_path, 'r', encoding='utf-8') as f:
|
| 20 |
+
return json.load(f)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def validate_game_schema(game: dict) -> Tuple[bool, list[str]]:
|
| 24 |
+
"""Validate a game JSON against the game schema.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
game: Game data to validate
|
| 28 |
+
|
| 29 |
+
Returns:
|
| 30 |
+
Tuple of (is_valid, list of error messages)
|
| 31 |
+
"""
|
| 32 |
+
schema = load_schema("game_schema.json")
|
| 33 |
+
errors = []
|
| 34 |
+
|
| 35 |
+
try:
|
| 36 |
+
jsonschema.validate(instance=game, schema=schema)
|
| 37 |
+
return True, []
|
| 38 |
+
except jsonschema.ValidationError as e:
|
| 39 |
+
errors.append(f"Validation error: {e.message}")
|
| 40 |
+
errors.append(f"Path: {'.'.join(str(p) for p in e.absolute_path)}")
|
| 41 |
+
return False, errors
|
| 42 |
+
except jsonschema.SchemaError as e:
|
| 43 |
+
errors.append(f"Schema error: {e.message}")
|
| 44 |
+
return False, errors
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def validate_task_structure(task: dict) -> Tuple[bool, list[str]]:
|
| 48 |
+
"""Validate a single task object.
|
| 49 |
+
|
| 50 |
+
Args:
|
| 51 |
+
task: Task data to validate
|
| 52 |
+
|
| 53 |
+
Returns:
|
| 54 |
+
Tuple of (is_valid, list of error messages)
|
| 55 |
+
"""
|
| 56 |
+
required_fields = [
|
| 57 |
+
'task_id', 'title', 'description', 'location_hint',
|
| 58 |
+
'points', 'time_limit_minutes', 'proof_type', 'hint', 'safety_note'
|
| 59 |
+
]
|
| 60 |
+
|
| 61 |
+
errors = []
|
| 62 |
+
for field in required_fields:
|
| 63 |
+
if field not in task:
|
| 64 |
+
errors.append(f"Missing required field: {field}")
|
| 65 |
+
|
| 66 |
+
# Validate proof_type enum
|
| 67 |
+
if 'proof_type' in task:
|
| 68 |
+
valid_types = ['photo', 'observation', 'text']
|
| 69 |
+
if task['proof_type'] not in valid_types:
|
| 70 |
+
errors.append(f"Invalid proof_type: {task['proof_type']}. Must be one of {valid_types}")
|
| 71 |
+
|
| 72 |
+
# Validate points and time_limit are positive
|
| 73 |
+
if 'points' in task and task['points'] < 0:
|
| 74 |
+
errors.append(f"Task points must be non-negative, got {task['points']}")
|
| 75 |
+
|
| 76 |
+
if 'time_limit_minutes' in task and task['time_limit_minutes'] is not None:
|
| 77 |
+
if task['time_limit_minutes'] < 0:
|
| 78 |
+
errors.append(f"Task time_limit_minutes must be non-negative, got {task['time_limit_minutes']}")
|
| 79 |
+
|
| 80 |
+
return len(errors) == 0, errors
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def validate_safety_structure(safety: dict) -> Tuple[bool, list[str]]:
|
| 84 |
+
"""Validate the safety object structure.
|
| 85 |
+
|
| 86 |
+
Args:
|
| 87 |
+
safety: Safety data to validate
|
| 88 |
+
|
| 89 |
+
Returns:
|
| 90 |
+
Tuple of (is_valid, list of error messages)
|
| 91 |
+
"""
|
| 92 |
+
required_fields = ['allowed_zone', 'forbidden_behaviors', 'adult_supervision', 'stop_conditions']
|
| 93 |
+
errors = []
|
| 94 |
+
|
| 95 |
+
for field in required_fields:
|
| 96 |
+
if field not in safety:
|
| 97 |
+
errors.append(f"Missing required safety field: {field}")
|
| 98 |
+
|
| 99 |
+
# Validate field types
|
| 100 |
+
if 'forbidden_behaviors' in safety and not isinstance(safety['forbidden_behaviors'], list):
|
| 101 |
+
errors.append("forbidden_behaviors must be an array")
|
| 102 |
+
|
| 103 |
+
if 'stop_conditions' in safety and not isinstance(safety['stop_conditions'], list):
|
| 104 |
+
errors.append("stop_conditions must be an array")
|
| 105 |
+
|
| 106 |
+
if 'adult_supervision' in safety and not isinstance(safety['adult_supervision'], bool):
|
| 107 |
+
errors.append("adult_supervision must be a boolean")
|
| 108 |
+
|
| 109 |
+
return len(errors) == 0, errors
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def create_minimal_game_template() -> dict:
|
| 113 |
+
"""Create a minimal valid game template for testing.
|
| 114 |
+
|
| 115 |
+
Returns:
|
| 116 |
+
Minimal valid game JSON matching the schema
|
| 117 |
+
"""
|
| 118 |
+
return {
|
| 119 |
+
"game_id": "test-game-001",
|
| 120 |
+
"title": "Test Game",
|
| 121 |
+
"theme": "test",
|
| 122 |
+
"setup": {
|
| 123 |
+
"city": "Paris",
|
| 124 |
+
"area": "Test Area",
|
| 125 |
+
"meeting_point": "Central location",
|
| 126 |
+
"duration_minutes": 45,
|
| 127 |
+
"num_players": 4
|
| 128 |
+
},
|
| 129 |
+
"rules": [
|
| 130 |
+
"Rule 1",
|
| 131 |
+
"Rule 2"
|
| 132 |
+
],
|
| 133 |
+
"tasks": [
|
| 134 |
+
{
|
| 135 |
+
"task_id": "t1",
|
| 136 |
+
"title": "Task 1",
|
| 137 |
+
"description": "Find something",
|
| 138 |
+
"location_hint": "Look near the entrance",
|
| 139 |
+
"points": 20,
|
| 140 |
+
"time_limit_minutes": 10,
|
| 141 |
+
"proof_type": "photo",
|
| 142 |
+
"hint": "It's visible from the street",
|
| 143 |
+
"safety_note": "Stay on public paths"
|
| 144 |
+
}
|
| 145 |
+
],
|
| 146 |
+
"global_hints": [
|
| 147 |
+
"Explore systematically"
|
| 148 |
+
],
|
| 149 |
+
"score_rules": [
|
| 150 |
+
"1 point per second under time limit",
|
| 151 |
+
"No penalty for hints"
|
| 152 |
+
],
|
| 153 |
+
"tie_breaker": "Team with most tasks completed first",
|
| 154 |
+
"safety": {
|
| 155 |
+
"allowed_zone": "Public streets and parks in the designated area",
|
| 156 |
+
"forbidden_behaviors": [
|
| 157 |
+
"Entering private buildings",
|
| 158 |
+
"Crossing major roads unsafely"
|
| 159 |
+
],
|
| 160 |
+
"adult_supervision": False,
|
| 161 |
+
"stop_conditions": [
|
| 162 |
+
"Player injury",
|
| 163 |
+
"Weather emergency"
|
| 164 |
+
]
|
| 165 |
+
},
|
| 166 |
+
"story_seed": {
|
| 167 |
+
"tone": "playful",
|
| 168 |
+
"motifs": [
|
| 169 |
+
"discovery",
|
| 170 |
+
"teamwork"
|
| 171 |
+
],
|
| 172 |
+
"recap_style": "episode_recap"
|
| 173 |
+
}
|
| 174 |
+
}
|
app/services/scoring.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Deterministic scoring module."""
|
| 2 |
+
|
| 3 |
+
from typing import Any
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def compute_scores(events: list[dict], game: dict) -> dict:
|
| 7 |
+
"""Compute final scores from gameplay events.
|
| 8 |
+
|
| 9 |
+
Args:
|
| 10 |
+
events: List of gameplay events
|
| 11 |
+
game: The original game definition
|
| 12 |
+
|
| 13 |
+
Returns:
|
| 14 |
+
Scoring output with team scores and winner
|
| 15 |
+
"""
|
| 16 |
+
# TODO: Implement deterministic scoring
|
| 17 |
+
return {
|
| 18 |
+
"team_scores": [],
|
| 19 |
+
"winner": None,
|
| 20 |
+
"scoring_explanation": []
|
| 21 |
+
}
|
app/services/story.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Final story and recap generation module."""
|
| 2 |
+
|
| 3 |
+
from typing import Any
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def generate_story(story_packet: dict) -> dict:
|
| 7 |
+
"""Generate final recap story from game data and events.
|
| 8 |
+
|
| 9 |
+
Uses OpenBMB MiniCPM5-1B or similar model.
|
| 10 |
+
|
| 11 |
+
Args:
|
| 12 |
+
story_packet: Structured packet with game info, scores, journals, photos
|
| 13 |
+
|
| 14 |
+
Returns:
|
| 15 |
+
Story output with short recap, long-form summary, and poster prompt
|
| 16 |
+
"""
|
| 17 |
+
# TODO: Implement with OpenBMB model
|
| 18 |
+
return {
|
| 19 |
+
"short_recap": "",
|
| 20 |
+
"long_summary": "",
|
| 21 |
+
"poster_prompt": ""
|
| 22 |
+
}
|
app/services/tracing.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Logging and tracing module for pipeline transparency."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def log_event(session_id: str, event_type: str, payload: dict, log_dir: str = "app/logs") -> None:
|
| 9 |
+
"""Log a gameplay event to JSONL file.
|
| 10 |
+
|
| 11 |
+
Args:
|
| 12 |
+
session_id: Session identifier
|
| 13 |
+
event_type: Type of event (task_revealed, completed, etc.)
|
| 14 |
+
payload: Event data
|
| 15 |
+
log_dir: Directory to store logs
|
| 16 |
+
"""
|
| 17 |
+
# TODO: Implement JSONL logging
|
| 18 |
+
pass
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def save_trace(trace_data: dict, output_path: str) -> None:
|
| 22 |
+
"""Save a complete pipeline trace for debugging and publication.
|
| 23 |
+
|
| 24 |
+
Args:
|
| 25 |
+
trace_data: Dictionary containing all pipeline data
|
| 26 |
+
output_path: Where to save the trace file
|
| 27 |
+
"""
|
| 28 |
+
# TODO: Implement trace saving
|
| 29 |
+
pass
|
app/services/validator.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Game validation and repair module."""
|
| 2 |
+
|
| 3 |
+
from typing import Any
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def validate_game(game: dict, config: dict) -> tuple[bool, list[str]]:
|
| 7 |
+
"""Validate generated game against hard rules.
|
| 8 |
+
|
| 9 |
+
Args:
|
| 10 |
+
game: Generated game JSON
|
| 11 |
+
config: Original game configuration
|
| 12 |
+
|
| 13 |
+
Returns:
|
| 14 |
+
Tuple of (is_valid, list of failure messages)
|
| 15 |
+
"""
|
| 16 |
+
failures = []
|
| 17 |
+
|
| 18 |
+
# Hard validation rules
|
| 19 |
+
hard_checks = [
|
| 20 |
+
("title", lambda g: g.get("title")),
|
| 21 |
+
("theme", lambda g: g.get("theme")),
|
| 22 |
+
("setup", lambda g: g.get("setup")),
|
| 23 |
+
("rules", lambda g: isinstance(g.get("rules"), list) and len(g.get("rules", [])) > 0),
|
| 24 |
+
("tasks", lambda g: isinstance(g.get("tasks"), list) and len(g.get("tasks", [])) > 0),
|
| 25 |
+
("no_buildings", lambda g: not any("building" in str(t).lower() for t in g.get("tasks", []))),
|
| 26 |
+
("no_private_areas", lambda g: not any("private" in str(t).lower() for t in g.get("tasks", []))),
|
| 27 |
+
("global_hints", lambda g: isinstance(g.get("global_hints"), list)),
|
| 28 |
+
("score_rules", lambda g: isinstance(g.get("score_rules"), list)),
|
| 29 |
+
("safety", lambda g: g.get("safety")),
|
| 30 |
+
]
|
| 31 |
+
|
| 32 |
+
for check_name, check_fn in hard_checks:
|
| 33 |
+
try:
|
| 34 |
+
if not check_fn(game):
|
| 35 |
+
failures.append(f"Failed check: {check_name}")
|
| 36 |
+
except Exception as e:
|
| 37 |
+
failures.append(f"Error in {check_name}: {str(e)}")
|
| 38 |
+
|
| 39 |
+
return len(failures) == 0, failures
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def repair_game(game: dict, failures: list[str], config: dict) -> dict:
|
| 43 |
+
"""Repair a game that failed validation.
|
| 44 |
+
|
| 45 |
+
Args:
|
| 46 |
+
game: Failed game JSON
|
| 47 |
+
failures: List of validation failures
|
| 48 |
+
config: Original game configuration
|
| 49 |
+
|
| 50 |
+
Returns:
|
| 51 |
+
Repaired game JSON
|
| 52 |
+
"""
|
| 53 |
+
# TODO: Implement repair logic with minimal modifications
|
| 54 |
+
return game
|
inspect_dataset.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Inspect and verify the games dataset loader and normalizer.
|
| 3 |
+
|
| 4 |
+
Run this script to:
|
| 5 |
+
1. Load the dataset
|
| 6 |
+
2. Normalize records
|
| 7 |
+
3. Print one normalized record per game type
|
| 8 |
+
4. Verify schema consistency
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import json
|
| 12 |
+
import sys
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
from app.services.retrieval import load_games_dataset, normalize_game_record
|
| 15 |
+
|
| 16 |
+
def main():
|
| 17 |
+
# Load dataset
|
| 18 |
+
dataset_path = Path("app/data/games_dataset.json")
|
| 19 |
+
print(f"Loading dataset from: {dataset_path}")
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
raw_records = load_games_dataset(str(dataset_path))
|
| 23 |
+
except FileNotFoundError:
|
| 24 |
+
print(f"ERROR: Dataset not found at {dataset_path}")
|
| 25 |
+
sys.exit(1)
|
| 26 |
+
|
| 27 |
+
print(f"\nβ Loaded {len(raw_records)} game records\n")
|
| 28 |
+
|
| 29 |
+
# Normalize and group by game type
|
| 30 |
+
normalized_records = []
|
| 31 |
+
game_types_seen = set()
|
| 32 |
+
|
| 33 |
+
for record in raw_records:
|
| 34 |
+
try:
|
| 35 |
+
normalized = normalize_game_record(record)
|
| 36 |
+
normalized_records.append(normalized)
|
| 37 |
+
except Exception as e:
|
| 38 |
+
print(f"ERROR normalizing record {record.get('id')}: {e}")
|
| 39 |
+
continue
|
| 40 |
+
|
| 41 |
+
print(f"β Normalized {len(normalized_records)} records\n")
|
| 42 |
+
|
| 43 |
+
# Print summary
|
| 44 |
+
print("=" * 80)
|
| 45 |
+
print("DATASET SUMMARY")
|
| 46 |
+
print("=" * 80)
|
| 47 |
+
|
| 48 |
+
game_types = {}
|
| 49 |
+
difficulties = set()
|
| 50 |
+
age_groups = set()
|
| 51 |
+
locations = set()
|
| 52 |
+
|
| 53 |
+
for norm in normalized_records:
|
| 54 |
+
gt = norm['game_type']
|
| 55 |
+
game_types[gt] = game_types.get(gt, 0) + 1
|
| 56 |
+
difficulties.add(norm['difficulty'])
|
| 57 |
+
age_groups.add(norm['age_group'])
|
| 58 |
+
locations.add((norm['city'], norm['area']))
|
| 59 |
+
|
| 60 |
+
print(f"\nGame Types: {dict(game_types)}")
|
| 61 |
+
print(f"Difficulties: {sorted(difficulties)}")
|
| 62 |
+
print(f"Age Groups: {sorted(age_groups)}")
|
| 63 |
+
print(f"Unique Locations: {len(locations)}")
|
| 64 |
+
for city, area in sorted(locations):
|
| 65 |
+
print(f" - {city}: {area}")
|
| 66 |
+
|
| 67 |
+
# Print one example per game type
|
| 68 |
+
print("\n" + "=" * 80)
|
| 69 |
+
print("SAMPLE NORMALIZED RECORDS (one per game type)")
|
| 70 |
+
print("=" * 80)
|
| 71 |
+
|
| 72 |
+
printed = set()
|
| 73 |
+
for norm in normalized_records:
|
| 74 |
+
gt = norm['game_type']
|
| 75 |
+
if gt not in printed:
|
| 76 |
+
print(f"\n--- {gt.upper()} (ID: {norm['id']}) ---")
|
| 77 |
+
print(f"City: {norm['city']}")
|
| 78 |
+
print(f"Area: {norm['area']}")
|
| 79 |
+
print(f"Duration: {norm['duration_minutes']} min | Players: {norm['num_players']}")
|
| 80 |
+
print(f"Difficulty: {norm['difficulty']} | Age Group: {norm['age_group']}")
|
| 81 |
+
print(f"Tasks: {norm['num_tasks']} | Rules: {norm['num_rules']} | Hints: {norm['num_hints']}")
|
| 82 |
+
print(f"Location Type: {norm['location_type']}")
|
| 83 |
+
print(f"Safety: {norm['is_safe']} | Quality Score: {norm['quality_score']}")
|
| 84 |
+
print(f"Notes: {norm['notes']}")
|
| 85 |
+
|
| 86 |
+
# Print first task as example
|
| 87 |
+
if norm['tasks']:
|
| 88 |
+
task = norm['tasks'][0]
|
| 89 |
+
print(f"\nFirst Task Example:")
|
| 90 |
+
print(f" Task ID: {task.get('task_id')}")
|
| 91 |
+
print(f" Description: {task.get('description')[:80]}...")
|
| 92 |
+
print(f" Points: {task.get('points')} | Time: {task.get('time_limit_minutes')} min")
|
| 93 |
+
|
| 94 |
+
printed.add(gt)
|
| 95 |
+
|
| 96 |
+
# Schema validation
|
| 97 |
+
print("\n" + "=" * 80)
|
| 98 |
+
print("SCHEMA VALIDATION")
|
| 99 |
+
print("=" * 80)
|
| 100 |
+
|
| 101 |
+
required_fields = [
|
| 102 |
+
'id', 'game_type', 'city', 'area', 'location_type',
|
| 103 |
+
'duration_minutes', 'num_players', 'difficulty', 'age_group',
|
| 104 |
+
'num_tasks', 'task_ids', 'num_rules', 'num_hints',
|
| 105 |
+
'is_safe', 'quality_score'
|
| 106 |
+
]
|
| 107 |
+
|
| 108 |
+
all_valid = True
|
| 109 |
+
for norm in normalized_records:
|
| 110 |
+
for field in required_fields:
|
| 111 |
+
if field not in norm:
|
| 112 |
+
print(f"β Record {norm.get('id')} missing field: {field}")
|
| 113 |
+
all_valid = False
|
| 114 |
+
|
| 115 |
+
if all_valid:
|
| 116 |
+
print("β All records have required fields")
|
| 117 |
+
|
| 118 |
+
print("\n" + "=" * 80)
|
| 119 |
+
print("INSPECTION COMPLETE")
|
| 120 |
+
print("=" * 80)
|
| 121 |
+
|
| 122 |
+
if __name__ == "__main__":
|
| 123 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio
|
| 2 |
+
torch
|
test_retrieval.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Test and demonstrate the retrieval system.
|
| 3 |
+
|
| 4 |
+
Run this script to:
|
| 5 |
+
1. Load and normalize the dataset
|
| 6 |
+
2. Test retrieval with various config examples
|
| 7 |
+
3. Display retrieved results with similarity scores
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import json
|
| 11 |
+
from app.services.retrieval import load_games_dataset, normalize_game_record, retrieve_examples
|
| 12 |
+
|
| 13 |
+
def main():
|
| 14 |
+
# Load and normalize dataset
|
| 15 |
+
print("Loading and normalizing dataset...")
|
| 16 |
+
raw_records = load_games_dataset("app/data/games_dataset.json")
|
| 17 |
+
normalized_records = [normalize_game_record(r) for r in raw_records]
|
| 18 |
+
print(f"β Loaded {len(normalized_records)} normalized records\n")
|
| 19 |
+
|
| 20 |
+
# Test cases: different user configurations
|
| 21 |
+
test_configs = [
|
| 22 |
+
{
|
| 23 |
+
"name": "Scavenger Hunt - Adults - Medium",
|
| 24 |
+
"config": {
|
| 25 |
+
"game_type": "scavenger_hunt",
|
| 26 |
+
"city": "Paris",
|
| 27 |
+
"area": "free text",
|
| 28 |
+
"location_type": "mixed",
|
| 29 |
+
"duration_minutes": 60,
|
| 30 |
+
"num_players": 4,
|
| 31 |
+
"difficulty": "medium",
|
| 32 |
+
"age_group": "adults",
|
| 33 |
+
"energy_level": "medium",
|
| 34 |
+
"photo_enabled": True
|
| 35 |
+
}
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"name": "Hide and Seek - Kids - Easy",
|
| 39 |
+
"config": {
|
| 40 |
+
"game_type": "hide_and_seek",
|
| 41 |
+
"city": "Paris",
|
| 42 |
+
"area": "park area",
|
| 43 |
+
"location_type": "park",
|
| 44 |
+
"duration_minutes": 45,
|
| 45 |
+
"num_players": 5,
|
| 46 |
+
"difficulty": "easy",
|
| 47 |
+
"age_group": "kids",
|
| 48 |
+
"energy_level": "high",
|
| 49 |
+
"photo_enabled": False
|
| 50 |
+
}
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
"name": "Tag - Teens - Hard",
|
| 54 |
+
"config": {
|
| 55 |
+
"game_type": "tag",
|
| 56 |
+
"city": "Paris",
|
| 57 |
+
"area": "outdoor spaces",
|
| 58 |
+
"location_type": "mixed",
|
| 59 |
+
"duration_minutes": 30,
|
| 60 |
+
"num_players": 8,
|
| 61 |
+
"difficulty": "hard",
|
| 62 |
+
"age_group": "teens",
|
| 63 |
+
"energy_level": "high",
|
| 64 |
+
"photo_enabled": False
|
| 65 |
+
}
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"name": "Mixed Age - 90 minutes - Medium",
|
| 69 |
+
"config": {
|
| 70 |
+
"game_type": "scavenger_hunt",
|
| 71 |
+
"city": "Paris",
|
| 72 |
+
"area": "outdoor",
|
| 73 |
+
"location_type": "mixed",
|
| 74 |
+
"duration_minutes": 90,
|
| 75 |
+
"num_players": 6,
|
| 76 |
+
"difficulty": "medium",
|
| 77 |
+
"age_group": "mixed",
|
| 78 |
+
"energy_level": "medium",
|
| 79 |
+
"photo_enabled": True
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
]
|
| 83 |
+
|
| 84 |
+
# Run retrieval tests
|
| 85 |
+
for test in test_configs:
|
| 86 |
+
print("=" * 80)
|
| 87 |
+
print(f"TEST: {test['name']}")
|
| 88 |
+
print("=" * 80)
|
| 89 |
+
config = test['config']
|
| 90 |
+
print(f"Query Config:")
|
| 91 |
+
print(f" Game Type: {config['game_type']}")
|
| 92 |
+
print(f" Duration: {config['duration_minutes']} min | Players: {config['num_players']}")
|
| 93 |
+
print(f" Difficulty: {config['difficulty']} | Age Group: {config['age_group']}")
|
| 94 |
+
print(f" Location Type: {config['location_type']}")
|
| 95 |
+
|
| 96 |
+
# Retrieve top 5 examples
|
| 97 |
+
retrieved = retrieve_examples(config, normalized_records, k=5)
|
| 98 |
+
|
| 99 |
+
print(f"\nTop 5 Retrieved Examples:")
|
| 100 |
+
print("-" * 80)
|
| 101 |
+
|
| 102 |
+
for i, example in enumerate(retrieved, 1):
|
| 103 |
+
print(f"\n{i}. {example['id']} (Score: {example['retrieval_score']:.1f})")
|
| 104 |
+
print(f" Game Type: {example['game_type']}")
|
| 105 |
+
print(f" Area: {example['area']}")
|
| 106 |
+
print(f" Duration: {example['duration_minutes']} min | Difficulty: {example['difficulty']}")
|
| 107 |
+
print(f" Age Group: {example['age_group']} | Quality: {example['quality_score']}/5")
|
| 108 |
+
print(f" Rules: {len(example['rules_summary'])} examples")
|
| 109 |
+
if example['rules_summary']:
|
| 110 |
+
print(f" β’ {example['rules_summary'][0][:70]}...")
|
| 111 |
+
print(f" Tasks: {len(example['task_patterns'])} patterns")
|
| 112 |
+
for task in example['task_patterns'][:2]:
|
| 113 |
+
print(f" β’ {task['task_id']}: {task['points']} pts ({task['proof_type']})")
|
| 114 |
+
if example['safety_patterns']:
|
| 115 |
+
print(f" Safety Flags: {example['safety_patterns']}")
|
| 116 |
+
|
| 117 |
+
print("\n")
|
| 118 |
+
|
| 119 |
+
# Demonstrate retrieval output format
|
| 120 |
+
print("=" * 80)
|
| 121 |
+
print("EXEMPLAR BUNDLE OUTPUT FORMAT")
|
| 122 |
+
print("=" * 80)
|
| 123 |
+
|
| 124 |
+
sample_config = test_configs[0]["config"]
|
| 125 |
+
sample_retrieved = retrieve_examples(sample_config, normalized_records, k=2)
|
| 126 |
+
|
| 127 |
+
print("\nJSON Output (first 2 results):")
|
| 128 |
+
print(json.dumps(sample_retrieved, indent=2))
|
| 129 |
+
|
| 130 |
+
print("\n" + "=" * 80)
|
| 131 |
+
print("RETRIEVAL TESTS COMPLETE")
|
| 132 |
+
print("=" * 80)
|
| 133 |
+
|
| 134 |
+
if __name__ == "__main__":
|
| 135 |
+
main()
|
test_schema.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Test the game JSON schema and validation utilities.
|
| 3 |
+
|
| 4 |
+
Run this script to:
|
| 5 |
+
1. Test the schema against example games from the dataset
|
| 6 |
+
2. Validate schema structure
|
| 7 |
+
3. Test minimal game templates
|
| 8 |
+
4. Display validation results
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import json
|
| 12 |
+
from app.services.retrieval import load_games_dataset, normalize_game_record
|
| 13 |
+
from app.services.schema_validator import (
|
| 14 |
+
load_schema,
|
| 15 |
+
validate_game_schema,
|
| 16 |
+
validate_task_structure,
|
| 17 |
+
validate_safety_structure,
|
| 18 |
+
create_minimal_game_template
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def test_schema_structure():
|
| 23 |
+
"""Test that the schema itself is valid."""
|
| 24 |
+
print("=" * 80)
|
| 25 |
+
print("SCHEMA STRUCTURE TEST")
|
| 26 |
+
print("=" * 80)
|
| 27 |
+
|
| 28 |
+
try:
|
| 29 |
+
schema = load_schema("game_schema.json")
|
| 30 |
+
print("β Game schema loaded successfully")
|
| 31 |
+
print(f" Title: {schema.get('title')}")
|
| 32 |
+
print(f" Type: {schema.get('type')}")
|
| 33 |
+
print(f" Required fields: {', '.join(schema.get('required', []))}")
|
| 34 |
+
return True
|
| 35 |
+
except Exception as e:
|
| 36 |
+
print(f"β Failed to load schema: {e}")
|
| 37 |
+
return False
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def test_minimal_template():
|
| 41 |
+
"""Test validation of a minimal game template."""
|
| 42 |
+
print("\n" + "=" * 80)
|
| 43 |
+
print("MINIMAL GAME TEMPLATE TEST")
|
| 44 |
+
print("=" * 80)
|
| 45 |
+
|
| 46 |
+
template = create_minimal_game_template()
|
| 47 |
+
is_valid, errors = validate_game_schema(template)
|
| 48 |
+
|
| 49 |
+
if is_valid:
|
| 50 |
+
print("β Minimal template is valid against schema")
|
| 51 |
+
print(f" Game ID: {template['game_id']}")
|
| 52 |
+
print(f" Title: {template['title']}")
|
| 53 |
+
print(f" Tasks: {len(template['tasks'])}")
|
| 54 |
+
print(f" Rules: {len(template['rules'])}")
|
| 55 |
+
return True
|
| 56 |
+
else:
|
| 57 |
+
print(f"β Minimal template validation failed:")
|
| 58 |
+
for error in errors:
|
| 59 |
+
print(f" - {error}")
|
| 60 |
+
return False
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def test_dataset_games():
|
| 64 |
+
"""Test validation of example games from the dataset."""
|
| 65 |
+
print("\n" + "=" * 80)
|
| 66 |
+
print("DATASET GAMES VALIDATION TEST")
|
| 67 |
+
print("=" * 80)
|
| 68 |
+
|
| 69 |
+
# Load dataset
|
| 70 |
+
raw_records = load_games_dataset("app/data/games_dataset.json")
|
| 71 |
+
|
| 72 |
+
# Convert raw records to game schema format for testing
|
| 73 |
+
tested_count = 0
|
| 74 |
+
valid_count = 0
|
| 75 |
+
invalid_games = []
|
| 76 |
+
|
| 77 |
+
for raw in raw_records[:3]: # Test first 3 as sample
|
| 78 |
+
try:
|
| 79 |
+
# Extract expected output as game structure
|
| 80 |
+
output = raw.get('expected_output', {})
|
| 81 |
+
input_data = raw.get('input', {})
|
| 82 |
+
|
| 83 |
+
# Build game in schema format
|
| 84 |
+
# Transform tasks to include required title field
|
| 85 |
+
tasks_transformed = []
|
| 86 |
+
for task in output.get('tasks', []):
|
| 87 |
+
task_copy = task.copy()
|
| 88 |
+
# Add title if missing
|
| 89 |
+
if 'title' not in task_copy:
|
| 90 |
+
task_copy['title'] = task_copy.get('description', 'Task')[:50]
|
| 91 |
+
# Ensure all required fields exist
|
| 92 |
+
task_copy.setdefault('proof_type', 'observation')
|
| 93 |
+
task_copy.setdefault('hint', 'See location hint above')
|
| 94 |
+
task_copy.setdefault('safety_note', 'Follow general safety rules')
|
| 95 |
+
tasks_transformed.append(task_copy)
|
| 96 |
+
|
| 97 |
+
game = {
|
| 98 |
+
"game_id": raw.get('id'),
|
| 99 |
+
"title": f"Game {raw.get('id')}",
|
| 100 |
+
"theme": "discovery",
|
| 101 |
+
"setup": {
|
| 102 |
+
"city": input_data.get('location', {}).get('city', 'Paris'),
|
| 103 |
+
"area": input_data.get('location', {}).get('area', ''),
|
| 104 |
+
"meeting_point": "Central meeting point",
|
| 105 |
+
"duration_minutes": input_data.get('preferences', {}).get('duration_minutes', 45),
|
| 106 |
+
"num_players": input_data.get('preferences', {}).get('num_players', 4)
|
| 107 |
+
},
|
| 108 |
+
"rules": output.get('rules', []),
|
| 109 |
+
"tasks": tasks_transformed,
|
| 110 |
+
"global_hints": output.get('hints', [[]])[0] if output.get('hints') else [],
|
| 111 |
+
"score_rules": ["Standard scoring"],
|
| 112 |
+
"tie_breaker": "Most tasks completed",
|
| 113 |
+
"safety": {
|
| 114 |
+
"allowed_zone": "Public area",
|
| 115 |
+
"forbidden_behaviors": [],
|
| 116 |
+
"adult_supervision": input_data.get('preferences', {}).get('age_group') == 'kids',
|
| 117 |
+
"stop_conditions": ["Emergency", "Weather"]
|
| 118 |
+
},
|
| 119 |
+
"story_seed": {
|
| 120 |
+
"tone": "playful",
|
| 121 |
+
"motifs": [],
|
| 122 |
+
"recap_style": "episode_recap"
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
tested_count += 1
|
| 127 |
+
is_valid, errors = validate_game_schema(game)
|
| 128 |
+
|
| 129 |
+
if is_valid:
|
| 130 |
+
valid_count += 1
|
| 131 |
+
print(f"β {game['game_id']}: Valid")
|
| 132 |
+
else:
|
| 133 |
+
invalid_games.append({
|
| 134 |
+
'id': game['game_id'],
|
| 135 |
+
'errors': errors
|
| 136 |
+
})
|
| 137 |
+
print(f"β {game['game_id']}: Invalid")
|
| 138 |
+
for error in errors[:2]: # Show first 2 errors
|
| 139 |
+
print(f" {error}")
|
| 140 |
+
|
| 141 |
+
except Exception as e:
|
| 142 |
+
tested_count += 1
|
| 143 |
+
print(f"β {raw.get('id')}: Exception - {str(e)[:60]}")
|
| 144 |
+
|
| 145 |
+
print(f"\nResults: {valid_count}/{tested_count} games valid")
|
| 146 |
+
|
| 147 |
+
if invalid_games:
|
| 148 |
+
print("\nInvalid games details:")
|
| 149 |
+
for game_info in invalid_games:
|
| 150 |
+
print(f" {game_info['id']}: {game_info['errors']}")
|
| 151 |
+
|
| 152 |
+
return valid_count == tested_count
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def test_task_validation():
|
| 156 |
+
"""Test task-level validation."""
|
| 157 |
+
print("\n" + "=" * 80)
|
| 158 |
+
print("TASK VALIDATION TEST")
|
| 159 |
+
print("=" * 80)
|
| 160 |
+
|
| 161 |
+
test_cases = [
|
| 162 |
+
{
|
| 163 |
+
"name": "Valid task",
|
| 164 |
+
"task": {
|
| 165 |
+
"task_id": "t1",
|
| 166 |
+
"title": "Find landmark",
|
| 167 |
+
"description": "Locate and photograph the fountain",
|
| 168 |
+
"location_hint": "Look in the central square",
|
| 169 |
+
"points": 25,
|
| 170 |
+
"time_limit_minutes": 15,
|
| 171 |
+
"proof_type": "photo",
|
| 172 |
+
"hint": "It's in the middle",
|
| 173 |
+
"safety_note": "Stay on paths"
|
| 174 |
+
},
|
| 175 |
+
"expect_valid": True
|
| 176 |
+
},
|
| 177 |
+
{
|
| 178 |
+
"name": "Invalid proof_type",
|
| 179 |
+
"task": {
|
| 180 |
+
"task_id": "t2",
|
| 181 |
+
"title": "Task",
|
| 182 |
+
"description": "Do something",
|
| 183 |
+
"location_hint": "Somewhere",
|
| 184 |
+
"points": 10,
|
| 185 |
+
"time_limit_minutes": 5,
|
| 186 |
+
"proof_type": "video", # Invalid
|
| 187 |
+
"hint": "Hint",
|
| 188 |
+
"safety_note": "Safe"
|
| 189 |
+
},
|
| 190 |
+
"expect_valid": False
|
| 191 |
+
},
|
| 192 |
+
{
|
| 193 |
+
"name": "Missing safety_note",
|
| 194 |
+
"task": {
|
| 195 |
+
"task_id": "t3",
|
| 196 |
+
"title": "Task",
|
| 197 |
+
"description": "Do something",
|
| 198 |
+
"location_hint": "Somewhere",
|
| 199 |
+
"points": 10,
|
| 200 |
+
"time_limit_minutes": 5,
|
| 201 |
+
"proof_type": "observation",
|
| 202 |
+
"hint": "Hint"
|
| 203 |
+
# Missing safety_note
|
| 204 |
+
},
|
| 205 |
+
"expect_valid": False
|
| 206 |
+
}
|
| 207 |
+
]
|
| 208 |
+
|
| 209 |
+
for test in test_cases:
|
| 210 |
+
is_valid, errors = validate_task_structure(test['task'])
|
| 211 |
+
status = "β" if is_valid == test['expect_valid'] else "β"
|
| 212 |
+
print(f"{status} {test['name']}: {is_valid}")
|
| 213 |
+
if errors:
|
| 214 |
+
for error in errors[:1]:
|
| 215 |
+
print(f" {error}")
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def test_safety_validation():
|
| 219 |
+
"""Test safety object validation."""
|
| 220 |
+
print("\n" + "=" * 80)
|
| 221 |
+
print("SAFETY VALIDATION TEST")
|
| 222 |
+
print("=" * 80)
|
| 223 |
+
|
| 224 |
+
valid_safety = {
|
| 225 |
+
"allowed_zone": "Public park and streets",
|
| 226 |
+
"forbidden_behaviors": [
|
| 227 |
+
"Entering buildings",
|
| 228 |
+
"Crossing roads unsafely"
|
| 229 |
+
],
|
| 230 |
+
"adult_supervision": True,
|
| 231 |
+
"stop_conditions": [
|
| 232 |
+
"Injury",
|
| 233 |
+
"Emergency"
|
| 234 |
+
]
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
is_valid, errors = validate_safety_structure(valid_safety)
|
| 238 |
+
print(f"{'β' if is_valid else 'β'} Valid safety object: {is_valid}")
|
| 239 |
+
if errors:
|
| 240 |
+
for error in errors:
|
| 241 |
+
print(f" {error}")
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def main():
|
| 245 |
+
print("\nGAME SCHEMA AND VALIDATION TESTS\n")
|
| 246 |
+
|
| 247 |
+
# Run all tests
|
| 248 |
+
schema_ok = test_schema_structure()
|
| 249 |
+
template_ok = test_minimal_template()
|
| 250 |
+
dataset_ok = test_dataset_games()
|
| 251 |
+
test_task_validation()
|
| 252 |
+
test_safety_validation()
|
| 253 |
+
|
| 254 |
+
# Summary
|
| 255 |
+
print("\n" + "=" * 80)
|
| 256 |
+
print("TEST SUMMARY")
|
| 257 |
+
print("=" * 80)
|
| 258 |
+
print(f"Schema structure: {'β PASS' if schema_ok else 'β FAIL'}")
|
| 259 |
+
print(f"Minimal template: {'β PASS' if template_ok else 'β FAIL'}")
|
| 260 |
+
print(f"Dataset games: {'β PASS' if dataset_ok else 'β FAIL'}")
|
| 261 |
+
print("\nSchema validation is ready for use in game generation and validation.")
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
if __name__ == "__main__":
|
| 265 |
+
main()
|