NANI-Nithin commited on
Commit
eaf7eca
Β·
1 Parent(s): ec2e649

Phase 1: establish the baseline

Browse files
.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()