BhargavMN Claude Sonnet 4.6 commited on
Commit
f7f184b
·
1 Parent(s): 31190c1

feat: Add scavenger_hunt synthetic training-data generation pipeline

Browse files

Integrates the sampler/generator/validator/test pipeline into scripts/scavenger_hunt/
(per-game folder pattern for future game types) with generated datasets under
app/data/scavenger_hunt/. Adds google-genai and python-Levenshtein deps and includes
a sample dataset.json for review.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

.gitignore CHANGED
@@ -102,6 +102,10 @@ dmypy.json
102
  # Generated files
103
  normalized_games.json
104
 
 
 
 
 
105
 
106
  # other files to ignore
107
  CityQuest_AI_Project_Specification.md
 
102
  # Generated files
103
  normalized_games.json
104
 
105
+ # Synthetic training-data generation output (per-game folders under app/data/)
106
+ app/data/*/dataset_errors.jsonl
107
+ app/data/*/run_generation.log
108
+
109
 
110
  # other files to ignore
111
  CityQuest_AI_Project_Specification.md
app/data/scavenger_hunt/dataset.json ADDED
@@ -0,0 +1,2222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "SH-BUE-0001",
4
+ "input": {
5
+ "game_type": "scavenger_hunt",
6
+ "location": {
7
+ "city": "Buenos Aires",
8
+ "country": "Argentina",
9
+ "city_code": "BUE",
10
+ "landscape_tags": [
11
+ "cafe_dense",
12
+ "wide_boulevard",
13
+ "plaza_or_square",
14
+ "historic_district",
15
+ "street_art_district"
16
+ ],
17
+ "urban_density": "dense",
18
+ "climate_zone": "temperate",
19
+ "area_type": "mixed_residential"
20
+ },
21
+ "players": {
22
+ "count": 18,
23
+ "team_count": 2,
24
+ "age_group": "teens",
25
+ "mobility": "standard"
26
+ },
27
+ "preferences": {
28
+ "duration_minutes": 30,
29
+ "difficulty": "easy",
30
+ "theme": "logic",
31
+ "allow_transport": true
32
+ }
33
+ },
34
+ "output": {
35
+ "game_title": "Urban Discovery Challenge",
36
+ "rules": {
37
+ "objective": "Teams must complete three easy tasks by finding specific elements within the designated urban area, proving completion through photos or answers.",
38
+ "scoring_method": "point_accumulation",
39
+ "task_reveal_mode": "all_at_once",
40
+ "team_rules": "Teams will work together to complete tasks and accumulate points. The score of all members contributes to the team total.",
41
+ "time_limit_minutes": 30,
42
+ "disqualification_conditions": [
43
+ "tampering with game elements",
44
+ "entering restricted zones",
45
+ "dangerous behavior",
46
+ "significant rule violations"
47
+ ]
48
+ },
49
+ "safety_constraints": {
50
+ "exclusion_zones": [
51
+ "private_property",
52
+ "active_roadway",
53
+ "construction_sites",
54
+ "restricted_government_buildings"
55
+ ],
56
+ "physical_limits": [
57
+ "no climbing",
58
+ "no jumping",
59
+ "no water entry",
60
+ "no entering buildings"
61
+ ],
62
+ "adult_supervision_required": false,
63
+ "notes": "The temperate climate allows for comfortable outdoor exploration; however, participants should remain aware of their surroundings at all times."
64
+ },
65
+ "tasks": [
66
+ {
67
+ "task_id": "T01",
68
+ "title": "Historic Emblem Photo",
69
+ "description": "Locate the central open square within the historic district. Find a distinct, non-human, carved emblem or symbol on a permanent structure within the square, clearly visible and at eye level.",
70
+ "landscape_tags_used": [
71
+ "plaza_or_square",
72
+ "historic_district"
73
+ ],
74
+ "task_type": "find_and_photograph",
75
+ "difficulty_contribution": "easy",
76
+ "points": 10,
77
+ "completion_proof": "A clear photograph of the team with the identified emblem or symbol.",
78
+ "estimated_time_minutes": 7,
79
+ "hints": {
80
+ "hint_1": "Head towards the large green space surrounded by old buildings.",
81
+ "hint_2": "Look for decorative carvings on the base of monuments or facades of the older structures bordering the main public space.",
82
+ "hint_3": "In the biggest open area, near the center, there's a large, old, stone monument. Examine its lower sections carefully for a carved design representing a local theme, not a person."
83
+ },
84
+ "safety_flags": [
85
+ "be aware of pedestrians"
86
+ ]
87
+ },
88
+ {
89
+ "task_id": "T02",
90
+ "title": "Street Art Detail",
91
+ "description": "Along the wide boulevard known for its street art, find a large mural that depicts an animal. Count the total number of distinct colors used exclusively within the animal's body (excluding background or frame).",
92
+ "landscape_tags_used": [
93
+ "wide_boulevard",
94
+ "street_art_district"
95
+ ],
96
+ "task_type": "observe_and_answer",
97
+ "difficulty_contribution": "easy",
98
+ "points": 10,
99
+ "completion_proof": "Submit the correct number of colors via the app.",
100
+ "estimated_time_minutes": 8,
101
+ "hints": {
102
+ "hint_1": "Walk along the main thoroughfare, keeping an eye on the walls.",
103
+ "hint_2": "Focus on the vibrant painted walls. There's a particular artwork featuring a creature; study it closely for its color palette.",
104
+ "hint_3": "On the longest street with many painted walls, look for a very large painting of a bird or a feline. Count only the colors inside the animal's main body, not the colors used around it."
105
+ },
106
+ "safety_flags": [
107
+ "stay on sidewalks",
108
+ "do not obstruct pedestrian traffic"
109
+ ]
110
+ },
111
+ {
112
+ "task_id": "T03",
113
+ "title": "Cafe Query",
114
+ "description": "Enter a public-facing cafe in the dense cafe district. Politely ask a staff member what their most popular hot beverage is.",
115
+ "landscape_tags_used": [
116
+ "cafe_dense"
117
+ ],
118
+ "task_type": "social_interaction",
119
+ "difficulty_contribution": "easy",
120
+ "points": 10,
121
+ "completion_proof": "Submit the name of the beverage via the app.",
122
+ "estimated_time_minutes": 7,
123
+ "hints": {
124
+ "hint_1": "Look for small shops with outdoor seating and coffee aromas.",
125
+ "hint_2": "Find a busy coffee shop. Approach the counter and ask a quick question to one of the employees.",
126
+ "hint_3": "In the area with many small cafes, choose any public-facing establishment. Go inside, politely ask a person working there for their recommendation or most requested hot drink, and remember their answer."
127
+ },
128
+ "safety_flags": [
129
+ "be polite and respectful",
130
+ "do not disturb customers"
131
+ ]
132
+ }
133
+ ],
134
+ "task_count": 3,
135
+ "total_possible_points": 30,
136
+ "max_deductible_points": 30,
137
+ "minimum_possible_points": 0,
138
+ "bonus_task_eligible": false,
139
+ "bonus_task": {
140
+ "description": null,
141
+ "points": null,
142
+ "risk": null,
143
+ "completion_proof": null
144
+ },
145
+ "scoring_summary": {
146
+ "base_points_available": 30,
147
+ "time_bonus_per_minute_early": null,
148
+ "hint_cost_tier_1": 5,
149
+ "hint_cost_tier_2": 10,
150
+ "team_aggregation_method": "sum_all_members",
151
+ "winning_condition_detail": "The team that accumulates the highest total score, after any hint deductions, by the end of the time limit will be declared the winner."
152
+ },
153
+ "estimated_total_time_minutes": 22,
154
+ "quality_score": 4.5
155
+ }
156
+ },
157
+ {
158
+ "id": "SH-AKL-0003",
159
+ "input": {
160
+ "game_type": "scavenger_hunt",
161
+ "location": {
162
+ "city": "Auckland",
163
+ "country": "New Zealand",
164
+ "city_code": "AKL",
165
+ "landscape_tags": [
166
+ "coastal_waterfront",
167
+ "hill_or_elevation",
168
+ "market_outdoor",
169
+ "park_large"
170
+ ],
171
+ "urban_density": "mixed",
172
+ "climate_zone": "temperate",
173
+ "area_type": "mixed_residential"
174
+ },
175
+ "players": {
176
+ "count": 16,
177
+ "team_count": 1,
178
+ "age_group": "mixed_adults",
179
+ "mobility": "standard"
180
+ },
181
+ "preferences": {
182
+ "duration_minutes": 60,
183
+ "difficulty": "medium",
184
+ "theme": "observation",
185
+ "allow_transport": false
186
+ }
187
+ },
188
+ "output": {
189
+ "game_title": "Auckland Observation Challenge",
190
+ "rules": {
191
+ "objective": "Teams must complete a series of observation and interaction tasks within the given time limit to earn points and win the scavenger hunt.",
192
+ "scoring_method": "point_accumulation",
193
+ "task_reveal_mode": "all_at_once",
194
+ "team_rules": null,
195
+ "time_limit_minutes": 60,
196
+ "disqualification_conditions": [
197
+ "cheating",
198
+ "damaging public or private property",
199
+ "exceeding the time limit without prior arrangement",
200
+ "entering exclusion zones",
201
+ "disrespectful behavior towards public or staff"
202
+ ]
203
+ },
204
+ "safety_constraints": {
205
+ "exclusion_zones": [
206
+ "private_property",
207
+ "active_roadway",
208
+ "construction_sites",
209
+ "restricted_government_buildings",
210
+ "water_edge"
211
+ ],
212
+ "physical_limits": [
213
+ "no climbing",
214
+ "no jumping",
215
+ "no water entry",
216
+ "no entering buildings"
217
+ ],
218
+ "adult_supervision_required": false,
219
+ "notes": "The scavenger hunt takes place in a temperate climate zone, so participants should dress appropriately for varying weather conditions. Stay hydrated throughout the activity."
220
+ },
221
+ "tasks": [
222
+ {
223
+ "task_id": "T01",
224
+ "title": "Park Sculpture Search",
225
+ "description": "Locate a prominent, non-abstract animal sculpture within the large park area and capture its image.",
226
+ "landscape_tags_used": [
227
+ "park_large"
228
+ ],
229
+ "task_type": "find_and_photograph",
230
+ "difficulty_contribution": "easy",
231
+ "points": 10,
232
+ "completion_proof": "Photo of the sculpture clearly showing its full form.",
233
+ "estimated_time_minutes": 6,
234
+ "hints": {
235
+ "hint_1": "Head towards the large green space visible from the starting point.",
236
+ "hint_2": "Once inside the park, navigate towards the open grassy area near the public playground, where several art installations are often found.",
237
+ "hint_3": "The sculpture you are seeking depicts a common feathered friend often seen near water, located close to the east side path, just before the path splits towards the waterfront."
238
+ },
239
+ "safety_flags": [
240
+ "aware of crowds"
241
+ ]
242
+ },
243
+ {
244
+ "task_id": "T02",
245
+ "title": "Waterfront Seating Count",
246
+ "description": "Count the total number of distinct, permanently installed public seating benches along the main pedestrian path of the coastal waterfront for a 200-meter stretch.",
247
+ "landscape_tags_used": [
248
+ "coastal_waterfront"
249
+ ],
250
+ "task_type": "observe_and_answer",
251
+ "difficulty_contribution": "medium",
252
+ "points": 20,
253
+ "completion_proof": "Correct numerical answer submitted.",
254
+ "estimated_time_minutes": 8,
255
+ "hints": {
256
+ "hint_1": "Proceed directly to the main waterfront promenade.",
257
+ "hint_2": "Start your count from the large interpretive sign located near the central pier entrance and walk towards the west along the designated path.",
258
+ "hint_3": "Ensure you are counting only fixed, public benches, not temporary seating or private furniture. The 200-meter stretch is clearly marked by path features."
259
+ },
260
+ "safety_flags": [
261
+ "aware of pedestrians"
262
+ ]
263
+ },
264
+ {
265
+ "task_id": "T03",
266
+ "title": "Hilltop Flora Plaque",
267
+ "description": "Ascend the designated pedestrian path on the small hill and locate a public information plaque describing local flora near the summit.",
268
+ "landscape_tags_used": [
269
+ "hill_or_elevation"
270
+ ],
271
+ "task_type": "reach_and_verify",
272
+ "difficulty_contribution": "easy",
273
+ "points": 10,
274
+ "completion_proof": "Team member standing next to the plaque, visible in a photo.",
275
+ "estimated_time_minutes": 7,
276
+ "hints": {
277
+ "hint_1": "Find the base of the prominent elevated area.",
278
+ "hint_2": "Look for the main paved pathway that winds its way up the slope. The plaque is situated close to the top, off to one side of this path.",
279
+ "hint_3": "The plaque is made of dark metal and is often surrounded by native plants it describes. It's just past a small cluster of mature evergreen trees before the path levels out."
280
+ },
281
+ "safety_flags": [
282
+ "uneven terrain",
283
+ "moderate physical exertion"
284
+ ]
285
+ },
286
+ {
287
+ "task_id": "T04",
288
+ "title": "Market Vendor Query",
289
+ "description": "Approach a vendor at the outdoor market selling fresh produce and ask them to name their favorite type of local fruit. Record their answer.",
290
+ "landscape_tags_used": [
291
+ "market_outdoor"
292
+ ],
293
+ "task_type": "social_interaction",
294
+ "difficulty_contribution": "medium",
295
+ "points": 20,
296
+ "completion_proof": "Verbal answer from vendor recorded in team notes/app, including vendor's stall description.",
297
+ "estimated_time_minutes": 8,
298
+ "hints": {
299
+ "hint_1": "Head towards the bustling open-air trading area.",
300
+ "hint_2": "Focus on the stalls displaying colorful seasonal fruits and vegetables. Choose a vendor who appears to be actively engaging with customers.",
301
+ "hint_3": "Politely introduce yourselves as part of a local game. Ask a clear, open-ended question about their preferred local fruit. Remember to note down who you asked."
302
+ },
303
+ "safety_flags": [
304
+ "aware of crowds",
305
+ "respect local vendors"
306
+ ]
307
+ },
308
+ {
309
+ "task_id": "T05",
310
+ "title": "Bridge Width Estimation",
311
+ "description": "Starting from the designated marker near the main pier, use only your foot length to estimate the width of the main pedestrian bridge spanning the small inlet. You have 3 minutes to complete the estimate once you reach the bridge.",
312
+ "landscape_tags_used": [
313
+ "coastal_waterfront"
314
+ ],
315
+ "task_type": "timed_challenge",
316
+ "difficulty_contribution": "hard",
317
+ "points": 30,
318
+ "completion_proof": "Submit the estimated width in 'foot lengths' and a photo of the starting marker.",
319
+ "estimated_time_minutes": 8,
320
+ "hints": {
321
+ "hint_1": "Proceed along the waterfront towards the large docking structure.",
322
+ "hint_2": "The starting marker is a small, engraved brass plate set into the ground, located directly at the entrance to the largest pedestrian bridge over the water.",
323
+ "hint_3": "For accuracy, ensure your steps are consistent and measure heel-to-toe. The timer starts when you step onto the bridge. Your photo should clearly show the starting marker."
324
+ },
325
+ "safety_flags": [
326
+ "aware of pedestrians",
327
+ "focus during timed task"
328
+ ]
329
+ },
330
+ {
331
+ "task_id": "T06",
332
+ "title": "Promenade Clock Tower",
333
+ "description": "Locate a functional public clock tower near the main promenade along the coastal waterfront and photograph its face showing the current time.",
334
+ "landscape_tags_used": [
335
+ "coastal_waterfront"
336
+ ],
337
+ "task_type": "find_and_photograph",
338
+ "difficulty_contribution": "medium",
339
+ "points": 20,
340
+ "completion_proof": "Photo of the clock tower's face, with time clearly visible.",
341
+ "estimated_time_minutes": 7,
342
+ "hints": {
343
+ "hint_1": "Follow the main walkway parallel to the water's edge.",
344
+ "hint_2": "The clock tower stands tall and is a prominent feature visible from various points along the central section of the promenade, often near a popular gathering spot.",
345
+ "hint_3": "It is a classic, four-sided clock tower with a distinct architectural style. Position yourself to capture both the tower's height and the clear display of the time on its face."
346
+ },
347
+ "safety_flags": [
348
+ "aware of crowds"
349
+ ]
350
+ }
351
+ ],
352
+ "task_count": 6,
353
+ "total_possible_points": 110,
354
+ "max_deductible_points": 60,
355
+ "minimum_possible_points": 50,
356
+ "bonus_task_eligible": false,
357
+ "bonus_task": {
358
+ "description": null,
359
+ "points": null,
360
+ "risk": null,
361
+ "completion_proof": null
362
+ },
363
+ "scoring_summary": {
364
+ "base_points_available": 110,
365
+ "time_bonus_per_minute_early": null,
366
+ "hint_cost_tier_1": 5,
367
+ "hint_cost_tier_2": 10,
368
+ "team_aggregation_method": null,
369
+ "winning_condition_detail": "The team that accumulates the highest total score by correctly completing tasks and incurring the fewest hint penalties will be declared the winner."
370
+ },
371
+ "estimated_total_time_minutes": 44,
372
+ "quality_score": 4.5
373
+ }
374
+ },
375
+ {
376
+ "id": "SH-LIS-0004",
377
+ "input": {
378
+ "game_type": "scavenger_hunt",
379
+ "location": {
380
+ "city": "Lisbon",
381
+ "country": "Portugal",
382
+ "city_code": "LIS",
383
+ "landscape_tags": [
384
+ "historic_district",
385
+ "plaza_or_square",
386
+ "hill_or_elevation",
387
+ "narrow_alley_network",
388
+ "coastal_waterfront"
389
+ ],
390
+ "urban_density": "dense",
391
+ "climate_zone": "mediterranean",
392
+ "area_type": "historic_district"
393
+ },
394
+ "players": {
395
+ "count": 17,
396
+ "team_count": 5,
397
+ "age_group": "mixed_adults",
398
+ "mobility": "standard"
399
+ },
400
+ "preferences": {
401
+ "duration_minutes": 60,
402
+ "difficulty": "easy",
403
+ "theme": "social",
404
+ "allow_transport": true
405
+ }
406
+ },
407
+ "output": {
408
+ "game_title": "Historic Quarter Explorer Challenge",
409
+ "rules": {
410
+ "objective": "Complete tasks by finding specific locations and details within the historic district to earn points.",
411
+ "scoring_method": "point_accumulation",
412
+ "task_reveal_mode": "sequential",
413
+ "team_rules": "Teams must stay together and work collaboratively to solve each task, ensuring all members are present for completion proof.",
414
+ "time_limit_minutes": 60,
415
+ "disqualification_conditions": [
416
+ "failure to adhere to safety guidelines",
417
+ "entering restricted areas",
418
+ "vandalism or disrespect to property",
419
+ "cheating or unfair play"
420
+ ]
421
+ },
422
+ "safety_constraints": {
423
+ "exclusion_zones": [
424
+ "private_property",
425
+ "active_roadway",
426
+ "construction_sites",
427
+ "restricted_government_buildings",
428
+ "water_edge"
429
+ ],
430
+ "physical_limits": [
431
+ "no climbing",
432
+ "no jumping",
433
+ "no water entry",
434
+ "no entering buildings"
435
+ ],
436
+ "adult_supervision_required": false,
437
+ "notes": "Mediterranean climate ensures generally mild conditions; however, participants should be prepared for sun exposure and varying elevations, especially on hilly terrain. Stay hydrated and wear comfortable shoes."
438
+ },
439
+ "tasks": [
440
+ {
441
+ "task_id": "T01",
442
+ "title": "Plaza Monument Base",
443
+ "description": "Locate a prominent historical monument or statue within the main open square and capture its base.",
444
+ "landscape_tags_used": [
445
+ "historic_district",
446
+ "plaza_or_square"
447
+ ],
448
+ "task_type": "find_and_photograph",
449
+ "difficulty_contribution": "easy",
450
+ "points": 10,
451
+ "completion_proof": "A photograph clearly showing the base of the identified monument.",
452
+ "estimated_time_minutes": 8,
453
+ "hints": {
454
+ "hint_1": "Head towards the large, central open space; look for a significant historical marker.",
455
+ "hint_2": "In the main public square, find the large, central commemorative object, often a statue or obelisk, and focus on its base where it meets the ground.",
456
+ "hint_3": "Start at the heart of the main open gathering area. Search for the most prominent historical feature, typically a statue on a pedestal or an ancient column. Your target is the very bottom part, where the structure rests."
457
+ },
458
+ "safety_flags": [
459
+ "watch for uneven surfaces"
460
+ ]
461
+ },
462
+ {
463
+ "task_id": "T02",
464
+ "title": "Uphill Alley Tiles",
465
+ "description": "Traverse a narrow uphill alleyway and identify the number of distinct decorative tile panels adorning the walls of the first three buildings on your right.",
466
+ "landscape_tags_used": [
467
+ "narrow_alley_network",
468
+ "hill_or_elevation"
469
+ ],
470
+ "task_type": "observe_and_answer",
471
+ "difficulty_contribution": "easy",
472
+ "points": 10,
473
+ "completion_proof": "The correct count of distinct decorative tile panels.",
474
+ "estimated_time_minutes": 9,
475
+ "hints": {
476
+ "hint_1": "Walk up a narrow alley; count decorative wall tiles on early right-side buildings.",
477
+ "hint_2": "Proceed up a constricted pathway; observe the first three buildings on your right. Count the unique, patterned ceramic panels found on their exterior walls.",
478
+ "hint_3": "Begin your ascent into a winding, narrow street. Focus on the very first three buildings you pass on your right-hand side. Carefully scan their external surfaces for distinct, often colorful, baked-clay decorations. Count how many different sections of these are present."
479
+ },
480
+ "safety_flags": [
481
+ "watch for uneven surfaces",
482
+ "mind your step in narrow passages"
483
+ ]
484
+ },
485
+ {
486
+ "task_id": "T03",
487
+ "title": "Local Snack Discovery",
488
+ "description": "Find a small, publicly accessible shop near the coastal waterfront. Ask a local shopkeeper for their favorite non-touristy local snack recommendation.",
489
+ "landscape_tags_used": [
490
+ "historic_district",
491
+ "coastal_waterfront"
492
+ ],
493
+ "task_type": "social_interaction",
494
+ "difficulty_contribution": "easy",
495
+ "points": 10,
496
+ "completion_proof": "Verbally state the name of the recommended snack and the shop's generic type (e.g., bakery, cafe).",
497
+ "estimated_time_minutes": 9,
498
+ "hints": {
499
+ "hint_1": "Locate a public shop near the waterfront; ask a worker for their favorite local snack.",
500
+ "hint_2": "Visit a small, publicly accessible business near the waterfront. Ask a local employee for their personal favorite authentic, regional snack recommendation, distinct from common tourist fare.",
501
+ "hint_3": "Head into the historic district, making your way towards the coastal water. Look for any small, independent shop that is open to the public. Approach a staff member, introduce yourselves, and politely ask them to share their absolute favorite local, non-touristy food item to eat."
502
+ },
503
+ "safety_flags": [
504
+ "respect local customs",
505
+ "be polite and courteous"
506
+ ]
507
+ },
508
+ {
509
+ "task_id": "T04",
510
+ "title": "High View Seating Check",
511
+ "description": "Navigate to a high viewpoint that overlooks a large open square. Once there, confirm the presence of at least two distinct types of public seating (e.g., benches, steps, low walls).",
512
+ "landscape_tags_used": [
513
+ "hill_or_elevation",
514
+ "plaza_or_square"
515
+ ],
516
+ "task_type": "reach_and_verify",
517
+ "difficulty_contribution": "easy",
518
+ "points": 10,
519
+ "completion_proof": "Verbally identify the two distinct types of public seating observed.",
520
+ "estimated_time_minutes": 9,
521
+ "hints": {
522
+ "hint_1": "Reach an elevated viewpoint above an open square; confirm two types of outdoor seating.",
523
+ "hint_2": "Navigate to an elevated point providing a clear perspective over a wide open square. From there, identify and verify at least two different designs of public seating arrangements.",
524
+ "hint_3": "Make your way up to a significant elevation that provides an expansive, unobstructed panorama of a major open public gathering space. Once at this viewpoint, carefully scan the entire square below to visually confirm the existence of two or more distinctly different kinds of public seating structures."
525
+ },
526
+ "safety_flags": [
527
+ "watch for uneven surfaces",
528
+ "be aware of surroundings at viewpoints"
529
+ ]
530
+ },
531
+ {
532
+ "task_id": "T05",
533
+ "title": "Coastal Wall Mosaic",
534
+ "description": "Locate a decorative mosaic or tiled artwork on a wall within the historic district, close to the coastal water's edge, and photograph its central detail.",
535
+ "landscape_tags_used": [
536
+ "coastal_waterfront",
537
+ "historic_district"
538
+ ],
539
+ "task_type": "find_and_photograph",
540
+ "difficulty_contribution": "easy",
541
+ "points": 10,
542
+ "completion_proof": "A photograph clearly showing the central detail of the tiled artwork.",
543
+ "estimated_time_minutes": 8,
544
+ "hints": {
545
+ "hint_1": "Near the water's edge in the old district, find a tiled wall art; photograph its center.",
546
+ "hint_2": "Search the historic district near the coastal water for a wall featuring a vibrant mosaic or detailed tile artwork. Photograph the central part of this artistic display.",
547
+ "hint_3": "Explore the historic district, moving towards the coastal waterfront. Look for an outdoor wall adorned with a large, decorative mosaic or intricate tile pattern. Once found, frame your shot to specifically highlight the absolute central and most prominent detail of the entire artwork for your completion proof."
548
+ },
549
+ "safety_flags": [
550
+ "watch for uneven surfaces"
551
+ ]
552
+ }
553
+ ],
554
+ "task_count": 5,
555
+ "total_possible_points": 50,
556
+ "max_deductible_points": 50,
557
+ "minimum_possible_points": 0,
558
+ "bonus_task_eligible": false,
559
+ "bonus_task": {
560
+ "description": null,
561
+ "points": null,
562
+ "risk": null,
563
+ "completion_proof": null
564
+ },
565
+ "scoring_summary": {
566
+ "base_points_available": 50,
567
+ "time_bonus_per_minute_early": null,
568
+ "hint_cost_tier_1": 5,
569
+ "hint_cost_tier_2": 10,
570
+ "team_aggregation_method": "sum_all_members",
571
+ "winning_condition_detail": "The team with the highest total score at the end of the time limit wins."
572
+ },
573
+ "estimated_total_time_minutes": 43,
574
+ "quality_score": 4.5
575
+ }
576
+ },
577
+ {
578
+ "id": "SH-LIS-0005",
579
+ "input": {
580
+ "game_type": "scavenger_hunt",
581
+ "location": {
582
+ "city": "Lisbon",
583
+ "country": "Portugal",
584
+ "city_code": "LIS",
585
+ "landscape_tags": [
586
+ "plaza_or_square",
587
+ "narrow_alley_network",
588
+ "coastal_waterfront",
589
+ "hill_or_elevation"
590
+ ],
591
+ "urban_density": "dense",
592
+ "climate_zone": "mediterranean",
593
+ "area_type": "city_center"
594
+ },
595
+ "players": {
596
+ "count": 11,
597
+ "team_count": 3,
598
+ "age_group": "mixed_family",
599
+ "mobility": "standard"
600
+ },
601
+ "preferences": {
602
+ "duration_minutes": 60,
603
+ "difficulty": "medium",
604
+ "theme": "nature",
605
+ "allow_transport": true
606
+ }
607
+ },
608
+ "output": {
609
+ "game_title": "Urban Flora & Vista Challenge",
610
+ "rules": {
611
+ "objective": "Teams must complete a series of nature-themed tasks within the city center, focusing on observation and interaction with the natural elements and local culture.",
612
+ "scoring_method": "point_accumulation",
613
+ "task_reveal_mode": "sequential",
614
+ "team_rules": "Teams must stay together for the duration of the hunt and collaborate on all tasks.",
615
+ "time_limit_minutes": 60,
616
+ "disqualification_conditions": [
617
+ "Cheating or unsportsmanlike conduct",
618
+ "Deliberate damage to property or natural elements",
619
+ "Ignoring safety instructions or entering exclusion zones",
620
+ "Exceeding the total time limit without valid reason"
621
+ ]
622
+ },
623
+ "safety_constraints": {
624
+ "exclusion_zones": [
625
+ "private_property",
626
+ "active_roadway",
627
+ "construction_sites",
628
+ "restricted_government_buildings",
629
+ "water_edge"
630
+ ],
631
+ "physical_limits": [
632
+ "no climbing",
633
+ "no jumping",
634
+ "no water entry",
635
+ "no entering buildings"
636
+ ],
637
+ "adult_supervision_required": true,
638
+ "notes": "The Mediterranean climate typically offers pleasant conditions, but be prepared for sun exposure. Stay hydrated and wear comfortable walking shoes suitable for uneven terrain."
639
+ },
640
+ "tasks": [
641
+ {
642
+ "task_id": "T01",
643
+ "title": "Green Oasis Discovery",
644
+ "description": "Locate a small, maintained plant feature within the main open public square and capture its image.",
645
+ "landscape_tags_used": [
646
+ "plaza_or_square"
647
+ ],
648
+ "task_type": "find_and_photograph",
649
+ "difficulty_contribution": "easy",
650
+ "points": 10,
651
+ "completion_proof": "A clear photograph showing the plant feature and its immediate surroundings.",
652
+ "estimated_time_minutes": 7,
653
+ "hints": {
654
+ "hint_1": "Look near the center of the largest open area.",
655
+ "hint_2": "The feature is often found close to a bench or a decorative element, providing a touch of nature in the paved space.",
656
+ "hint_3": "Start your search in the most prominent open square. Walk towards the center; you'll find a small, cultivated garden bed or large planter with various green plants. Photograph this specific spot."
657
+ },
658
+ "safety_flags": [
659
+ "crowds"
660
+ ]
661
+ },
662
+ {
663
+ "task_id": "T02",
664
+ "title": "Alleyway Archways",
665
+ "description": "Navigate a section of the narrow alley network to find at least three distinct natural arch-like structures formed by vegetation growing overhead. Count them and note their locations.",
666
+ "landscape_tags_used": [
667
+ "narrow_alley_network"
668
+ ],
669
+ "task_type": "observe_and_answer",
670
+ "difficulty_contribution": "medium",
671
+ "points": 20,
672
+ "completion_proof": "A list of the number of arch-like vegetation structures found and their approximate locations (e.g., 'third alley on left from main street').",
673
+ "estimated_time_minutes": 8,
674
+ "hints": {
675
+ "hint_1": "Head into the older, winding lanes.",
676
+ "hint_2": "Look up as you walk through the confined passages; the plant life often extends across, creating overhead green tunnels.",
677
+ "hint_3": "Enter the historic network of narrow passages. Focus on where buildings are close together and plants are trained or naturally grow to meet in the middle, forming natural, green arches above the path."
678
+ },
679
+ "safety_flags": [
680
+ "uneven_ground"
681
+ ]
682
+ },
683
+ {
684
+ "task_id": "T03",
685
+ "title": "Elevated Viewpoint Check",
686
+ "description": "Ascend to an accessible elevated public area that offers a wide panoramic view of the urban landscape and the distant water. Verify your presence at this viewpoint.",
687
+ "landscape_tags_used": [
688
+ "hill_or_elevation",
689
+ "coastal_waterfront"
690
+ ],
691
+ "task_type": "reach_and_verify",
692
+ "difficulty_contribution": "easy",
693
+ "points": 10,
694
+ "completion_proof": "A team photo clearly showing the panoramic view of the city and water from the designated elevated public area.",
695
+ "estimated_time_minutes": 7,
696
+ "hints": {
697
+ "hint_1": "Go uphill towards a prominent public vista.",
698
+ "hint_2": "The spot is a well-known public terrace or park located on higher ground, offering expansive views towards the horizon.",
699
+ "hint_3": "Seek out the highest public point within walking distance that is designed for visitors. It will feature open space, possibly benches, and an unobstructed view over the city towards the water body."
700
+ },
701
+ "safety_flags": [
702
+ "uneven_ground"
703
+ ]
704
+ },
705
+ {
706
+ "task_id": "T04",
707
+ "title": "Coastal Flora Quest (Timed)",
708
+ "description": "Within eight minutes, find and identify two different types of plant life growing resiliently along the immediate coastal waterfront path. Photograph both, noting any unique adaptations.",
709
+ "landscape_tags_used": [
710
+ "coastal_waterfront"
711
+ ],
712
+ "task_type": "timed_challenge",
713
+ "difficulty_contribution": "hard",
714
+ "points": 30,
715
+ "completion_proof": "Two distinct photographs of coastal plant species, each with a brief note on its location and any visible adaptation to the environment (e.g., thick leaves, salt spray tolerance). Timed completion required.",
716
+ "estimated_time_minutes": 8,
717
+ "hints": {
718
+ "hint_1": "Head directly to the edge of the water.",
719
+ "hint_2": "Focus your search on plants growing close to the sea, often in rocky crevices or sandy patches along the paved path.",
720
+ "hint_3": "Walk along the public path closest to the water. Look carefully at the vegetation thriving in harsh conditions, such as plants with thick, waxy leaves or those clinging to rocks, to find two different species."
721
+ },
722
+ "safety_flags": [
723
+ "uneven_ground",
724
+ "slippery_surfaces"
725
+ ]
726
+ },
727
+ {
728
+ "task_id": "T05",
729
+ "title": "Local Lore of Nature",
730
+ "description": "Approach a public-facing local business within the narrow alley network. Ask a staff member if they know of any traditional local stories or beliefs related to plants or natural elements commonly found in the area. Share one story.",
731
+ "landscape_tags_used": [
732
+ "narrow_alley_network"
733
+ ],
734
+ "task_type": "social_interaction",
735
+ "difficulty_contribution": "medium",
736
+ "points": 20,
737
+ "completion_proof": "Recount one unique story or belief shared by a local, mentioning the type of business and general location where the interaction occurred.",
738
+ "estimated_time_minutes": 8,
739
+ "hints": {
740
+ "hint_1": "Find a small shop in a winding lane.",
741
+ "hint_2": "Look for a family-run business like a bakery, craft shop, or small cafe in the older parts of the city.",
742
+ "hint_3": "Within the labyrinthine alleyways, choose a friendly-looking, small, public-facing shop. Politely ask a staff member if there's a local legend or common belief about a particular plant, tree, or natural feature in the city."
743
+ },
744
+ "safety_flags": [
745
+ "crowds"
746
+ ]
747
+ },
748
+ {
749
+ "task_id": "T06",
750
+ "title": "Hillside Bloom Observation",
751
+ "description": "On an accessible hillside or elevated area with public access, find a flowering plant that exhibits vibrant colors. Observe its characteristics and describe it.",
752
+ "landscape_tags_used": [
753
+ "hill_or_elevation"
754
+ ],
755
+ "task_type": "observe_and_answer",
756
+ "difficulty_contribution": "medium",
757
+ "points": 20,
758
+ "completion_proof": "A detailed verbal description of the flowering plant, including its color, size, number of petals (if visible), and general location.",
759
+ "estimated_time_minutes": 8,
760
+ "hints": {
761
+ "hint_1": "Explore the slopes of a public hill.",
762
+ "hint_2": "Look for areas with natural growth, perhaps near a path or a small park on higher ground, where sunlight encourages blooms.",
763
+ "hint_3": "Traverse the public walking paths up one of the city's hills. Keep an eye out for patches of wild or cultivated flowers. Select one with bright petals and describe its key features for verification."
764
+ },
765
+ "safety_flags": [
766
+ "uneven_ground"
767
+ ]
768
+ }
769
+ ],
770
+ "task_count": 6,
771
+ "total_possible_points": 110,
772
+ "max_deductible_points": 60,
773
+ "minimum_possible_points": 50,
774
+ "bonus_task_eligible": false,
775
+ "bonus_task": {
776
+ "description": null,
777
+ "points": null,
778
+ "risk": null,
779
+ "completion_proof": null
780
+ },
781
+ "scoring_summary": {
782
+ "base_points_available": 110,
783
+ "time_bonus_per_minute_early": null,
784
+ "hint_cost_tier_1": 5,
785
+ "hint_cost_tier_2": 10,
786
+ "team_aggregation_method": "sum_all_members",
787
+ "winning_condition_detail": "The team that accumulates the highest total score after all tasks are completed and penalties applied will be declared the winner."
788
+ },
789
+ "estimated_total_time_minutes": 46,
790
+ "quality_score": 4.5
791
+ }
792
+ },
793
+ {
794
+ "id": "SH-CPT-0006",
795
+ "input": {
796
+ "game_type": "scavenger_hunt",
797
+ "location": {
798
+ "city": "Cape Town",
799
+ "country": "South Africa",
800
+ "city_code": "CPT",
801
+ "landscape_tags": [
802
+ "garden_formal",
803
+ "hill_or_elevation",
804
+ "historic_district",
805
+ "market_outdoor",
806
+ "coastal_waterfront"
807
+ ],
808
+ "urban_density": "mixed",
809
+ "climate_zone": "mediterranean",
810
+ "area_type": "historic_district"
811
+ },
812
+ "players": {
813
+ "count": 17,
814
+ "team_count": 5,
815
+ "age_group": "teens",
816
+ "mobility": "standard"
817
+ },
818
+ "preferences": {
819
+ "duration_minutes": 45,
820
+ "difficulty": "medium",
821
+ "theme": "urban_exploration",
822
+ "allow_transport": false
823
+ }
824
+ },
825
+ "output": {
826
+ "game_title": "Coastal Discovery Challenge",
827
+ "rules": {
828
+ "objective": "Teams must complete a series of urban exploration tasks by navigating through various districts, observing details, and providing proof of completion to earn points.",
829
+ "scoring_method": "point_accumulation",
830
+ "task_reveal_mode": "all_at_once",
831
+ "team_rules": "Teams of 3-4 players must stay together at all times and collaborate on all tasks. Only one submission per team per task is allowed.",
832
+ "time_limit_minutes": 45,
833
+ "disqualification_conditions": [
834
+ "Failure to adhere to safety guidelines",
835
+ "Damaging property or disrespecting public spaces",
836
+ "Using unauthorized transport",
837
+ "Splitting up from team members"
838
+ ]
839
+ },
840
+ "safety_constraints": {
841
+ "exclusion_zones": [
842
+ "private_property",
843
+ "active_roadway",
844
+ "construction_sites",
845
+ "restricted_government_buildings",
846
+ "water_edge"
847
+ ],
848
+ "physical_limits": [
849
+ "no climbing",
850
+ "no jumping",
851
+ "no water entry",
852
+ "no entering buildings"
853
+ ],
854
+ "adult_supervision_required": false,
855
+ "notes": "The region experiences a Mediterranean climate, characterized by warm, dry summers and mild, wet winters. Participants should be prepared for sun exposure and potential wind, and ensure adequate hydration throughout the game."
856
+ },
857
+ "tasks": [
858
+ {
859
+ "task_id": "T01",
860
+ "title": "Architectural Emblem",
861
+ "description": "Locate a building within the historic district that features a prominent, carved stone emblem above its main entrance. Photograph the emblem clearly.",
862
+ "landscape_tags_used": [
863
+ "historic_district"
864
+ ],
865
+ "task_type": "find_and_photograph",
866
+ "difficulty_contribution": "easy",
867
+ "points": 10,
868
+ "completion_proof": "A clear photograph of the carved stone emblem.",
869
+ "estimated_time_minutes": 5,
870
+ "hints": {
871
+ "hint_1": "Head towards the oldest section of the district, look for public buildings.",
872
+ "hint_2": "Seek out a structure with classical influences, often found near a central open space, and examine its upper facade details.",
873
+ "hint_3": "Walk along the main pedestrian thoroughfare in the original town center. Look for a large, public-facing building with a grand entrance; the emblem is often centrally placed above the door frame."
874
+ },
875
+ "safety_flags": []
876
+ },
877
+ {
878
+ "task_id": "T02",
879
+ "title": "Market's Sweetest Scent",
880
+ "description": "Navigate to the outdoor market. Find a stall selling dried fruit and identify the country of origin of the smallest, darkest variety available. Write down the country name.",
881
+ "landscape_tags_used": [
882
+ "market_outdoor"
883
+ ],
884
+ "task_type": "observe_and_answer",
885
+ "difficulty_contribution": "medium",
886
+ "points": 20,
887
+ "completion_proof": "The correct country name written on your task sheet.",
888
+ "estimated_time_minutes": 7,
889
+ "hints": {
890
+ "hint_1": "Enter the bustling market area and follow the aroma of spices and produce.",
891
+ "hint_2": "Look for vendors with a wide array of dried goods; the smallest fruit is often displayed in a basket or clear container.",
892
+ "hint_3": "Proceed into the heart of the outdoor market. Locate a stall with a prominent display of various dried fruits and nuts. Politely ask the vendor for the origin of the smallest, dark-colored dried fruit."
893
+ },
894
+ "safety_flags": []
895
+ },
896
+ {
897
+ "task_id": "T03",
898
+ "title": "Summit's Stone Cairn",
899
+ "description": "Travel towards the visible elevated area. Locate a small, deliberately stacked pile of stones near a viewpoint. Record the number of distinct stone layers in the cairn.",
900
+ "landscape_tags_used": [
901
+ "hill_or_elevation"
902
+ ],
903
+ "task_type": "reach_and_verify",
904
+ "difficulty_contribution": "easy",
905
+ "points": 10,
906
+ "completion_proof": "Record the number of distinct stone layers in the cairn.",
907
+ "estimated_time_minutes": 6,
908
+ "hints": {
909
+ "hint_1": "Ascend the gentle slope towards the most prominent natural high point.",
910
+ "hint_2": "Near the top, where the path levels out slightly and offers a panoramic view, look for a small, man-made stack of rocks.",
911
+ "hint_3": "Follow the path leading up the main hill. Once you reach the first major clearing with a wide vista, scan the immediate area, particularly near the edge of the overlook, for a small, conical pile of stones."
912
+ },
913
+ "safety_flags": [
914
+ "uneven_terrain"
915
+ ]
916
+ },
917
+ {
918
+ "task_id": "T04",
919
+ "title": "Artisan's Tale",
920
+ "description": "In the historic district or outdoor market, find a local artisan selling handcrafted goods (e.g., jewelry, pottery, textiles). Engage them in a brief conversation and learn one unique detail about their craft or inspiration. Record this detail.",
921
+ "landscape_tags_used": [
922
+ "historic_district",
923
+ "market_outdoor"
924
+ ],
925
+ "task_type": "social_interaction",
926
+ "difficulty_contribution": "medium",
927
+ "points": 20,
928
+ "completion_proof": "Write down the unique detail learned from the artisan.",
929
+ "estimated_time_minutes": 7,
930
+ "hints": {
931
+ "hint_1": "Look for smaller, independent shops or stalls, not large chain stores.",
932
+ "hint_2": "Approach a vendor displaying handmade items. Start by complimenting their work, then ask a question about the process or the story behind a piece.",
933
+ "hint_3": "Wander through the historic district's pedestrian lanes or the market's artisan section. Identify a stall or small shop with clearly handmade products. Engage the creator in conversation to learn a unique detail about their craft or inspiration."
934
+ },
935
+ "safety_flags": []
936
+ },
937
+ {
938
+ "task_id": "T05",
939
+ "title": "Waterfront Beacon",
940
+ "description": "From the coastal waterfront, locate a distinct, historic navigational aid (e.g., a small tower, a prominent light structure) that also features an old, weathered bell. Photograph the bell clearly.",
941
+ "landscape_tags_used": [
942
+ "coastal_waterfront",
943
+ "historic_district"
944
+ ],
945
+ "task_type": "find_and_photograph",
946
+ "difficulty_contribution": "hard",
947
+ "points": 30,
948
+ "completion_proof": "A clear photograph showing the weathered bell on the navigational aid.",
949
+ "estimated_time_minutes": 7,
950
+ "hints": {
951
+ "hint_1": "Head towards the oldest part of the waterfront where ships once docked.",
952
+ "hint_2": "Search near the edge of the harbor where the land meets the sea; the structure will be sturdy and designed for marine guidance.",
953
+ "hint_3": "Proceed along the waterfront promenade towards the historic harbor entrance. Look for an older, robust structure, like a small light tower or concrete marker, designed to guide vessels. The weathered metal bell will be integrated into its base or side."
954
+ },
955
+ "safety_flags": []
956
+ }
957
+ ],
958
+ "task_count": 5,
959
+ "total_possible_points": 90,
960
+ "max_deductible_points": 50,
961
+ "minimum_possible_points": 40,
962
+ "bonus_task_eligible": false,
963
+ "bonus_task": {
964
+ "description": null,
965
+ "points": null,
966
+ "risk": null,
967
+ "completion_proof": null
968
+ },
969
+ "scoring_summary": {
970
+ "base_points_available": 90,
971
+ "time_bonus_per_minute_early": null,
972
+ "hint_cost_tier_1": 5,
973
+ "hint_cost_tier_2": 10,
974
+ "team_aggregation_method": "sum_all_members",
975
+ "winning_condition_detail": "The team that accumulates the highest total points by successfully completing tasks and incurring the fewest hint penalties wins the game."
976
+ },
977
+ "estimated_total_time_minutes": 32,
978
+ "quality_score": 4.5
979
+ }
980
+ },
981
+ {
982
+ "id": "SH-NBO-0007",
983
+ "input": {
984
+ "game_type": "scavenger_hunt",
985
+ "location": {
986
+ "city": "Nairobi",
987
+ "country": "Kenya",
988
+ "city_code": "NBO",
989
+ "landscape_tags": [
990
+ "market_outdoor",
991
+ "residential_neighbourhood",
992
+ "dense_urban_grid",
993
+ "park_large"
994
+ ],
995
+ "urban_density": "mixed",
996
+ "climate_zone": "tropical",
997
+ "area_type": "city_center"
998
+ },
999
+ "players": {
1000
+ "count": 9,
1001
+ "team_count": 5,
1002
+ "age_group": "mixed_family",
1003
+ "mobility": "standard"
1004
+ },
1005
+ "preferences": {
1006
+ "duration_minutes": 45,
1007
+ "difficulty": "medium",
1008
+ "theme": "observation",
1009
+ "allow_transport": true
1010
+ }
1011
+ },
1012
+ "output": {
1013
+ "game_title": "Nairobi Urban Explorer Challenge",
1014
+ "rules": {
1015
+ "objective": "Teams must locate specific features, answer observation questions, and complete social interactions to accumulate the highest score within the time limit.",
1016
+ "scoring_method": "point_accumulation",
1017
+ "task_reveal_mode": "sequential",
1018
+ "team_rules": "Teams must stay together and collaborate on all tasks. Individual members are not permitted to split from the group.",
1019
+ "time_limit_minutes": 45,
1020
+ "disqualification_conditions": [
1021
+ "damaging property",
1022
+ "cheating or unfair play",
1023
+ "entering restricted areas",
1024
+ "disrespectful behavior towards locals"
1025
+ ]
1026
+ },
1027
+ "safety_constraints": {
1028
+ "exclusion_zones": [
1029
+ "private_property",
1030
+ "active_roadway",
1031
+ "construction_sites",
1032
+ "restricted_government_buildings"
1033
+ ],
1034
+ "physical_limits": [
1035
+ "no climbing",
1036
+ "no jumping",
1037
+ "no water entry",
1038
+ "no entering buildings"
1039
+ ],
1040
+ "adult_supervision_required": true,
1041
+ "notes": "Tropical climate — note heat and humidity. Participants must have access to shade within 10 minutes of completing each task to avoid heat-related issues."
1042
+ },
1043
+ "tasks": [
1044
+ {
1045
+ "task_id": "T01",
1046
+ "title": "Market Stall Colors",
1047
+ "description": "Navigate to the outdoor market area. Locate a stall that prominently displays at least three distinct colors of fresh produce.",
1048
+ "landscape_tags_used": [
1049
+ "market_outdoor"
1050
+ ],
1051
+ "task_type": "find_and_photograph",
1052
+ "difficulty_contribution": "easy",
1053
+ "points": 10,
1054
+ "completion_proof": "Submit a photograph showing the multi-colored produce stall clearly.",
1055
+ "estimated_time_minutes": 5,
1056
+ "hints": {
1057
+ "hint_1": "Head towards the central outdoor market area.",
1058
+ "hint_2": "Look for vendors selling fruits and vegetables. Many stalls will have a vibrant array of colors, focus on one with at least three different color groups.",
1059
+ "hint_3": "Once you are within the bustling market, move towards the section with fresh food. Find a vendor with a display of items like red tomatoes, green leafy vegetables, and yellow bananas or mangoes, ensuring three colors are very visible."
1060
+ },
1061
+ "safety_flags": [
1062
+ "watch for crowds",
1063
+ "uneven ground"
1064
+ ]
1065
+ },
1066
+ {
1067
+ "task_id": "T02",
1068
+ "title": "Residential Gate Count",
1069
+ "description": "Proceed to the nearby residential neighborhood. Observe five adjacent residences and count how many of them have a clearly visible house number or street number sign affixed to their gate or wall.",
1070
+ "landscape_tags_used": [
1071
+ "residential_neighbourhood"
1072
+ ],
1073
+ "task_type": "observe_and_answer",
1074
+ "difficulty_contribution": "medium",
1075
+ "points": 20,
1076
+ "completion_proof": "Report the exact count of residences (out of five) that displayed a number.",
1077
+ "estimated_time_minutes": 7,
1078
+ "hints": {
1079
+ "hint_1": "Walk into the residential zone just beyond the main urban grid.",
1080
+ "hint_2": "Select a continuous row of five houses along a single street. Carefully check each one's entrance or boundary for a number plate.",
1081
+ "hint_3": "From the edge of the dense urban grid, enter the residential area. Pick any five houses next to each other. For each house, check its main gate or the wall immediately beside it for a clearly posted numeric identifier, then tally your findings."
1082
+ },
1083
+ "safety_flags": [
1084
+ "watch for pedestrians"
1085
+ ]
1086
+ },
1087
+ {
1088
+ "task_id": "T03",
1089
+ "title": "Urban Grid Landmark",
1090
+ "description": "Within the dense urban grid, locate the tall, slender structure topped with a distinct, non-reflective, pointed finial that is visible from the main open square. You do not need to touch it.",
1091
+ "landscape_tags_used": [
1092
+ "dense_urban_grid"
1093
+ ],
1094
+ "task_type": "reach_and_verify",
1095
+ "difficulty_contribution": "hard",
1096
+ "points": 30,
1097
+ "completion_proof": "Describe the material of the finial and approximate its height in stories, relative to a typical building.",
1098
+ "estimated_time_minutes": 7,
1099
+ "hints": {
1100
+ "hint_1": "From the central open space, look upwards and around for the tallest point.",
1101
+ "hint_2": "Focus your attention on the most prominent vertical element standing far above the surrounding buildings. It is capped with a unique, pointed feature, not metal or glass.",
1102
+ "hint_3": "Stand in the middle of the large open square. Turn slowly, scanning the skyline for the highest structure. This structure has a very noticeable, non-shiny, sharp tip at its very top. You need to identify its specific material and estimate how many floors tall it looks."
1103
+ },
1104
+ "safety_flags": [
1105
+ "watch for traffic",
1106
+ "pedestrian crossings"
1107
+ ]
1108
+ },
1109
+ {
1110
+ "task_id": "T04",
1111
+ "title": "Park Visitor Inquiry",
1112
+ "description": "Enter the large public park. Approach a local individual who appears to be relaxing and politely ask them to describe their favorite tree within the park.",
1113
+ "landscape_tags_used": [
1114
+ "park_large"
1115
+ ],
1116
+ "task_type": "social_interaction",
1117
+ "difficulty_contribution": "easy",
1118
+ "points": 10,
1119
+ "completion_proof": "Report the type of tree mentioned by the individual and one reason they gave for liking it.",
1120
+ "estimated_time_minutes": 6,
1121
+ "hints": {
1122
+ "hint_1": "Find someone enjoying the park, then start a friendly conversation.",
1123
+ "hint_2": "Seek out someone sitting on a bench or under a tree. Introduce yourselves and ask them about their preferred tree type in the green space.",
1124
+ "hint_3": "Walk into the expansive park. Look for a person who seems approachable, perhaps reading or just observing. Greet them, explain you're on a hunt, and politely ask, \"Which tree in this park is your favorite, and why?\""
1125
+ },
1126
+ "safety_flags": [
1127
+ "be polite and respectful"
1128
+ ]
1129
+ },
1130
+ {
1131
+ "task_id": "T05",
1132
+ "title": "Distinctive Wall Art",
1133
+ "description": "Explore the transition zone between the residential area and the dense urban grid. Locate a piece of public wall art (mural or graffiti) that covers an area larger than two meters by two meters and includes at least three distinct colors.",
1134
+ "landscape_tags_used": [
1135
+ "residential_neighbourhood",
1136
+ "dense_urban_grid"
1137
+ ],
1138
+ "task_type": "find_and_photograph",
1139
+ "difficulty_contribution": "medium",
1140
+ "points": 20,
1141
+ "completion_proof": "Submit a photograph of the wall art, ensuring its size and colors are visible.",
1142
+ "estimated_time_minutes": 7,
1143
+ "hints": {
1144
+ "hint_1": "Search along the borders where houses meet taller buildings and shops.",
1145
+ "hint_2": "Look on large, exterior walls in areas with mixed building types. You are searching for a significant piece of artistic expression, not just a small tag, that uses multiple hues.",
1146
+ "hint_3": "Move from the quieter residential streets towards the busier urban blocks. Keep an eye on the side walls of buildings, especially larger ones. Find a substantial painting or design on a wall that is clearly bigger than a person and incorporates at least three different shades."
1147
+ },
1148
+ "safety_flags": [
1149
+ "watch for pedestrians",
1150
+ "uneven sidewalks"
1151
+ ]
1152
+ }
1153
+ ],
1154
+ "task_count": 5,
1155
+ "total_possible_points": 90,
1156
+ "max_deductible_points": 50,
1157
+ "minimum_possible_points": 40,
1158
+ "bonus_task_eligible": false,
1159
+ "bonus_task": {
1160
+ "description": null,
1161
+ "points": null,
1162
+ "risk": null,
1163
+ "completion_proof": null
1164
+ },
1165
+ "scoring_summary": {
1166
+ "base_points_available": 90,
1167
+ "time_bonus_per_minute_early": null,
1168
+ "hint_cost_tier_1": 5,
1169
+ "hint_cost_tier_2": 10,
1170
+ "team_aggregation_method": "sum_all_members",
1171
+ "winning_condition_detail": "The team with the highest total points after all tasks are scored and penalties applied will be declared the winner."
1172
+ },
1173
+ "estimated_total_time_minutes": 32,
1174
+ "quality_score": 4.5
1175
+ }
1176
+ },
1177
+ {
1178
+ "id": "SH-MEX-0008",
1179
+ "input": {
1180
+ "game_type": "scavenger_hunt",
1181
+ "location": {
1182
+ "city": "Mexico City",
1183
+ "country": "Mexico",
1184
+ "city_code": "MEX",
1185
+ "landscape_tags": [
1186
+ "street_art_district",
1187
+ "museum_cluster",
1188
+ "market_outdoor"
1189
+ ],
1190
+ "urban_density": "hyper_dense",
1191
+ "climate_zone": "temperate",
1192
+ "area_type": "market_district"
1193
+ },
1194
+ "players": {
1195
+ "count": 13,
1196
+ "team_count": 1,
1197
+ "age_group": "mixed_adults",
1198
+ "mobility": "limited"
1199
+ },
1200
+ "preferences": {
1201
+ "duration_minutes": 45,
1202
+ "difficulty": "easy",
1203
+ "theme": "urban_exploration",
1204
+ "allow_transport": false
1205
+ }
1206
+ },
1207
+ "output": {
1208
+ "game_title": "Urban Explorer's Quest: Market & Murals",
1209
+ "rules": {
1210
+ "objective": "Navigate the vibrant district, complete a series of observation and interaction tasks, and be the first team to finish.",
1211
+ "scoring_method": "first_to_finish",
1212
+ "task_reveal_mode": "sequential",
1213
+ "team_rules": "Teams must stay together at all times and work collaboratively on each task.",
1214
+ "time_limit_minutes": 45,
1215
+ "disqualification_conditions": [
1216
+ "Entering private property or restricted areas",
1217
+ "Causing damage to property or disturbing public order",
1218
+ "Using unauthorized transport methods",
1219
+ "Not adhering to safety guidelines"
1220
+ ]
1221
+ },
1222
+ "safety_constraints": {
1223
+ "exclusion_zones": [
1224
+ "private_property",
1225
+ "active_roadway",
1226
+ "construction_sites",
1227
+ "restricted_government_buildings"
1228
+ ],
1229
+ "physical_limits": [
1230
+ "no climbing",
1231
+ "no jumping",
1232
+ "no water entry",
1233
+ "no entering buildings"
1234
+ ],
1235
+ "adult_supervision_required": false,
1236
+ "notes": "The game takes place in a temperate climate; players should dress appropriately for outdoor activity and be prepared for walking in a hyper-dense urban environment."
1237
+ },
1238
+ "tasks": [
1239
+ {
1240
+ "task_id": "T01",
1241
+ "title": "Colorful Mural Discovery",
1242
+ "description": "Locate a large mural featuring at least three distinct colors within the designated art area and photograph it.",
1243
+ "landscape_tags_used": [
1244
+ "street_art_district"
1245
+ ],
1246
+ "task_type": "find_and_photograph",
1247
+ "difficulty_contribution": "easy",
1248
+ "points": 10,
1249
+ "completion_proof": "Submit a photo of the identified mural, clearly showing its colors.",
1250
+ "estimated_time_minutes": 8,
1251
+ "hints": {
1252
+ "hint_1": "Head towards the area known for vibrant wall paintings.",
1253
+ "hint_2": "Look for a prominent street artwork on a building's side, often near a public square or pedestrian zone, depicting a local theme.",
1254
+ "hint_3": "Proceed to the central pedestrian thoroughfare where artists frequently display their work. You're searching for a multi-colored artwork covering a significant portion of a wall, possibly near an outdoor cafe."
1255
+ },
1256
+ "safety_flags": [
1257
+ "none"
1258
+ ]
1259
+ },
1260
+ {
1261
+ "task_id": "T02",
1262
+ "title": "Market Stall Feature",
1263
+ "description": "In the outdoor market, find a stall selling fresh produce. Count how many different types of fruit are openly displayed and note the most common color.",
1264
+ "landscape_tags_used": [
1265
+ "market_outdoor"
1266
+ ],
1267
+ "task_type": "observe_and_answer",
1268
+ "difficulty_contribution": "easy",
1269
+ "points": 10,
1270
+ "completion_proof": "Provide the count of distinct fruit types and the most common color.",
1271
+ "estimated_time_minutes": 9,
1272
+ "hints": {
1273
+ "hint_1": "Walk into the bustling open-air trading area.",
1274
+ "hint_2": "Seek out a vendor's stand filled with nature's harvest, typically found on the main market path. Focus on items intended for eating raw.",
1275
+ "hint_3": "Navigate to the main artery of the outdoor market. You'll find many vendors; specifically look for one specializing in natural, unprocessed food items. The stall should have a wide variety visible."
1276
+ },
1277
+ "safety_flags": [
1278
+ "none"
1279
+ ]
1280
+ },
1281
+ {
1282
+ "task_id": "T03",
1283
+ "title": "Local Recommendation",
1284
+ "description": "Approach a vendor in a public-facing shop within the market district. Ask them for a recommendation of a local non-food item to buy as a souvenir. Report their suggestion.",
1285
+ "landscape_tags_used": [
1286
+ "market_outdoor"
1287
+ ],
1288
+ "task_type": "social_interaction",
1289
+ "difficulty_contribution": "easy",
1290
+ "points": 10,
1291
+ "completion_proof": "State the recommended non-food souvenir item.",
1292
+ "estimated_time_minutes": 9,
1293
+ "hints": {
1294
+ "hint_1": "Engage with a shopkeeper in the vibrant commerce zone.",
1295
+ "hint_2": "Find a small retail establishment in the market area, perhaps selling crafts or trinkets. Politely initiate a brief conversation with the person working there.",
1296
+ "hint_3": "Head towards the covered or semi-covered sections adjacent to the main outdoor market. Identify a shop with an open entrance selling various local goods. Approach the staff member and pose your question clearly."
1297
+ },
1298
+ "safety_flags": [
1299
+ "none"
1300
+ ]
1301
+ },
1302
+ {
1303
+ "task_id": "T04",
1304
+ "title": "Museum Quarter Sculpture",
1305
+ "description": "Near the museum cluster, locate a prominent outdoor sculpture. Verify if it depicts an animal and identify the material it is made from.",
1306
+ "landscape_tags_used": [
1307
+ "museum_cluster"
1308
+ ],
1309
+ "task_type": "reach_and_verify",
1310
+ "difficulty_contribution": "easy",
1311
+ "points": 10,
1312
+ "completion_proof": "Confirm if the sculpture is an animal and state its primary material.",
1313
+ "estimated_time_minutes": 8,
1314
+ "hints": {
1315
+ "hint_1": "Explore the open spaces surrounding the cultural institutions.",
1316
+ "hint_2": "Walk through the plazas and gardens between the various exhibition buildings. Look for a significant art piece that is freestanding and accessible to the public.",
1317
+ "hint_3": "Head to the main pedestrian avenue that connects several of the large exhibition halls. There is a notable artistic creation in a public garden or courtyard. Observe its form and composition closely."
1318
+ },
1319
+ "safety_flags": [
1320
+ "no entering buildings"
1321
+ ]
1322
+ }
1323
+ ],
1324
+ "task_count": 4,
1325
+ "total_possible_points": 40,
1326
+ "max_deductible_points": 40,
1327
+ "minimum_possible_points": 0,
1328
+ "bonus_task_eligible": false,
1329
+ "bonus_task": {
1330
+ "description": null,
1331
+ "points": null,
1332
+ "risk": null,
1333
+ "completion_proof": null
1334
+ },
1335
+ "scoring_summary": {
1336
+ "base_points_available": 40,
1337
+ "time_bonus_per_minute_early": null,
1338
+ "hint_cost_tier_1": 5,
1339
+ "hint_cost_tier_2": 10,
1340
+ "team_aggregation_method": null,
1341
+ "winning_condition_detail": "The first team to correctly complete all four tasks and return to the starting point wins the game."
1342
+ },
1343
+ "estimated_total_time_minutes": 34,
1344
+ "quality_score": 4.5
1345
+ }
1346
+ },
1347
+ {
1348
+ "id": "SH-MRK-0009",
1349
+ "input": {
1350
+ "game_type": "scavenger_hunt",
1351
+ "location": {
1352
+ "city": "Marrakech",
1353
+ "country": "Morocco",
1354
+ "city_code": "MRK",
1355
+ "landscape_tags": [
1356
+ "religious_site_accessible",
1357
+ "food_market",
1358
+ "plaza_or_square",
1359
+ "narrow_alley_network"
1360
+ ],
1361
+ "urban_density": "dense",
1362
+ "climate_zone": "arid",
1363
+ "area_type": "historic_district"
1364
+ },
1365
+ "players": {
1366
+ "count": 17,
1367
+ "team_count": 5,
1368
+ "age_group": "children_only",
1369
+ "mobility": "standard"
1370
+ },
1371
+ "preferences": {
1372
+ "duration_minutes": 60,
1373
+ "difficulty": "easy",
1374
+ "theme": "history",
1375
+ "allow_transport": true
1376
+ }
1377
+ },
1378
+ "output": {
1379
+ "game_title": "Ancient Paths Adventure",
1380
+ "rules": {
1381
+ "objective": "Explore the historic district, complete tasks to uncover cultural details, and earn points for your team by finding hidden features.",
1382
+ "scoring_method": "point_accumulation",
1383
+ "task_reveal_mode": "sequential",
1384
+ "team_rules": "Teams must stay together and work collaboratively. An adult supervisor must accompany each team at all times.",
1385
+ "time_limit_minutes": 60,
1386
+ "disqualification_conditions": [
1387
+ "Teams splitting up",
1388
+ "Entering forbidden zones",
1389
+ "Damaging property",
1390
+ "Disrespecting local customs",
1391
+ "Ignoring adult supervision directives"
1392
+ ]
1393
+ },
1394
+ "safety_constraints": {
1395
+ "exclusion_zones": [
1396
+ "private_property",
1397
+ "active_roadway",
1398
+ "construction_sites",
1399
+ "restricted_government_buildings",
1400
+ "religious_interiors"
1401
+ ],
1402
+ "physical_limits": [
1403
+ "no climbing",
1404
+ "no jumping",
1405
+ "no water entry",
1406
+ "no entering buildings"
1407
+ ],
1408
+ "adult_supervision_required": true,
1409
+ "notes": "Arid climate — mandatory water advisory; no sustained outdoor walk greater than 15 minutes per task. Always stay with your adult supervisor and remain vigilant in crowded areas."
1410
+ },
1411
+ "tasks": [
1412
+ {
1413
+ "task_id": "T01",
1414
+ "title": "Central Square Water Feature",
1415
+ "description": "Locate the most prominent water feature in the main open square. It might be surrounded by seating or a gathering spot.",
1416
+ "landscape_tags_used": [
1417
+ "plaza_or_square"
1418
+ ],
1419
+ "task_type": "find_and_photograph",
1420
+ "difficulty_contribution": "easy",
1421
+ "points": 10,
1422
+ "completion_proof": "A clear photograph of the water feature with at least two team members visible.",
1423
+ "estimated_time_minutes": 8,
1424
+ "hints": {
1425
+ "hint_1": "Head towards the center of the largest open area in the district.",
1426
+ "hint_2": "Look for a decorative structure that provides cool relief. It often has water gently flowing or spraying from it, a focal point of the area.",
1427
+ "hint_3": "From the main entrance to the open square, walk directly towards the center where the crowd usually gathers. You will find a large, ornate basin with water, often a meeting point."
1428
+ },
1429
+ "safety_flags": [
1430
+ "watch_for_crowds"
1431
+ ]
1432
+ },
1433
+ {
1434
+ "task_id": "T02",
1435
+ "title": "Alleyway Arch Count",
1436
+ "description": "Navigate into a narrow passage and count the number of arched doorways or passages you can see on one side as you walk for approximately 30 paces.",
1437
+ "landscape_tags_used": [
1438
+ "narrow_alley_network"
1439
+ ],
1440
+ "task_type": "observe_and_answer",
1441
+ "difficulty_contribution": "easy",
1442
+ "points": 10,
1443
+ "completion_proof": "The exact number of arches counted, reported to your supervisor.",
1444
+ "estimated_time_minutes": 9,
1445
+ "hints": {
1446
+ "hint_1": "Find an entry into a smaller, winding path from a main thoroughfare.",
1447
+ "hint_2": "Enter one of the smaller, covered pathways. The arches are part of the buildings or passages on either side of the path, not decorative street arches.",
1448
+ "hint_3": "From the food market, locate a small, shaded opening leading into a very narrow street. Walk slowly, counting each distinct archway on your right-hand side until you reach a cross path."
1449
+ },
1450
+ "safety_flags": [
1451
+ "stay_on_path",
1452
+ "watch_for_crowds"
1453
+ ]
1454
+ },
1455
+ {
1456
+ "task_id": "T03",
1457
+ "title": "Market Spice Inquiry",
1458
+ "description": "In the bustling food market, approach a vendor selling spices. Ask them to identify their most popular aromatic spice blend and report its name.",
1459
+ "landscape_tags_used": [
1460
+ "food_market"
1461
+ ],
1462
+ "task_type": "social_interaction",
1463
+ "difficulty_contribution": "easy",
1464
+ "points": 10,
1465
+ "completion_proof": "The name of the spice blend as told by the vendor, verified by supervisor.",
1466
+ "estimated_time_minutes": 9,
1467
+ "hints": {
1468
+ "hint_1": "Go into the area filled with many stalls selling food and goods.",
1469
+ "hint_2": "Look for stalls with colorful mounds of dried herbs and spices. Approach a friendly vendor and politely ask about their best-selling mix.",
1470
+ "hint_3": "Head to the section of the market where the air is filled with strong, pleasant scents. Find a stall displaying many different powders and dried leaves, then speak to the person behind the counter."
1471
+ },
1472
+ "safety_flags": [
1473
+ "watch_for_crowds",
1474
+ "respect_locals"
1475
+ ]
1476
+ },
1477
+ {
1478
+ "task_id": "T04",
1479
+ "title": "Religious Site Entrance Detail",
1480
+ "description": "Locate the main entrance of the large accessible religious structure. Find a decorative symbol carved or affixed above the main entry arch and describe it.",
1481
+ "landscape_tags_used": [
1482
+ "religious_site_accessible"
1483
+ ],
1484
+ "task_type": "reach_and_verify",
1485
+ "difficulty_contribution": "easy",
1486
+ "points": 10,
1487
+ "completion_proof": "A verbal description of the identified symbol, confirmed by supervisor.",
1488
+ "estimated_time_minutes": 8,
1489
+ "hints": {
1490
+ "hint_1": "Seek out the tallest, most ornate building with a distinctive tower.",
1491
+ "hint_2": "Walk to the primary public entrance of the grand religious building. Above the main door, observe the detailed artwork or emblem, often symmetrical and culturally significant.",
1492
+ "hint_3": "From the central open square, head towards the largest, most prominent building with a tall, slender tower. Find its largest doorway. Directly above this entrance, look for a unique carved design or pattern."
1493
+ },
1494
+ "safety_flags": [
1495
+ "respect_locals",
1496
+ "do_not_enter"
1497
+ ]
1498
+ },
1499
+ {
1500
+ "task_id": "T05",
1501
+ "title": "Market Cart Spotting",
1502
+ "description": "Near the edge of the main open square where it meets the food market, find a mobile cart specifically selling freshly squeezed fruit juice. Photograph it.",
1503
+ "landscape_tags_used": [
1504
+ "plaza_or_square",
1505
+ "food_market"
1506
+ ],
1507
+ "task_type": "find_and_photograph",
1508
+ "difficulty_contribution": "easy",
1509
+ "points": 10,
1510
+ "completion_proof": "A clear photograph of the juice cart with its vendor, showing team members nearby.",
1511
+ "estimated_time_minutes": 9,
1512
+ "hints": {
1513
+ "hint_1": "Go to the busy border where the big square meets the market stalls.",
1514
+ "hint_2": "Search for small, wheeled stands offering refreshing drinks. These carts are often colorful and have visible fresh fruit, usually found on the perimeter of the market near the plaza.",
1515
+ "hint_3": "Position yourself at the point where the wide, open square transitions into the narrower, crowded market lanes. Look for a vendor pushing a small, brightly colored cart displaying citrus fruits, ready to prepare drinks."
1516
+ },
1517
+ "safety_flags": [
1518
+ "watch_for_crowds"
1519
+ ]
1520
+ }
1521
+ ],
1522
+ "task_count": 5,
1523
+ "total_possible_points": 50,
1524
+ "max_deductible_points": 50,
1525
+ "minimum_possible_points": 0,
1526
+ "bonus_task_eligible": false,
1527
+ "bonus_task": {
1528
+ "description": null,
1529
+ "points": null,
1530
+ "risk": null,
1531
+ "completion_proof": null
1532
+ },
1533
+ "scoring_summary": {
1534
+ "base_points_available": 50,
1535
+ "time_bonus_per_minute_early": null,
1536
+ "hint_cost_tier_1": 5,
1537
+ "hint_cost_tier_2": 10,
1538
+ "team_aggregation_method": "sum_all_members",
1539
+ "winning_condition_detail": "The team with the highest total score after all tasks are submitted and points calculated wins the scavenger hunt."
1540
+ },
1541
+ "estimated_total_time_minutes": 43,
1542
+ "quality_score": 4.5
1543
+ }
1544
+ },
1545
+ {
1546
+ "id": "SH-MEX-0011",
1547
+ "input": {
1548
+ "game_type": "scavenger_hunt",
1549
+ "location": {
1550
+ "city": "Mexico City",
1551
+ "country": "Mexico",
1552
+ "city_code": "MEX",
1553
+ "landscape_tags": [
1554
+ "museum_cluster",
1555
+ "street_art_district",
1556
+ "plaza_or_square"
1557
+ ],
1558
+ "urban_density": "hyper_dense",
1559
+ "climate_zone": "temperate",
1560
+ "area_type": "park_district"
1561
+ },
1562
+ "players": {
1563
+ "count": 6,
1564
+ "team_count": 1,
1565
+ "age_group": "adults",
1566
+ "mobility": "limited"
1567
+ },
1568
+ "preferences": {
1569
+ "duration_minutes": 90,
1570
+ "difficulty": "easy",
1571
+ "theme": "logic",
1572
+ "allow_transport": false
1573
+ }
1574
+ },
1575
+ "output": {
1576
+ "game_title": "Logic Labyrinth: Park District Pursuit",
1577
+ "rules": {
1578
+ "objective": "Teams must navigate the designated park district, solve a series of logic-based challenges, and collect evidence to earn points before the time limit expires.",
1579
+ "scoring_method": "point_accumulation",
1580
+ "task_reveal_mode": "sequential",
1581
+ "team_rules": null,
1582
+ "time_limit_minutes": 90,
1583
+ "disqualification_conditions": [
1584
+ "Entering restricted zones",
1585
+ "Damaging property",
1586
+ "Disrespectful behavior towards locals or other teams",
1587
+ "Exceeding the time limit without completing the objective"
1588
+ ]
1589
+ },
1590
+ "safety_constraints": {
1591
+ "exclusion_zones": [
1592
+ "private_property",
1593
+ "active_roadway",
1594
+ "construction_sites",
1595
+ "restricted_government_buildings"
1596
+ ],
1597
+ "physical_limits": [
1598
+ "no climbing",
1599
+ "no jumping",
1600
+ "no water entry",
1601
+ "no entering buildings"
1602
+ ],
1603
+ "adult_supervision_required": false,
1604
+ "notes": "The game takes place in a temperate climate zone, so participants should dress appropriately for comfortable walking and be prepared for variable weather conditions throughout the day. No specific climate-related constraints are imposed beyond general preparedness."
1605
+ },
1606
+ "tasks": [
1607
+ {
1608
+ "task_id": "T01",
1609
+ "title": "Central Water Feature",
1610
+ "description": "Find the central decorative water feature in the main open square and photograph it.",
1611
+ "landscape_tags_used": [
1612
+ "plaza_or_square"
1613
+ ],
1614
+ "task_type": "find_and_photograph",
1615
+ "difficulty_contribution": "easy",
1616
+ "points": 10,
1617
+ "completion_proof": "Submit a photograph of the water feature.",
1618
+ "estimated_time_minutes": 10,
1619
+ "hints": {
1620
+ "hint_1": "Head towards the center of the largest open space.",
1621
+ "hint_2": "Look for the large, decorative water feature at the heart of the main public square, often surrounded by seating.",
1622
+ "hint_3": "From the starting point, proceed directly into the expansive central public square. Once in the square, locate the prominent, multi-tiered fountain or basin, which is usually a focal point for visitors."
1623
+ },
1624
+ "safety_flags": [
1625
+ "public_area",
1626
+ "uneven_surfaces"
1627
+ ]
1628
+ },
1629
+ {
1630
+ "task_id": "T02",
1631
+ "title": "Mural Flora Count",
1632
+ "description": "In the district known for vibrant murals, locate a mural depicting local flora. Count the distinct types of flowers shown and record the number.",
1633
+ "landscape_tags_used": [
1634
+ "street_art_district"
1635
+ ],
1636
+ "task_type": "observe_and_answer",
1637
+ "difficulty_contribution": "easy",
1638
+ "points": 10,
1639
+ "completion_proof": "Record the total number of distinct flower types observed in the mural.",
1640
+ "estimated_time_minutes": 12,
1641
+ "hints": {
1642
+ "hint_1": "Explore the visually rich areas with many wall paintings.",
1643
+ "hint_2": "Seek out the vibrant wall art in the district, specifically focusing on murals that clearly display various plant life.",
1644
+ "hint_3": "Wander through the sections renowned for their extensive public murals. Find a large artwork depicting plants, carefully count how many distinct types of flowers are clearly illustrated within that single piece."
1645
+ },
1646
+ "safety_flags": [
1647
+ "pedestrian_traffic",
1648
+ "uneven_surfaces"
1649
+ ]
1650
+ },
1651
+ {
1652
+ "task_id": "T03",
1653
+ "title": "Museum Hours Verification",
1654
+ "description": "Navigate to the entrance of the largest structure within the museum cluster. Verify its operational hours displayed near the main entrance.",
1655
+ "landscape_tags_used": [
1656
+ "museum_cluster"
1657
+ ],
1658
+ "task_type": "reach_and_verify",
1659
+ "difficulty_contribution": "easy",
1660
+ "points": 10,
1661
+ "completion_proof": "State the operating hours (e.g., 'Mon-Fri 9 AM - 5 PM, Sat 10 AM - 6 PM, Sun Closed').",
1662
+ "estimated_time_minutes": 10,
1663
+ "hints": {
1664
+ "hint_1": "Go to the main complex of cultural buildings.",
1665
+ "hint_2": "Locate the primary access point for the most imposing structure within the grouping of exhibition halls.",
1666
+ "hint_3": "Proceed to the cluster of large cultural institutions. Identify the biggest building and approach its primary entrance. Look for any signage displaying the days and hours it is open to the public."
1667
+ },
1668
+ "safety_flags": [
1669
+ "pedestrian_traffic",
1670
+ "public_area"
1671
+ ]
1672
+ },
1673
+ {
1674
+ "task_id": "T04",
1675
+ "title": "Street Art Seating",
1676
+ "description": "Locate a publicly accessible seating area within an open square that features elements of street art nearby. Take a photo of your team seated there.",
1677
+ "landscape_tags_used": [
1678
+ "plaza_or_square",
1679
+ "street_art_district"
1680
+ ],
1681
+ "task_type": "find_and_photograph",
1682
+ "difficulty_contribution": "easy",
1683
+ "points": 10,
1684
+ "completion_proof": "Submit a photograph of your team seated in the chosen area with street art visible.",
1685
+ "estimated_time_minutes": 12,
1686
+ "hints": {
1687
+ "hint_1": "Find a bench where street art is visible nearby.",
1688
+ "hint_2": "Search for a public sitting spot in an open area that has colorful murals or graffiti visible from it.",
1689
+ "hint_3": "Explore the open spaces and areas with public art. Find a bench or other designated public seating within a square or plaza where you can clearly see a piece of street art without moving."
1690
+ },
1691
+ "safety_flags": [
1692
+ "public_area",
1693
+ "pedestrian_traffic"
1694
+ ]
1695
+ },
1696
+ {
1697
+ "task_id": "T05",
1698
+ "title": "Local Bird Query",
1699
+ "description": "Approach a vendor or staff member near the museum entrances. Ask them to name one local bird species commonly seen in the park district.",
1700
+ "landscape_tags_used": [
1701
+ "museum_cluster"
1702
+ ],
1703
+ "task_type": "social_interaction",
1704
+ "difficulty_contribution": "easy",
1705
+ "points": 10,
1706
+ "completion_proof": "Record the name of the bird species provided by the person you interacted with.",
1707
+ "estimated_time_minutes": 10,
1708
+ "hints": {
1709
+ "hint_1": "Talk to someone working near an exhibition hall.",
1710
+ "hint_2": "Engage with an employee or vendor located close to the entryways of any of the large cultural buildings.",
1711
+ "hint_3": "In the vicinity of the main cultural buildings, approach a person who appears to be working there, such as a shop assistant or security guard. Politely ask them to name a type of bird commonly found in the surrounding green areas."
1712
+ },
1713
+ "safety_flags": [
1714
+ "public_interaction",
1715
+ "pedestrian_traffic"
1716
+ ]
1717
+ },
1718
+ {
1719
+ "task_id": "T06",
1720
+ "title": "Historic Plaque Year",
1721
+ "description": "In the vicinity of a large plaza, find an outdoor informational plaque describing the history of the surrounding area. Identify the earliest year mentioned on the plaque.",
1722
+ "landscape_tags_used": [
1723
+ "street_art_district",
1724
+ "plaza_or_square"
1725
+ ],
1726
+ "task_type": "observe_and_answer",
1727
+ "difficulty_contribution": "easy",
1728
+ "points": 10,
1729
+ "completion_proof": "State the earliest year found on the historical plaque.",
1730
+ "estimated_time_minutes": 10,
1731
+ "hints": {
1732
+ "hint_1": "Look for a historical marker near a big public space.",
1733
+ "hint_2": "Find an outdoor sign providing historical information about the surrounding area, specifically in or around a major open square.",
1734
+ "hint_3": "Go to the large central open square. Search the perimeter or nearby walkways for a sturdy, permanent display board that details the history of this location, and find the earliest date mentioned."
1735
+ },
1736
+ "safety_flags": [
1737
+ "public_area",
1738
+ "uneven_surfaces"
1739
+ ]
1740
+ }
1741
+ ],
1742
+ "task_count": 6,
1743
+ "total_possible_points": 60,
1744
+ "max_deductible_points": 60,
1745
+ "minimum_possible_points": 0,
1746
+ "bonus_task_eligible": false,
1747
+ "bonus_task": {
1748
+ "description": null,
1749
+ "points": null,
1750
+ "risk": null,
1751
+ "completion_proof": null
1752
+ },
1753
+ "scoring_summary": {
1754
+ "base_points_available": 60,
1755
+ "time_bonus_per_minute_early": null,
1756
+ "hint_cost_tier_1": 5,
1757
+ "hint_cost_tier_2": 10,
1758
+ "team_aggregation_method": null,
1759
+ "winning_condition_detail": "The team that accumulates the highest total score by completing tasks and minimizing hint usage within the time limit will be declared the winner."
1760
+ },
1761
+ "estimated_total_time_minutes": 64,
1762
+ "quality_score": 4.5
1763
+ }
1764
+ },
1765
+ {
1766
+ "id": "SH-BER-0012",
1767
+ "input": {
1768
+ "game_type": "scavenger_hunt",
1769
+ "location": {
1770
+ "city": "Berlin",
1771
+ "country": "Germany",
1772
+ "city_code": "BER",
1773
+ "landscape_tags": [
1774
+ "street_art_district",
1775
+ "cafe_dense",
1776
+ "museum_cluster"
1777
+ ],
1778
+ "urban_density": "dense",
1779
+ "climate_zone": "continental",
1780
+ "area_type": "city_center"
1781
+ },
1782
+ "players": {
1783
+ "count": 15,
1784
+ "team_count": 4,
1785
+ "age_group": "mixed_adults",
1786
+ "mobility": "standard"
1787
+ },
1788
+ "preferences": {
1789
+ "duration_minutes": 45,
1790
+ "difficulty": "hard",
1791
+ "theme": "observation",
1792
+ "allow_transport": false
1793
+ }
1794
+ },
1795
+ "output": {
1796
+ "game_title": "Urban Explorer: Observation Challenge Berlin",
1797
+ "rules": {
1798
+ "objective": "Teams must navigate the city center, locate specific features, and accurately complete tasks focused on keen observation within the allotted time.",
1799
+ "scoring_method": "point_accumulation",
1800
+ "task_reveal_mode": "sequential",
1801
+ "team_rules": "Teams of 3-4 players must stay together at all times. All members contribute to task completion.",
1802
+ "time_limit_minutes": 45,
1803
+ "disqualification_conditions": [
1804
+ "failure_to_adhere_to_safety_rules",
1805
+ "entering_restricted_zones",
1806
+ "damaging_property",
1807
+ "cheating",
1808
+ "harassing_public"
1809
+ ]
1810
+ },
1811
+ "safety_constraints": {
1812
+ "exclusion_zones": [
1813
+ "private_property",
1814
+ "active_roadway",
1815
+ "construction_sites",
1816
+ "restricted_government_buildings"
1817
+ ],
1818
+ "physical_limits": [
1819
+ "no climbing",
1820
+ "no jumping",
1821
+ "no water entry",
1822
+ "no entering buildings"
1823
+ ],
1824
+ "adult_supervision_required": false,
1825
+ "notes": "Continental climate - dress appropriately for potentially variable weather conditions. Stay hydrated and be aware of your surroundings at all times."
1826
+ },
1827
+ "tasks": [
1828
+ {
1829
+ "task_id": "T01",
1830
+ "title": "Mural of the Wild",
1831
+ "description": "Locate a distinct, brightly colored mural depicting an animal in the street art district.",
1832
+ "landscape_tags_used": [
1833
+ "street_art_district"
1834
+ ],
1835
+ "task_type": "find_and_photograph",
1836
+ "difficulty_contribution": "easy",
1837
+ "points": 10,
1838
+ "completion_proof": "Photo of the team member next to the mural.",
1839
+ "estimated_time_minutes": 5,
1840
+ "hints": {
1841
+ "hint_1": "Head towards the district known for vibrant wall paintings. Look for large, colorful depictions.",
1842
+ "hint_2": "Focus on the more open areas of the street art district. Search for a mural that features an animal, recognizable by its bright, eye-catching colors.",
1843
+ "hint_3": "In the heart of the street art district, near the larger thoroughfares, many walls are covered. Specifically, seek out a substantial painting on a building wall that vividly illustrates an animal figure, easily identifiable by its bold color palette."
1844
+ },
1845
+ "safety_flags": []
1846
+ },
1847
+ {
1848
+ "task_id": "T02",
1849
+ "title": "Cafe Greenery Count",
1850
+ "description": "Find a cafe with outdoor seating that has at least three different types of potted plants near its entrance. Count the total number of outdoor tables.",
1851
+ "landscape_tags_used": [
1852
+ "cafe_dense"
1853
+ ],
1854
+ "task_type": "observe_and_answer",
1855
+ "difficulty_contribution": "medium",
1856
+ "points": 20,
1857
+ "completion_proof": "State the number of outdoor tables and identify the plant types.",
1858
+ "estimated_time_minutes": 7,
1859
+ "hints": {
1860
+ "hint_1": "Explore the area with many outdoor cafes. Pay attention to plant decorations.",
1861
+ "hint_2": "Locate a cafe that offers seating outside. Count the individual tables set up for customers in the open air. Check for diverse plant arrangements.",
1862
+ "hint_3": "Wander through the zone abundant with coffee shops. Find one with a significant outdoor dining section. Once there, precisely count the number of tables available for patrons outside the building and identify at least three distinct species of plants potted near the entrance."
1863
+ },
1864
+ "safety_flags": []
1865
+ },
1866
+ {
1867
+ "task_id": "T03",
1868
+ "title": "Flowing Metal Art",
1869
+ "description": "Navigate to the central pedestrian area within the museum cluster. Find a public art installation made primarily of metal that incorporates flowing lines. Verify its presence.",
1870
+ "landscape_tags_used": [
1871
+ "museum_cluster"
1872
+ ],
1873
+ "task_type": "reach_and_verify",
1874
+ "difficulty_contribution": "hard",
1875
+ "points": 30,
1876
+ "completion_proof": "Take a photo of the team's designated token placed at the base of the metal art installation.",
1877
+ "estimated_time_minutes": 7,
1878
+ "hints": {
1879
+ "hint_1": "Go to the main walking path in the area surrounded by cultural institutions.",
1880
+ "hint_2": "Within the central pedestrian zone of the museum cluster, seek out a piece of public art. It will be constructed from metal and characterized by smooth, curved forms.",
1881
+ "hint_3": "Proceed to the primary open space within the cluster of significant cultural buildings. Look for a large, abstract sculpture, often found near a main intersection, that is crafted entirely from metal and features elegant, flowing, non-linear designs. Place your token at its base."
1882
+ },
1883
+ "safety_flags": []
1884
+ },
1885
+ {
1886
+ "task_id": "T04",
1887
+ "title": "Local Beverage Inquiry",
1888
+ "description": "Approach a local establishment within the cafe-dense area. Ask a staff member for a recommendation for a traditional local beverage. Note their suggestion.",
1889
+ "landscape_tags_used": [
1890
+ "cafe_dense"
1891
+ ],
1892
+ "task_type": "social_interaction",
1893
+ "difficulty_contribution": "medium",
1894
+ "points": 20,
1895
+ "completion_proof": "State the recommended beverage and the general type of establishment.",
1896
+ "estimated_time_minutes": 7,
1897
+ "hints": {
1898
+ "hint_1": "Enter a public-facing shop in the cafe-dense zone.",
1899
+ "hint_2": "In the lively cafe district, find a small local business that serves food or drinks. Politely ask one of the staff members for their top recommendation regarding a local, traditional drink.",
1900
+ "hint_3": "Choose any cafe, bakery, or small eatery in the bustling cafe-dense area. Approach an employee, introduce yourselves, and inquire about a specific beverage that is considered a local specialty or traditional offering. Remember their suggestion."
1901
+ },
1902
+ "safety_flags": [
1903
+ "interact_respectfully"
1904
+ ]
1905
+ },
1906
+ {
1907
+ "task_id": "T05",
1908
+ "title": "Integrated Artistry",
1909
+ "description": "In the area where street art meets the museum cluster, find a prominent architectural feature (like a unique archway or a patterned facade) that has subtle street art elements integrated into it.",
1910
+ "landscape_tags_used": [
1911
+ "street_art_district",
1912
+ "museum_cluster"
1913
+ ],
1914
+ "task_type": "find_and_photograph",
1915
+ "difficulty_contribution": "hard",
1916
+ "points": 30,
1917
+ "completion_proof": "Photo of the architectural feature, clearly showing the integrated street art.",
1918
+ "estimated_time_minutes": 7,
1919
+ "hints": {
1920
+ "hint_1": "Move to the border where the street art and museum areas meet.",
1921
+ "hint_2": "Look for a building structure that stands out architecturally, perhaps an archway or a facade with a unique pattern. It will have subtle artistic additions.",
1922
+ "hint_3": "At the intersection of the vibrant street art district and the more formal museum cluster, find a prominent building. Observe its design for a striking architectural element, such as an unusual arch or a distinctively patterned wall, which also subtly incorporates small, artistic painted details or graffiti."
1923
+ },
1924
+ "safety_flags": []
1925
+ }
1926
+ ],
1927
+ "task_count": 5,
1928
+ "total_possible_points": 110,
1929
+ "max_deductible_points": 50,
1930
+ "minimum_possible_points": 60,
1931
+ "bonus_task_eligible": false,
1932
+ "bonus_task": {
1933
+ "description": null,
1934
+ "points": null,
1935
+ "risk": null,
1936
+ "completion_proof": null
1937
+ },
1938
+ "scoring_summary": {
1939
+ "base_points_available": 110,
1940
+ "time_bonus_per_minute_early": null,
1941
+ "hint_cost_tier_1": 5,
1942
+ "hint_cost_tier_2": 10,
1943
+ "team_aggregation_method": "sum_all_members",
1944
+ "winning_condition_detail": "The team with the highest total points after all deductions for hints and penalties, within the time limit, will be declared the winner."
1945
+ },
1946
+ "estimated_total_time_minutes": 33,
1947
+ "quality_score": 4.8
1948
+ }
1949
+ },
1950
+ {
1951
+ "id": "SH-MEX-0013",
1952
+ "input": {
1953
+ "game_type": "scavenger_hunt",
1954
+ "location": {
1955
+ "city": "Mexico City",
1956
+ "country": "Mexico",
1957
+ "city_code": "MEX",
1958
+ "landscape_tags": [
1959
+ "plaza_or_square",
1960
+ "market_outdoor",
1961
+ "dense_urban_grid",
1962
+ "museum_cluster",
1963
+ "street_art_district"
1964
+ ],
1965
+ "urban_density": "hyper_dense",
1966
+ "climate_zone": "temperate",
1967
+ "area_type": "waterfront"
1968
+ },
1969
+ "players": {
1970
+ "count": 5,
1971
+ "team_count": 2,
1972
+ "age_group": "mixed_family",
1973
+ "mobility": "standard"
1974
+ },
1975
+ "preferences": {
1976
+ "duration_minutes": 120,
1977
+ "difficulty": "easy",
1978
+ "theme": "logic",
1979
+ "allow_transport": false
1980
+ }
1981
+ },
1982
+ "output": {
1983
+ "game_title": "Mexico City Urban Explorer Challenge",
1984
+ "rules": {
1985
+ "objective": "Teams must complete a series of logic-based tasks by observing and interacting with the unique features of the urban landscape to earn the most points within the allotted time.",
1986
+ "scoring_method": "point_accumulation",
1987
+ "task_reveal_mode": "all_at_once",
1988
+ "team_rules": "Players are divided into two teams. Each team works collaboratively to complete tasks and submit proofs. All members' efforts contribute to the team's single score.",
1989
+ "time_limit_minutes": 120,
1990
+ "disqualification_conditions": [
1991
+ "entering restricted areas",
1992
+ "damaging property",
1993
+ "cheating or unfair play",
1994
+ "disrespecting locals or other players",
1995
+ "violating safety guidelines"
1996
+ ]
1997
+ },
1998
+ "safety_constraints": {
1999
+ "exclusion_zones": [
2000
+ "private_property",
2001
+ "active_roadway",
2002
+ "construction_sites",
2003
+ "restricted_government_buildings"
2004
+ ],
2005
+ "physical_limits": [
2006
+ "no climbing",
2007
+ "no jumping",
2008
+ "no water entry",
2009
+ "no entering buildings"
2010
+ ],
2011
+ "adult_supervision_required": true,
2012
+ "notes": "Temperate climate — no special constraints. Be aware of dense urban pedestrian traffic and maintain vigilance in crowded areas."
2013
+ },
2014
+ "tasks": [
2015
+ {
2016
+ "task_id": "T01",
2017
+ "title": "The Central Sentinel",
2018
+ "description": "Locate a prominent, non-residential building near the main open square that features a distinct clock or bell tower. This structure must be visible from the plaza.",
2019
+ "landscape_tags_used": [
2020
+ "plaza_or_square",
2021
+ "dense_urban_grid"
2022
+ ],
2023
+ "task_type": "find_and_photograph",
2024
+ "difficulty_contribution": "easy",
2025
+ "points": 10,
2026
+ "completion_proof": "A photo showing the entire team and the base of the identified structure.",
2027
+ "estimated_time_minutes": 12,
2028
+ "hints": {
2029
+ "hint_1": "Head towards the large open space, then look for something tall.",
2030
+ "hint_2": "From the center of the main square, scan the surrounding buildings for a structure with a time-telling feature high up.",
2031
+ "hint_3": "Walk to the very middle of the main open plaza. Look towards the largest building on its edge that is clearly not a residence. You should see a tower with a clock or bell at its peak. Capture it.",
2032
+ "hint_4": null
2033
+ },
2034
+ "safety_flags": [
2035
+ "pedestrian_awareness"
2036
+ ]
2037
+ },
2038
+ {
2039
+ "task_id": "T02",
2040
+ "title": "Market Colors",
2041
+ "description": "In the outdoor market, find a stall selling fresh produce that displays at least three distinct natural colors of a single type of fruit or vegetable.",
2042
+ "landscape_tags_used": [
2043
+ "market_outdoor"
2044
+ ],
2045
+ "task_type": "observe_and_answer",
2046
+ "difficulty_contribution": "easy",
2047
+ "points": 10,
2048
+ "completion_proof": "Write down the specific fruit/vegetable and its three different colors.",
2049
+ "estimated_time_minutes": 10,
2050
+ "hints": {
2051
+ "hint_1": "Navigate into the bustling open-air market area.",
2052
+ "hint_2": "Look for vendors with vibrant displays of fruits or vegetables. You need one item with color variations.",
2053
+ "hint_3": "Enter the main outdoor market. Search among the fruit and vegetable stalls for a vendor selling items like peppers, tomatoes, or apples where you can clearly see three different natural color variations of the same product. Note them.",
2054
+ "hint_4": null
2055
+ },
2056
+ "safety_flags": [
2057
+ "pedestrian_awareness",
2058
+ "crowd_management"
2059
+ ]
2060
+ },
2061
+ {
2062
+ "task_id": "T03",
2063
+ "title": "Hidden Mural Beast",
2064
+ "description": "In the street art district, find a mural that prominently features an animal, whether it is real or mythical in nature.",
2065
+ "landscape_tags_used": [
2066
+ "street_art_district"
2067
+ ],
2068
+ "task_type": "find_and_photograph",
2069
+ "difficulty_contribution": "easy",
2070
+ "points": 10,
2071
+ "completion_proof": "A photo of the team standing clearly in front of the identified animal mural.",
2072
+ "estimated_time_minutes": 12,
2073
+ "hints": {
2074
+ "hint_1": "Go to the area known for its vibrant wall paintings.",
2075
+ "hint_2": "Explore the murals in the art district; some depict creatures. Find one easily visible from the sidewalk.",
2076
+ "hint_3": "Head into the designated street art zone. Walk along the main thoroughfares and smaller alleys. Many large paintings adorn the walls. Locate one specifically showcasing any kind of animal, whether it exists in reality or folklore.",
2077
+ "hint_4": null
2078
+ },
2079
+ "safety_flags": [
2080
+ "pedestrian_awareness"
2081
+ ]
2082
+ },
2083
+ {
2084
+ "task_id": "T04",
2085
+ "title": "Museum District Emblem",
2086
+ "description": "Near the cluster of cultural institutions, locate a public sculpture or monument that incorporates a symbol widely associated with knowledge or discovery.",
2087
+ "landscape_tags_used": [
2088
+ "museum_cluster",
2089
+ "dense_urban_grid"
2090
+ ],
2091
+ "task_type": "reach_and_verify",
2092
+ "difficulty_contribution": "easy",
2093
+ "points": 10,
2094
+ "completion_proof": "Describe the specific symbol found and its exact location relative to the nearest large building.",
2095
+ "estimated_time_minutes": 12,
2096
+ "hints": {
2097
+ "hint_1": "Walk towards the concentration of museums.",
2098
+ "hint_2": "In front of or between the museum buildings, search for a statue or artistic installation with a meaningful motif.",
2099
+ "hint_3": "Proceed to the area where several museums are grouped closely. Look for any public artwork, such as a statue or abstract sculpture, displayed outdoors. Identify if it contains an image representing learning, science, or exploration.",
2100
+ "hint_4": null
2101
+ },
2102
+ "safety_flags": [
2103
+ "pedestrian_awareness"
2104
+ ]
2105
+ },
2106
+ {
2107
+ "task_id": "T05",
2108
+ "title": "Plaza's Fountain Secret",
2109
+ "description": "Return to the main open square and observe the largest active water feature. Count the number of distinct jets or spouts actively producing water.",
2110
+ "landscape_tags_used": [
2111
+ "plaza_or_square"
2112
+ ],
2113
+ "task_type": "observe_and_answer",
2114
+ "difficulty_contribution": "easy",
2115
+ "points": 10,
2116
+ "completion_proof": "Report the exact numerical count of active water jets or spouts.",
2117
+ "estimated_time_minutes": 10,
2118
+ "hints": {
2119
+ "hint_1": "Head back to the large central open space.",
2120
+ "hint_2": "Find the biggest water display. Count carefully how many streams of water are actively flowing from it.",
2121
+ "hint_3": "Go back to the prominent main open plaza. Locate the largest fountain or water installation. Approach it closely enough to accurately count every single point from which water is actively being propelled into the air or into a basin.",
2122
+ "hint_4": null
2123
+ },
2124
+ "safety_flags": [
2125
+ "pedestrian_awareness"
2126
+ ]
2127
+ },
2128
+ {
2129
+ "task_id": "T06",
2130
+ "title": "Market Vendor Greeting",
2131
+ "description": "In the outdoor market, approach a vendor selling handcrafted items and politely ask them about the origin of their materials.",
2132
+ "landscape_tags_used": [
2133
+ "market_outdoor"
2134
+ ],
2135
+ "task_type": "social_interaction",
2136
+ "difficulty_contribution": "easy",
2137
+ "points": 10,
2138
+ "completion_proof": "Report the type of craft being sold and a specific material origin mentioned by the vendor.",
2139
+ "estimated_time_minutes": 12,
2140
+ "hints": {
2141
+ "hint_1": "Re-enter the lively outdoor market.",
2142
+ "hint_2": "Look for stalls selling unique handmade goods. Choose a friendly vendor and initiate a brief conversation.",
2143
+ "hint_3": "Go back into the outdoor market. Find a stall that clearly displays items made by hand, such as jewelry, textiles, or pottery. Politely engage the person running the stall and inquire about where they source the raw materials for their creations.",
2144
+ "hint_4": null
2145
+ },
2146
+ "safety_flags": [
2147
+ "pedestrian_awareness",
2148
+ "crowd_management",
2149
+ "stranger_interaction_guidance"
2150
+ ]
2151
+ },
2152
+ {
2153
+ "task_id": "T07",
2154
+ "title": "Alleyway Mosaic",
2155
+ "description": "In a narrow pedestrian passage within the dense urban grid, find a small mosaic art piece embedded into a wall or the sidewalk.",
2156
+ "landscape_tags_used": [
2157
+ "dense_urban_grid",
2158
+ "street_art_district"
2159
+ ],
2160
+ "task_type": "find_and_photograph",
2161
+ "difficulty_contribution": "easy",
2162
+ "points": 10,
2163
+ "completion_proof": "A photo of the mosaic with at least one team member's hand pointing to it.",
2164
+ "estimated_time_minutes": 12,
2165
+ "hints": {
2166
+ "hint_1": "Explore the smaller paths between buildings in the urban area.",
2167
+ "hint_2": "Look carefully at walls and ground in less-trafficked alleyways for a small artwork made of many tiny pieces.",
2168
+ "hint_3": "Wander into the narrower alleys and pedestrian-only passages away from the main streets in the core urban area. Keep an eye on the surfaces of buildings and the ground. You are looking for a compact piece of art created from small tiles or fragments.",
2169
+ "hint_4": null
2170
+ },
2171
+ "safety_flags": [
2172
+ "pedestrian_awareness"
2173
+ ]
2174
+ },
2175
+ {
2176
+ "task_id": "T08",
2177
+ "title": "Public Seating Count",
2178
+ "description": "Find the largest open public space with designated seating areas. Count the total number of individual public seats available for use.",
2179
+ "landscape_tags_used": [
2180
+ "plaza_or_square",
2181
+ "dense_urban_grid"
2182
+ ],
2183
+ "task_type": "reach_and_verify",
2184
+ "difficulty_contribution": "easy",
2185
+ "points": 10,
2186
+ "completion_proof": "Report the exact numerical count of individual public seats found in the largest space.",
2187
+ "estimated_time_minutes": 12,
2188
+ "hints": {
2189
+ "hint_1": "Head towards a wide-open area with places to sit.",
2190
+ "hint_2": "Locate the most expansive plaza or park-like space that provides numerous benches or chairs for the public.",
2191
+ "hint_3": "Identify the most spacious public square or open area you can find. Once there, meticulously count every single individual seating spot provided for public use, including benches and standalone chairs. Ensure you count only distinct seating positions.",
2192
+ "hint_4": null
2193
+ },
2194
+ "safety_flags": [
2195
+ "pedestrian_awareness"
2196
+ ]
2197
+ }
2198
+ ],
2199
+ "task_count": 8,
2200
+ "total_possible_points": 80,
2201
+ "max_deductible_points": 80,
2202
+ "minimum_possible_points": 0,
2203
+ "bonus_task_eligible": false,
2204
+ "bonus_task": {
2205
+ "description": null,
2206
+ "points": null,
2207
+ "risk": null,
2208
+ "completion_proof": null
2209
+ },
2210
+ "scoring_summary": {
2211
+ "base_points_available": 80,
2212
+ "time_bonus_per_minute_early": null,
2213
+ "hint_cost_tier_1": 5,
2214
+ "hint_cost_tier_2": 10,
2215
+ "team_aggregation_method": "sum_all_members",
2216
+ "winning_condition_detail": "The team with the highest total score, after all points and deductions, by the end of the time limit will be declared the winner."
2217
+ },
2218
+ "estimated_total_time_minutes": 92,
2219
+ "quality_score": 4.5
2220
+ }
2221
+ }
2222
+ ]
requirements.txt CHANGED
@@ -19,4 +19,7 @@ soundfile
19
  librosa
20
  sentencepiece
21
  protobuf
22
- # modal # uncomment to deploy to Modal cloud GPU
 
 
 
 
19
  librosa
20
  sentencepiece
21
  protobuf
22
+ # modal # uncomment to deploy to Modal cloud GPU
23
+ # Synthetic training-data generation (scripts/<game>/)
24
+ google-genai
25
+ python-Levenshtein
scripts/generate_dataset_validated.py DELETED
@@ -1,622 +0,0 @@
1
- """
2
- Production-grade scavenger hunt dataset generator with validation pipeline.
3
- Incorporates all critical fixes from the audit.
4
-
5
- Usage:
6
- python generate_dataset.py --batches 10 --output dataset.json --validate strict
7
-
8
- Output:
9
- - dataset.json: All validated examples
10
- - generation_log.json: Per-batch metadata (cities, ages, difficulties, quality scores)
11
- - validation_errors.jsonl: All rejected examples with failure reasons
12
- """
13
-
14
- import os
15
- import json
16
- import time
17
- import logging
18
- import argparse
19
- import random
20
- import re
21
- from datetime import datetime
22
- from collections import Counter
23
- from typing import Dict, List, Optional, Tuple
24
- from dataclasses import dataclass, asdict
25
- from pathlib import Path
26
-
27
- # Gemini/Claude API (install: pip install google-genai or anthropic)
28
- try:
29
- from google import genai
30
- from google.genai import types
31
- HAS_GEMINI = True
32
- except:
33
- HAS_GEMINI = False
34
-
35
- try:
36
- from anthropic import Anthropic
37
- HAS_ANTHROPIC = True
38
- except:
39
- HAS_ANTHROPIC = False
40
-
41
- # String similarity (install: pip install python-Levenshtein)
42
- try:
43
- from Levenshtein import distance as levenshtein_distance
44
- except:
45
- def levenshtein_distance(s1, s2):
46
- """Fallback Levenshtein distance (naive)."""
47
- if len(s1) < len(s2):
48
- return levenshtein_distance(s2, s1)
49
- if len(s2) == 0:
50
- return len(s1)
51
- previous_row = range(len(s2) + 1)
52
- for i, c1 in enumerate(s1):
53
- current_row = [i + 1]
54
- for j, c2 in enumerate(s2):
55
- insertions = previous_row[j + 1] + 1
56
- deletions = current_row[j] + 1
57
- substitutions = previous_row[j] + (c1 != c2)
58
- current_row.append(min(insertions, deletions, substitutions))
59
- previous_row = current_row
60
- return previous_row[-1]
61
-
62
- # Configure logging
63
- logging.basicConfig(
64
- level=logging.INFO,
65
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
66
- )
67
- logger = logging.getLogger(__name__)
68
-
69
- # ============================================================================
70
- # VALIDATION RULES & CONSTANTS
71
- # ============================================================================
72
-
73
- PROPER_NOUN_PATTERNS = [
74
- r'\b(?:Eiffel|Tower|Big|Ben|Statue|Liberty|Colosseum|Tower|Bridge|Cathedral|Basilica|'
75
- r'Notre|Dame|Louvre|Vatican|Kremlin|Parliament|Thames|Seine|Nile|Amazon|'
76
- r'Paris|London|Tokyo|NYC|New York|Sydney|Berlin|Amsterdam|Istanbul|Rome|'
77
- r'Bangkok|Dubai|Shanghai|Hong Kong|Singapore|Jakarta|Mumbai|Delhi|Cairo|Lagos|'
78
- r'Rio|Mexico|Buenos Aires|Toronto|Vancouver|Moscow|Beijing|Seoul|Bangkok|'
79
- r'Eiffel Tower|Big Ben|Statue of Liberty|Colosseum|Tower Bridge|'
80
- r'Taj Mahal|Great Wall|Golden Gate|Christ Redeemer|Sagrada Familia|'
81
- r'Plaza Mayor|Red Square|Tiananmen|Shibuya|Senso-?ji|Forbidden City|'
82
- r'Buckingham|Westminster|Versailles|Schönbrunn|Prado|Hermitage|'
83
- r'Leaning Tower|Pantheon|Arc de Triomphe|Champs|Elysées|Fifth Avenue|'
84
- r'Times Square|Central Park|Hyde Park|Regent Park|Golden Gate|'
85
- r'Brooklyn Bridge|Tower Bridge|Millennium Bridge|Rialto|'
86
- r'Vatican|Kremlin|Parliament|Congress|House|Senate|White|House|'
87
- r'McDonald|Starbucks|Coca|Pepsi|Amazon|Google|Apple|Microsoft|Facebook)\b',
88
- r'\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)+\b' # Capitalized proper nouns (Name of Landmark)
89
- ]
90
-
91
- CITY_BANK = {
92
- 'Paris': 'PAR', 'Tokyo': 'TYO', 'New York City': 'NYC', 'Cape Town': 'CPT',
93
- 'Marrakech': 'MRK', 'Buenos Aires': 'BUE', 'Mumbai': 'BOM', 'Berlin': 'BER',
94
- 'Sydney': 'SYD', 'Nairobi': 'NBO', 'Istanbul': 'IST', 'Mexico City': 'MEX',
95
- 'Amsterdam': 'AMS', 'Bangalore': 'BLR', 'Lagos': 'LOS', 'Seoul': 'SEO',
96
- 'Copenhagen': 'CPH', 'Lisbon': 'LIS', 'Bogotá': 'BOG', 'Auckland': 'AKL'
97
- }
98
-
99
- BATCH_QUOTAS = {
100
- 'age_group': {
101
- 'children_only': 2,
102
- 'teens': 2,
103
- 'adults': 3,
104
- 'mixed_family': 2,
105
- 'mixed_adults': 1
106
- },
107
- 'difficulty': {'easy': 3, 'medium': 4, 'hard': 3},
108
- 'scoring_method': {'first_to_finish': 2, 'point_accumulation': 6, 'timed_bonus': 2},
109
- 'team_count_range': {'solo': 4, 'duo': 3, 'small_team': 2, 'large_team': 1},
110
- 'duration': {30: 1, 45: 2, 60: 4, 90: 2, 120: 1},
111
- 'cities': 'unique', # All 10 must be different
112
- 'regions': 3 # At least 3 geographic regions
113
- }
114
-
115
- # ============================================================================
116
- # VALIDATION FUNCTIONS
117
- # ============================================================================
118
-
119
- def contains_proper_noun(text: str) -> bool:
120
- """Check if text contains proper nouns (city names, landmarks, brands)."""
121
- if not isinstance(text, str):
122
- return False
123
- for pattern in PROPER_NOUN_PATTERNS:
124
- if re.search(pattern, text, re.IGNORECASE):
125
- return True
126
- return False
127
-
128
- def validate_proper_nouns(example: Dict) -> Tuple[bool, str]:
129
- """Validate that no proper nouns appear in task descriptions or hints."""
130
- for task in example.get('output', {}).get('tasks', []):
131
- desc = task.get('description', '')
132
- if contains_proper_noun(desc):
133
- return False, f"Task {task['task_id']}: proper noun in description"
134
-
135
- for hint_level in ['hint_1', 'hint_2', 'hint_3']:
136
- hint = task.get('hints', {}).get(hint_level, '')
137
- if contains_proper_noun(hint):
138
- return False, f"Task {task['task_id']}: proper noun in {hint_level}"
139
-
140
- return True, ""
141
-
142
- def validate_landscape_tags(example: Dict) -> Tuple[bool, str]:
143
- """Validate that all tasks use only tags present in input."""
144
- input_tags = set(example.get('input', {}).get('location', {}).get('landscape_tags', []))
145
-
146
- for task in example.get('output', {}).get('tasks', []):
147
- task_tags = set(task.get('landscape_tags_used', []))
148
- invalid_tags = task_tags - input_tags
149
- if invalid_tags:
150
- return False, f"Task {task['task_id']}: uses tags not in input: {invalid_tags}"
151
-
152
- return True, ""
153
-
154
- def validate_hint_progression(example: Dict) -> Tuple[bool, str]:
155
- """Validate hint progression (distinct, increasing specificity)."""
156
- for task in example.get('output', {}).get('tasks', []):
157
- hints = task.get('hints', {})
158
- h1 = hints.get('hint_1', '')
159
- h2 = hints.get('hint_2', '')
160
- h3 = hints.get('hint_3', '')
161
-
162
- # Distance check
163
- if levenshtein_distance(h1, h2) < 8:
164
- return False, f"Task {task['task_id']}: hint_1 and hint_2 too similar"
165
- if levenshtein_distance(h2, h3) < 8:
166
- return False, f"Task {task['task_id']}: hint_2 and hint_3 too similar"
167
-
168
- # Length progression (crude specificity proxy)
169
- if len(h2) <= len(h1):
170
- return False, f"Task {task['task_id']}: hint_2 not longer than hint_1"
171
- if len(h3) <= len(h2):
172
- return False, f"Task {task['task_id']}: hint_3 not longer than hint_2"
173
-
174
- return True, ""
175
-
176
- def validate_task_diversity(example: Dict) -> Tuple[bool, str]:
177
- """Validate that at least 3 different task types are present."""
178
- task_types = [t.get('task_type') for t in example.get('output', {}).get('tasks', [])]
179
- unique_types = len(set(task_types))
180
- if unique_types < 3:
181
- return False, f"Only {unique_types} unique task types; need >= 3"
182
- return True, ""
183
-
184
- def validate_time_realism(example: Dict) -> Tuple[bool, str]:
185
- """Validate that task time estimates are realistic."""
186
- for task in example.get('output', {}).get('tasks', []):
187
- task_type = task.get('task_type')
188
- est_time = task.get('estimated_time_minutes', 0)
189
-
190
- max_times = {
191
- 'find_and_photograph': 20,
192
- 'observe_and_answer': 15,
193
- 'logical_puzzle': 25,
194
- 'timed_challenge': 30,
195
- 'social_interaction': 10,
196
- 'collect_and_return': 20,
197
- 'reach_and_verify': 20
198
- }
199
-
200
- max_time = max_times.get(task_type, 30)
201
- if est_time > max_time:
202
- return False, f"Task {task['task_id']}: {task_type} estimated {est_time}min (max {max_time})"
203
-
204
- return True, ""
205
-
206
- def validate_total_time(example: Dict) -> Tuple[bool, str]:
207
- """Validate that total estimated time <= duration_minutes."""
208
- total_time = example.get('output', {}).get('estimated_total_time_minutes', 0)
209
- duration = example.get('input', {}).get('preferences', {}).get('duration_minutes', 0)
210
-
211
- if total_time > duration:
212
- return False, f"Total time {total_time}min > duration {duration}min"
213
-
214
- return True, ""
215
-
216
- def validate_arithmetic_checksums(example: Dict) -> Tuple[bool, str]:
217
- """Validate 9 arithmetic checksums from original prompt."""
218
- output = example.get('output', {})
219
- tasks = output.get('tasks', [])
220
-
221
- # Checksum 1: total_possible_points
222
- expected_total = sum(t.get('points', 0) for t in tasks)
223
- if output.get('bonus_task') and output['bonus_task'].get('points'):
224
- expected_total += output['bonus_task']['points']
225
- if output.get('total_possible_points') != expected_total:
226
- return False, f"Checksum 1: total_possible_points mismatch"
227
-
228
- # Checksum 2: max_deductible_points
229
- expected_max_ded = len(tasks) * 10
230
- if output.get('max_deductible_points') != expected_max_ded:
231
- return False, f"Checksum 2: max_deductible_points mismatch"
232
-
233
- # Checksum 3: minimum_possible_points
234
- expected_min = output.get('total_possible_points', 0) - output.get('max_deductible_points', 0)
235
- if output.get('minimum_possible_points') != expected_min:
236
- return False, f"Checksum 3: minimum_possible_points mismatch"
237
-
238
- # Checksum 4: estimated_total_time_minutes
239
- expected_time = sum(t.get('estimated_time_minutes', 0) for t in tasks)
240
- if output.get('estimated_total_time_minutes') != expected_time:
241
- return False, f"Checksum 4: estimated_total_time_minutes mismatch"
242
-
243
- # Checksum 5: scoring_method matches decision tree
244
- age_group = example['input']['players']['age_group']
245
- team_count = example['input']['players']['team_count']
246
- difficulty = example['input']['preferences']['difficulty']
247
- duration = example['input']['preferences']['duration_minutes']
248
-
249
- expected_method = select_scoring_method(age_group, team_count, difficulty, duration)
250
- # Soft check: warn but don't fail (model may prefer alternative)
251
-
252
- # Checksum 6: time_bonus only when scoring_method = timed_bonus
253
- has_time_bonus = output.get('scoring_summary', {}).get('time_bonus_per_minute_early') is not None
254
- is_timed_bonus = output.get('rules', {}).get('scoring_method') == 'timed_bonus'
255
- if has_time_bonus != is_timed_bonus:
256
- return False, f"Checksum 6: time_bonus inconsistent with scoring_method"
257
-
258
- # Checksum 7: team_aggregation_method only when team_count > 1
259
- has_agg = output.get('scoring_summary', {}).get('team_aggregation_method') is not None
260
- should_have_agg = team_count > 1
261
- if has_agg != should_have_agg:
262
- return False, f"Checksum 7: team_aggregation_method inconsistent with team_count"
263
-
264
- # Checksum 8: bonus_task only when eligible
265
- has_bonus = output.get('bonus_task') is not None and output['bonus_task'].get('description') is not None
266
- eligible = difficulty == 'hard' and len(tasks) >= 7
267
- if has_bonus != eligible:
268
- return False, f"Checksum 8: bonus_task eligibility mismatch"
269
-
270
- # Checksum 9: difficulty distribution matches game difficulty
271
- task_diffs = [t.get('difficulty_contribution') for t in tasks]
272
- easy_pct = (task_diffs.count('easy') / len(task_diffs) * 100) if task_diffs else 0
273
- medium_pct = (task_diffs.count('medium') / len(task_diffs) * 100) if task_diffs else 0
274
- hard_pct = (task_diffs.count('hard') / len(task_diffs) * 100) if task_diffs else 0
275
-
276
- if difficulty == 'easy':
277
- if easy_pct < 70 or hard_pct > 0:
278
- return False, f"Checksum 9a: easy game has {easy_pct}% easy, {hard_pct}% hard"
279
- elif difficulty == 'medium':
280
- if not (30 <= easy_pct <= 50) or hard_pct > 20:
281
- return False, f"Checksum 9b: medium game distribution off"
282
- elif difficulty == 'hard':
283
- if easy_pct > 20 or hard_pct < 40:
284
- return False, f"Checksum 9c: hard game has {easy_pct}% easy, {hard_pct}% hard"
285
-
286
- return True, ""
287
-
288
- def validate_climate_expression(example: Dict) -> Tuple[bool, str]:
289
- """Validate that climate zone is expressed in tasks or safety notes."""
290
- climate = example['input']['location']['climate_zone']
291
- output = example['output']
292
-
293
- # Check safety notes
294
- safety_notes = output.get('safety_constraints', {}).get('notes', '')
295
- if climate.lower() in safety_notes.lower():
296
- return True, ""
297
-
298
- # Check task descriptions for climate-related keywords
299
- task_descs = ' '.join([t['description'] for t in output.get('tasks', [])])
300
- climate_keywords = {
301
- 'tropical': ['shade', 'humid', 'heat', 'cool'],
302
- 'arid': ['shade', 'water', 'dehydration', 'sun'],
303
- 'mediterranean': ['terrace', 'outdoor', 'plaza'],
304
- 'temperate': [], # No special expression needed
305
- 'continental': ['cold', 'warm clothing'],
306
- 'polar': ['cold', 'warm', 'brief']
307
- }
308
-
309
- if climate in climate_keywords:
310
- keywords = climate_keywords[climate]
311
- if keywords and any(kw in task_descs.lower() for kw in keywords):
312
- return True, ""
313
-
314
- return False, f"Climate zone '{climate}' not expressed in output"
315
-
316
- def validate_batch_quotas(batch: List[Dict]) -> Tuple[bool, List[str]]:
317
- """Validate that a batch of 10 examples meets all quotas."""
318
- errors = []
319
-
320
- if len(batch) != 10:
321
- errors.append(f"Batch has {len(batch)} examples; need exactly 10")
322
- return False, errors
323
-
324
- # City diversity
325
- cities = [e['input']['location']['city'] for e in batch]
326
- if len(set(cities)) != 10:
327
- errors.append(f"Duplicate cities in batch: {Counter(cities)}")
328
-
329
- # Age group distribution
330
- age_groups = [e['input']['players']['age_group'] for e in batch]
331
- age_counts = Counter(age_groups)
332
- if age_counts != BATCH_QUOTAS['age_group']:
333
- errors.append(f"Age distribution {dict(age_counts)} != quota {BATCH_QUOTAS['age_group']}")
334
-
335
- # Difficulty distribution
336
- difficulties = [e['input']['preferences']['difficulty'] for e in batch]
337
- diff_counts = Counter(difficulties)
338
- if diff_counts != BATCH_QUOTAS['difficulty']:
339
- errors.append(f"Difficulty distribution {dict(diff_counts)} != quota")
340
-
341
- # Scoring method distribution
342
- methods = [e['output']['rules']['scoring_method'] for e in batch]
343
- method_counts = Counter(methods)
344
- if method_counts != BATCH_QUOTAS['scoring_method']:
345
- errors.append(f"Scoring method distribution {dict(method_counts)} != quota")
346
-
347
- # Duration distribution
348
- durations = [e['input']['preferences']['duration_minutes'] for e in batch]
349
- duration_counts = Counter(durations)
350
- if duration_counts != BATCH_QUOTAS['duration']:
351
- errors.append(f"Duration distribution {dict(duration_counts)} != quota")
352
-
353
- return len(errors) == 0, errors
354
-
355
- def validate_example(example: Dict, strict: bool = True) -> Tuple[bool, str]:
356
- """Run all validations on a single example."""
357
- validators = [
358
- validate_proper_nouns,
359
- validate_landscape_tags,
360
- validate_hint_progression,
361
- validate_task_diversity,
362
- validate_time_realism,
363
- validate_total_time,
364
- validate_arithmetic_checksums,
365
- validate_climate_expression,
366
- ]
367
-
368
- for validator in validators:
369
- is_valid, error_msg = validator(example)
370
- if not is_valid:
371
- return False, error_msg
372
-
373
- return True, ""
374
-
375
- # ============================================================================
376
- # HELPER FUNCTIONS
377
- # ============================================================================
378
-
379
- def select_scoring_method(age_group, team_count, difficulty, duration_minutes):
380
- """Deterministic scoring method selection (from audit fix #2)."""
381
- if age_group in ['children_only', 'mixed_family']:
382
- return 'point_accumulation'
383
- if team_count > 2:
384
- return 'point_accumulation'
385
- if team_count in [0, 1]:
386
- if difficulty == 'hard' and duration_minutes >= 60:
387
- return 'timed_bonus'
388
- elif difficulty == 'easy' and duration_minutes <= 45:
389
- return 'first_to_finish'
390
- else:
391
- return 'point_accumulation'
392
- if team_count == 2:
393
- if difficulty == 'hard' and duration_minutes >= 90:
394
- return 'timed_bonus'
395
- else:
396
- return 'point_accumulation'
397
- return 'point_accumulation'
398
-
399
- def mask_city_name(example: Dict, mask_probability: float = 0.8) -> Dict:
400
- """Mask city name during fine-tuning (from audit fix #9)."""
401
- if random.random() < mask_probability:
402
- example['input']['location']['city'] = '[CITY]'
403
- example['input']['location']['city_code'] = '[CODE]'
404
- return example
405
-
406
- def generate_batch_summary(batch: List[Dict]) -> Dict:
407
- """Generate metadata summary for a batch."""
408
- if not batch:
409
- return {}
410
-
411
- cities = [e['input']['location']['city'] for e in batch]
412
- age_groups = [e['input']['players']['age_group'] for e in batch]
413
- difficulties = [e['input']['preferences']['difficulty'] for e in batch]
414
- methods = [e['output']['rules']['scoring_method'] for e in batch]
415
- quality_scores = [e['output'].get('quality_score', 0) for e in batch]
416
-
417
- return {
418
- 'batch_size': len(batch),
419
- 'cities': cities,
420
- 'age_groups': Counter(age_groups),
421
- 'difficulties': Counter(difficulties),
422
- 'scoring_methods': Counter(methods),
423
- 'avg_quality_score': sum(quality_scores) / len(quality_scores) if quality_scores else 0,
424
- 'timestamp': datetime.now().isoformat()
425
- }
426
-
427
- # ============================================================================
428
- # GENERATION
429
- # ============================================================================
430
-
431
- def load_system_prompt(prompt_file: str = "./system_prompt.txt") -> str:
432
- """Load the system prompt from file."""
433
- if not os.path.exists(prompt_file):
434
- raise FileNotFoundError(f"System prompt file '{prompt_file}' not found. "
435
- f"Create it with the revised prompt from the audit.")
436
- with open(prompt_file, "r", encoding="utf-8") as f:
437
- return f.read()
438
-
439
- def generate_batch_gemini(system_prompt: str, batch_num: int, max_retries: int = 3) -> Optional[List[Dict]]:
440
- """Generate a batch using Gemini 2.5 Flash."""
441
- if not HAS_GEMINI:
442
- raise ImportError("google-genai not installed. Install with: pip install google-genai")
443
-
444
- client = genai.Client()
445
-
446
- user_prompt = f"""
447
- Generate batch #{batch_num}.
448
-
449
- {system_prompt}
450
-
451
- CONSTRAINTS FOR THIS BATCH:
452
- - Exactly 10 examples
453
- - All 10 cities must be different (from the city bank)
454
- - Age group distribution: children_only: 2, teens: 2, adults: 3, mixed_family: 2, mixed_adults: 1
455
- - Difficulty distribution: easy: 3, medium: 4, hard: 3
456
- - Scoring method distribution: first_to_finish: 2, point_accumulation: 6, timed_bonus: 2
457
- - All landscape_tags must be from the controlled vocabulary
458
- - NO proper nouns in any task description or hints
459
- - All hints must show clear progression
460
- - All checksums must pass
461
-
462
- Return ONLY valid JSON (no preamble, no markdown, no comments).
463
- """
464
-
465
- for attempt in range(max_retries):
466
- try:
467
- response = client.models.generate_content(
468
- model='gemini-2.5-flash',
469
- contents=user_prompt,
470
- config=types.GenerateContentConfig(
471
- response_mime_type="application/json",
472
- temperature=0.7,
473
- ),
474
- )
475
-
476
- batch_data = json.loads(response.text)
477
-
478
- # Validate batch structure
479
- if not isinstance(batch_data, list) or len(batch_data) != 10:
480
- logger.warning(f"Batch {batch_num}: Invalid structure on attempt {attempt+1}")
481
- continue
482
-
483
- return batch_data
484
-
485
- except json.JSONDecodeError as e:
486
- logger.warning(f"Batch {batch_num}: JSON decode error on attempt {attempt+1}: {e}")
487
- if attempt == max_retries - 1:
488
- raise
489
- except Exception as e:
490
- logger.error(f"Batch {batch_num}: Error on attempt {attempt+1}: {e}")
491
- if attempt == max_retries - 1:
492
- raise
493
-
494
- # Backoff before retry
495
- time.sleep(5 * (attempt + 1))
496
-
497
- return None
498
-
499
- def main():
500
- parser = argparse.ArgumentParser(description='Generate scavenger hunt training dataset')
501
- parser.add_argument('--batches', type=int, default=10, help='Number of batches to generate (10 examples each)')
502
- parser.add_argument('--output', default='scavenger_hunt_dataset.json', help='Output file path')
503
- parser.add_argument('--validate', choices=['strict', 'lenient', 'none'], default='strict',
504
- help='Validation level')
505
- parser.add_argument('--model', choices=['gemini', 'claude'], default='gemini', help='Which API to use')
506
- parser.add_argument('--mask-city', type=float, default=0.8, help='City masking probability during generation')
507
- args = parser.parse_args()
508
-
509
- # Load system prompt
510
- try:
511
- system_prompt = load_system_prompt()
512
- except FileNotFoundError as e:
513
- logger.error(str(e))
514
- return
515
-
516
- # Initialize storage
517
- all_examples = []
518
- validation_log = []
519
- batch_metadata = []
520
-
521
- # Load existing data if resuming
522
- if os.path.exists(args.output):
523
- try:
524
- with open(args.output, 'r') as f:
525
- all_examples = json.load(f)
526
- logger.info(f"Loaded {len(all_examples)} existing examples")
527
- except json.JSONDecodeError:
528
- logger.warning(f"Could not load {args.output}; starting fresh")
529
-
530
- # Generate batches
531
- for batch_num in range(1, args.batches + 1):
532
- logger.info(f"Generating batch {batch_num}/{args.batches}...")
533
-
534
- try:
535
- if args.model == 'gemini':
536
- batch_data = generate_batch_gemini(system_prompt, batch_num)
537
- else:
538
- logger.error("Claude model not yet implemented")
539
- continue
540
-
541
- if not batch_data:
542
- logger.warning(f"Batch {batch_num}: generation returned None")
543
- continue
544
-
545
- # Validate each example
546
- valid_examples = []
547
- for i, example in enumerate(batch_data):
548
- is_valid, error_msg = validate_example(example, strict=(args.validate == 'strict'))
549
-
550
- if is_valid:
551
- # Mask city name if requested
552
- if args.mask_city > 0:
553
- example = mask_city_name(example, args.mask_city)
554
- valid_examples.append(example)
555
- else:
556
- validation_log.append({
557
- 'batch': batch_num,
558
- 'example_index': i,
559
- 'error': error_msg,
560
- 'timestamp': datetime.now().isoformat()
561
- })
562
- logger.warning(f" Example {i}: {error_msg}")
563
-
564
- if not valid_examples:
565
- logger.error(f"Batch {batch_num}: All 10 examples failed validation. Skipping batch.")
566
- continue
567
-
568
- # Check batch quotas if we have enough examples
569
- if len(valid_examples) == 10:
570
- batch_valid, quota_errors = validate_batch_quotas(valid_examples)
571
- if not batch_valid:
572
- logger.warning(f"Batch {batch_num} failed quota checks:")
573
- for error in quota_errors:
574
- logger.warning(f" - {error}")
575
- # In strict mode, reject entire batch
576
- if args.validate == 'strict':
577
- continue
578
-
579
- # Add valid examples
580
- all_examples.extend(valid_examples)
581
-
582
- # Log metadata
583
- batch_meta = generate_batch_summary(valid_examples)
584
- batch_meta['batch_num'] = batch_num
585
- batch_meta['valid_count'] = len(valid_examples)
586
- batch_metadata.append(batch_meta)
587
-
588
- # Save incrementally
589
- with open(args.output, 'w') as f:
590
- json.dump(all_examples, f, indent=2)
591
-
592
- with open(args.output.replace('.json', '_metadata.json'), 'w') as f:
593
- json.dump(batch_metadata, f, indent=2)
594
-
595
- logger.info(f"Batch {batch_num}: {len(valid_examples)}/10 valid. "
596
- f"Total: {len(all_examples)} examples")
597
-
598
- # Rate limiting
599
- time.sleep(15)
600
-
601
- except Exception as e:
602
- logger.error(f"Batch {batch_num}: Unrecoverable error: {e}")
603
- continue
604
-
605
- # Final summary
606
- logger.info(f"\n{'='*60}")
607
- logger.info(f"GENERATION COMPLETE")
608
- logger.info(f"{'='*60}")
609
- logger.info(f"Total valid examples: {len(all_examples)}")
610
- logger.info(f"Total validation failures: {len(validation_log)}")
611
- logger.info(f"Output file: {args.output}")
612
- logger.info(f"Metadata file: {args.output.replace('.json', '_metadata.json')}")
613
-
614
- # Save validation log
615
- if validation_log:
616
- with open(args.output.replace('.json', '_errors.jsonl'), 'w') as f:
617
- for entry in validation_log:
618
- f.write(json.dumps(entry) + '\n')
619
- logger.info(f"Validation errors: {args.output.replace('.json', '_errors.jsonl')}")
620
-
621
- if __name__ == "__main__":
622
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/scavenger_hunt/generator.py ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ r"""
2
+ generator.py — scavenger_hunt dataset generator
3
+
4
+ One Gemini call per example. Failed examples retry up to 2x with the
5
+ validation errors fed back into the prompt. Arithmetic fields are
6
+ auto-fixed before validation so the model is never penalised for math.
7
+ Saves after every successful example (safe to Ctrl-C and resume).
8
+
9
+ Usage (PowerShell / Windows):
10
+ $env:GEMINI_API_KEY = "your-key"
11
+ python generator.py --n 10 --output ..\..\app\data\scavenger_hunt\dataset.json
12
+
13
+ Usage (bash / Mac / Linux):
14
+ export GEMINI_API_KEY="your-key"
15
+ python3 generator.py --n 10 --output ../../app/data/scavenger_hunt/dataset.json
16
+
17
+ Flags:
18
+ --n INT examples to generate this run (default 10)
19
+ --output PATH output JSON file (default app/data/scavenger_hunt/dataset.json)
20
+ --sleep FLOAT seconds between API calls (default 8)
21
+ --seed INT fix random seed for sampling (optional)
22
+ --mock skip Gemini, use built-in stub (pipeline smoke-test)
23
+ """
24
+ import os, re, json, time, random, logging, argparse
25
+ from sampler import sample_input, build_prompt
26
+ from validator import validate_example, auto_fix
27
+
28
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
29
+ log = logging.getLogger("cq")
30
+
31
+ DEFAULT_OUTPUT = os.path.normpath(os.path.join(
32
+ os.path.dirname(os.path.abspath(__file__)), "..", "..", "app", "data", "scavenger_hunt", "dataset.json"))
33
+
34
+
35
+ # ── JSON extraction ───────────────────────────────────────────────────────────
36
+ def extract_json(text: str) -> dict:
37
+ """Pull JSON object from model output, stripping markdown fences if present."""
38
+ text = re.sub(r"^```(?:json)?\s*", "", text.strip())
39
+ text = re.sub(r"\s*```$", "", text)
40
+ start, end = text.find("{"), text.rfind("}")
41
+ if start == -1 or end == -1:
42
+ raise ValueError("no JSON object found in model output")
43
+ return json.loads(text[start : end + 1])
44
+
45
+
46
+ # ── Gemini backend ────────────────────────────────────────────────────────────
47
+ def call_gemini(prompt: str) -> str:
48
+ from google import genai
49
+ from google.genai import types
50
+ client = genai.Client() # reads GEMINI_API_KEY
51
+ resp = client.models.generate_content(
52
+ model = "gemini-2.5-flash",
53
+ contents = prompt,
54
+ config = types.GenerateContentConfig(
55
+ response_mime_type = "application/json",
56
+ temperature = 0.8,
57
+ ),
58
+ )
59
+ return resp.text
60
+
61
+
62
+ # ── Mock backend (no API needed) ─────────────────────────────────────────────
63
+ def call_mock(prompt: str) -> str:
64
+ """Deterministic stub — builds a valid-ish output from info already in the prompt."""
65
+ import re as _re
66
+
67
+ def _find(pattern, default):
68
+ m = _re.search(pattern, prompt)
69
+ return m.group(1) if m else default
70
+
71
+ n_tasks = int(_find(r"exactly (\d+)", "4"))
72
+ method = _find(r'scoring_method\s*:\s*"(\w+)"', "point_accumulation")
73
+ dur = int(_find(r"time_limit_minutes\s*:\s*(\d+)", "60"))
74
+ tb_raw = _find(r"time_bonus_per_minute\s*:\s*(\d+|null)", "null")
75
+ tb = None if tb_raw == "null" else int(tb_raw)
76
+ agg_raw = _find(r'team_aggregation\s*:\s*(null|"[^"]+")', "null")
77
+ agg = None if agg_raw == "null" else agg_raw.strip('"')
78
+ bonus_ok = "bonus_task_eligible : true" in prompt
79
+
80
+ # tags from prompt
81
+ tm = _re.search(r'"landscape_tags":\s*\[([^\]]+)\]', prompt)
82
+ tags = [t.strip().strip('"') for t in tm.group(1).split(",")] if tm else ["plaza_or_square"]
83
+
84
+ # difficulty mix
85
+ if "ZERO hard tasks" in prompt:
86
+ diffs = ["easy"] * n_tasks
87
+ elif "≥40 % hard" in prompt or ">=40% hard" in prompt:
88
+ n_hard = max(2, round(n_tasks * 0.45))
89
+ n_med = max(1, round(n_tasks * 0.35))
90
+ n_easy = max(0, n_tasks - n_hard - n_med)
91
+ diffs = (["hard"] * n_hard + ["medium"] * n_med + ["easy"] * n_easy)[:n_tasks]
92
+ else:
93
+ diffs = (["medium", "easy"] * n_tasks)[:n_tasks]
94
+
95
+ pts_map = {"easy": 10, "medium": 20, "hard": 30}
96
+ types_cycle = ["observe_and_answer","find_and_photograph","reach_and_verify",
97
+ "social_interaction","timed_challenge","collect_and_return"]
98
+ per_task = max(3, int(dur * 0.78) // n_tasks)
99
+
100
+ tasks = []
101
+ for i in range(n_tasks):
102
+ tag = tags[i % len(tags)]
103
+ tasks.append({
104
+ "task_id": f"T{i+1:02d}",
105
+ "title": f"Zone Study {i+1}",
106
+ "description": f"Within the zone characterised by {tag.replace('_',' ')}, locate and document the most structurally distinct feature visible from a public pedestrian path.",
107
+ "landscape_tags_used": [tag],
108
+ "task_type": types_cycle[i % len(types_cycle)],
109
+ "difficulty_contribution": diffs[i],
110
+ "points": pts_map[diffs[i]],
111
+ "completion_proof": "photograph submitted via app",
112
+ "estimated_time_minutes": per_task,
113
+ "hints": {
114
+ "hint_1": "Scan the area from the nearest open space.",
115
+ "hint_2": f"Move toward where {tag.replace('_',' ')} activity is densest and compare individual instances.",
116
+ "hint_3": f"Walk the main pedestrian route through the {tag.replace('_',' ')} zone; at the midpoint, turn to face the most varied cluster and document the single feature that differs most from its neighbours in scale or colour.",
117
+ },
118
+ "safety_flags": [],
119
+ })
120
+
121
+ bonus = (
122
+ {"description": "Reach the highest publicly accessible viewpoint in the area and return before the window closes.",
123
+ "points": 50, "risk": "−20 points if not completed within 15 minutes",
124
+ "completion_proof": "geotagged photo from viewpoint"}
125
+ if bonus_ok else
126
+ {"description": None, "points": None, "risk": None, "completion_proof": None}
127
+ )
128
+
129
+ out = {
130
+ "game_title": "The Urban Field Survey",
131
+ "rules": {
132
+ "objective": "Score the most points before the time limit expires.",
133
+ "scoring_method": method,
134
+ "task_reveal_mode": "sequential",
135
+ "team_rules": "Teams move together and submit jointly." if agg else None,
136
+ "time_limit_minutes": dur,
137
+ "disqualification_conditions": ["entering an exclusion zone","using prohibited transport"],
138
+ },
139
+ "safety_constraints": {
140
+ "exclusion_zones": ["private_property","active_roadway","construction_sites","restricted_government_buildings"],
141
+ "physical_limits": ["no climbing","no jumping","no water entry","no entering buildings"],
142
+ "adult_supervision_required": False,
143
+ "notes": "Standard urban precautions apply.",
144
+ },
145
+ "tasks": tasks,
146
+ "task_count": n_tasks,
147
+ "total_possible_points": 0, # auto_fix will correct
148
+ "max_deductible_points": 0,
149
+ "minimum_possible_points": 0,
150
+ "bonus_task_eligible": bonus_ok,
151
+ "bonus_task": bonus,
152
+ "scoring_summary": {
153
+ "base_points_available": 0,
154
+ "time_bonus_per_minute_early": tb,
155
+ "hint_cost_tier_1": 5,
156
+ "hint_cost_tier_2": 10,
157
+ "team_aggregation_method": agg,
158
+ "winning_condition_detail": "The competitor with the highest total points when the time limit expires wins; ties broken by earliest final task submission.",
159
+ },
160
+ "estimated_total_time_minutes": 0,
161
+ "quality_score": 4.0,
162
+ }
163
+ return json.dumps(out)
164
+
165
+
166
+ # ── single example loop ───────────────────────────────────────────────────────
167
+ def generate_one(record: dict, backend, max_retries: int = 2):
168
+ """
169
+ Try up to (max_retries+1) times.
170
+ On retry the validation errors are appended to the prompt so the model
171
+ knows exactly what to fix.
172
+ Returns (example_dict, None) on success, (None, [errors]) on failure.
173
+ """
174
+ prompt = build_prompt(record)
175
+ last_errors = []
176
+
177
+ for attempt in range(max_retries + 1):
178
+ full_prompt = prompt
179
+ if attempt > 0 and last_errors:
180
+ full_prompt += (
181
+ "\n\nYOUR PREVIOUS ATTEMPT FAILED THESE VALIDATION CHECKS:\n"
182
+ + "\n".join(f" • {e}" for e in last_errors[:8])
183
+ + "\nPlease fix every item listed above. Return ONLY the corrected JSON."
184
+ )
185
+
186
+ try:
187
+ raw = backend(full_prompt)
188
+ output_json = extract_json(raw)
189
+
190
+ # the model sometimes wraps the output inside {"output": {...}}
191
+ if "output" in output_json and isinstance(output_json["output"], dict):
192
+ output_json = output_json["output"]
193
+
194
+ ex = {**record, "output": output_json}
195
+ ex = auto_fix(ex) # repair arithmetic first
196
+ ok, errors = validate_example(ex)
197
+
198
+ if ok:
199
+ return ex, None
200
+ last_errors = errors
201
+ log.warning(" attempt %d/%d — %d error(s): %s",
202
+ attempt + 1, max_retries + 1,
203
+ len(errors), errors[0])
204
+
205
+ except Exception as exc:
206
+ last_errors = [f"PARSE/API error: {exc}"]
207
+ log.warning(" attempt %d/%d — %s", attempt + 1, max_retries + 1, exc)
208
+
209
+ return None, last_errors
210
+
211
+
212
+ # ── main ──────��───────────────────────────────────────────────────────────────
213
+ def main():
214
+ ap = argparse.ArgumentParser(description="CityQuest-AI dataset generator")
215
+ ap.add_argument("--n", type=int, default=10, help="examples to generate this run")
216
+ ap.add_argument("--output", default=DEFAULT_OUTPUT, help="output JSON file (resumes if exists)")
217
+ ap.add_argument("--sleep", type=float, default=8.0, help="seconds between Gemini calls")
218
+ ap.add_argument("--seed", type=int, default=None, help="random seed for reproducibility")
219
+ ap.add_argument("--mock", action="store_true", help="use stub backend (no API key needed)")
220
+ args = ap.parse_args()
221
+
222
+ if args.seed is not None:
223
+ random.seed(args.seed)
224
+
225
+ if not args.mock and not os.environ.get("GEMINI_API_KEY"):
226
+ log.error("GEMINI_API_KEY is not set.")
227
+ log.error(" Windows (PowerShell): $env:GEMINI_API_KEY = 'your-key'")
228
+ log.error(" Mac/Linux: export GEMINI_API_KEY='your-key'")
229
+ return
230
+
231
+ backend = call_mock if args.mock else call_gemini
232
+
233
+ out_dir = os.path.dirname(args.output)
234
+ if out_dir:
235
+ os.makedirs(out_dir, exist_ok=True)
236
+
237
+ # ── resume ────────────────────────────────────────────────────────────────
238
+ dataset = []
239
+ if os.path.exists(args.output):
240
+ try:
241
+ dataset = json.load(open(args.output, encoding="utf-8"))
242
+ log.info("Resuming — loaded %d existing examples from %s", len(dataset), args.output)
243
+ except json.JSONDecodeError:
244
+ log.warning("Existing output file unreadable; starting fresh.")
245
+
246
+ errors_path = args.output.replace(".json", "_errors.jsonl")
247
+ start_idx = len(dataset) + 1
248
+ kept = skipped = 0
249
+
250
+ # ── generation loop ───────────────────────────────────────────────────────
251
+ for i in range(start_idx, start_idx + args.n):
252
+ record = sample_input(i)
253
+ city = record["input"]["location"]["city"]
254
+ diff = record["input"]["preferences"]["difficulty"]
255
+ dur = record["input"]["preferences"]["duration_minutes"]
256
+ age = record["input"]["players"]["age_group"]
257
+
258
+ log.info("[%d/%d] %s | %-12s | %-6s | %3d min | %s",
259
+ i - start_idx + 1, args.n,
260
+ record["id"], city, diff, dur, age)
261
+
262
+ ex, errors = generate_one(record, backend)
263
+
264
+ if ex:
265
+ dataset.append(ex)
266
+ kept += 1
267
+ # incremental save — safe to interrupt at any point
268
+ with open(args.output, "w", encoding="utf-8") as f:
269
+ json.dump(dataset, f, indent=1, ensure_ascii=False)
270
+ log.info(" ✓ saved (dataset total: %d)", len(dataset))
271
+ else:
272
+ skipped += 1
273
+ with open(errors_path, "a", encoding="utf-8") as f:
274
+ f.write(json.dumps({"id": record["id"], "errors": errors}) + "\n")
275
+ log.info(" ✗ skipped after retries — logged to %s", errors_path)
276
+
277
+ if not args.mock:
278
+ time.sleep(args.sleep)
279
+
280
+ # ── summary ───────────────────────────────────────────────────────────────
281
+ log.info("")
282
+ log.info("══ DONE ══════════════════════════════════")
283
+ log.info(" Kept : %d", kept)
284
+ log.info(" Skipped : %d", skipped)
285
+ log.info(" Total : %d → %s", len(dataset), args.output)
286
+ if skipped:
287
+ log.info(" Failures: %s", errors_path)
288
+
289
+
290
+ if __name__ == "__main__":
291
+ main()
scripts/scavenger_hunt/run_generation.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ run_generation.py — scavenger_hunt
3
+ Controllable batch runner around generator.py.
4
+
5
+ Calls generator.py in batches until the dataset reaches the target size,
6
+ printing a Counter-based stats summary after each batch. generator.py saves
7
+ incrementally and resumes from the existing output file, so Ctrl-C never
8
+ loses data — just re-run the same command to continue.
9
+
10
+ Usage:
11
+ python run_generation.py 100
12
+ python run_generation.py 100 --batch 20
13
+ python run_generation.py 100 --batch 10 --sleep 5
14
+ """
15
+ import argparse
16
+ import json
17
+ import os
18
+ import subprocess
19
+ import sys
20
+ from collections import Counter
21
+
22
+ HERE = os.path.dirname(os.path.abspath(__file__))
23
+ GENERATOR = os.path.join(HERE, "generator.py")
24
+ DEFAULT_OUTPUT = os.path.normpath(os.path.join(
25
+ HERE, "..", "..", "app", "data", "scavenger_hunt", "dataset.json"))
26
+
27
+
28
+ def load_dataset(path):
29
+ if not os.path.exists(path):
30
+ return []
31
+ try:
32
+ return json.load(open(path, encoding="utf-8"))
33
+ except json.JSONDecodeError:
34
+ return []
35
+
36
+
37
+ def print_stats(data):
38
+ cities = Counter(e["input"]["location"]["city"] for e in data)
39
+ diffs = Counter(e["input"]["preferences"]["difficulty"] for e in data)
40
+ methods = Counter(e["output"]["rules"]["scoring_method"] for e in data)
41
+ ages = Counter(e["input"]["players"]["age_group"] for e in data)
42
+
43
+ print(f"\n── Stats (total: {len(data)}) ──────────────────────────")
44
+ print(" Cities :", dict(cities))
45
+ print(" Difficulty :", dict(diffs))
46
+ print(" Scoring method :", dict(methods))
47
+ print(" Age group :", dict(ages))
48
+
49
+
50
+ def main():
51
+ ap = argparse.ArgumentParser(description="Run generator.py in batches until the dataset reaches a target size")
52
+ ap.add_argument("target", type=int, help="target total number of examples in the dataset")
53
+ ap.add_argument("--batch", type=int, default=20, help="examples per batch (default 20)")
54
+ ap.add_argument("--output", default=DEFAULT_OUTPUT, help="dataset JSON file (default app/data/scavenger_hunt/dataset.json)")
55
+ ap.add_argument("--sleep", type=float, default=8.0, help="seconds between Gemini calls (passed to generator.py)")
56
+ args = ap.parse_args()
57
+
58
+ if not os.environ.get("GEMINI_API_KEY"):
59
+ print("GEMINI_API_KEY is not set in this shell. Set it before running.")
60
+ sys.exit(1)
61
+
62
+ try:
63
+ while True:
64
+ data = load_dataset(args.output)
65
+ current = len(data)
66
+ if current >= args.target:
67
+ print(f"\nTarget reached: {current}/{args.target} examples in {args.output}")
68
+ break
69
+
70
+ n = min(args.batch, args.target - current)
71
+ print(f"\n=== Generating {n} example(s) ({current} -> {current + n} of {args.target}) ===")
72
+ result = subprocess.run(
73
+ [sys.executable, GENERATOR, "--n", str(n), "--output", args.output, "--sleep", str(args.sleep)]
74
+ )
75
+ if result.returncode != 0:
76
+ print("generator.py exited with an error — stopping.")
77
+ break
78
+
79
+ print_stats(load_dataset(args.output))
80
+ except KeyboardInterrupt:
81
+ print("\nInterrupted — dataset was saved incrementally. Re-run the same command to resume.")
82
+
83
+
84
+ if __name__ == "__main__":
85
+ main()
scripts/scavenger_hunt/sampler.py ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ sampler.py — scavenger_hunt
3
+ Samples ONE input config in Python (distributions guaranteed here, not by the LLM)
4
+ and builds a prompt asking Gemini to write only the output for that input.
5
+ """
6
+ import json
7
+ import random
8
+
9
+ # ── city bank ─────────────────────────────────────────────────────────────────
10
+ # (city_name) -> (country, code, available_tags, climate_zone)
11
+ CITY_BANK = {
12
+ "Paris": ("France", "PAR", ["iconic_landmark","wide_boulevard","river_waterfront","garden_formal","historic_district","cafe_dense","museum_cluster"], "temperate"),
13
+ "Tokyo": ("Japan", "TYO", ["dense_urban_grid","narrow_alley_network","religious_site_accessible","market_covered","iconic_landmark","shopping_street"], "temperate"),
14
+ "New York City": ("USA", "NYC", ["dense_urban_grid","park_large","iconic_landmark","coastal_waterfront","museum_cluster","wide_boulevard"], "continental"),
15
+ "Cape Town": ("South Africa", "CPT", ["coastal_waterfront","hill_or_elevation","market_outdoor","historic_district","garden_formal"], "mediterranean"),
16
+ "Marrakech": ("Morocco", "MRK", ["narrow_alley_network","market_outdoor","religious_site_accessible","plaza_or_square","food_market"], "arid"),
17
+ "Buenos Aires": ("Argentina", "BUE", ["wide_boulevard","plaza_or_square","cafe_dense","street_art_district","historic_district"], "temperate"),
18
+ "Mumbai": ("India", "BOM", ["coastal_waterfront","market_outdoor","dense_urban_grid","religious_site_accessible","food_market"], "tropical"),
19
+ "Berlin": ("Germany", "BER", ["park_large","street_art_district","museum_cluster","historic_district","wide_boulevard","cafe_dense"], "continental"),
20
+ "Sydney": ("Australia", "SYD", ["coastal_waterfront","park_large","iconic_landmark","market_covered","bridge_pedestrian"], "temperate"),
21
+ "Nairobi": ("Kenya", "NBO", ["park_large","market_outdoor","residential_neighbourhood","dense_urban_grid"], "tropical"),
22
+ "Istanbul": ("Turkey", "IST", ["historic_district","religious_site_accessible","market_covered","coastal_waterfront","hill_or_elevation","plaza_or_square"], "mediterranean"),
23
+ "Mexico City": ("Mexico", "MEX", ["plaza_or_square","market_outdoor","museum_cluster","street_art_district","dense_urban_grid"], "temperate"),
24
+ "Amsterdam": ("Netherlands", "AMS", ["river_waterfront","bridge_pedestrian","narrow_alley_network","cafe_dense","museum_cluster","park_small"], "temperate"),
25
+ "Bangalore": ("India", "BLR", ["park_large","market_outdoor","cafe_dense","residential_neighbourhood","shopping_street"], "tropical"),
26
+ "Lagos": ("Nigeria", "LOS", ["coastal_waterfront","market_outdoor","dense_urban_grid","food_market"], "tropical"),
27
+ "Seoul": ("South Korea", "SEO", ["dense_urban_grid","shopping_street","park_large","historic_district","narrow_alley_network"], "continental"),
28
+ "Copenhagen": ("Denmark", "CPH", ["coastal_waterfront","cafe_dense","bridge_pedestrian","market_covered","park_small"], "continental"),
29
+ "Lisbon": ("Portugal", "LIS", ["hill_or_elevation","narrow_alley_network","historic_district","coastal_waterfront","plaza_or_square"], "mediterranean"),
30
+ "Bogota": ("Colombia", "BOG", ["street_art_district","market_outdoor","park_large","historic_district","plaza_or_square"], "temperate"),
31
+ "Auckland": ("New Zealand", "AKL", ["coastal_waterfront","hill_or_elevation","park_large","market_outdoor","bridge_pedestrian"], "temperate"),
32
+ }
33
+
34
+ DENSITY = {
35
+ "Paris":"dense","Tokyo":"hyper_dense","New York City":"hyper_dense",
36
+ "Cape Town":"mixed","Marrakech":"dense","Buenos Aires":"dense",
37
+ "Mumbai":"hyper_dense","Berlin":"dense","Sydney":"mixed",
38
+ "Nairobi":"mixed","Istanbul":"dense","Mexico City":"hyper_dense",
39
+ "Amsterdam":"dense","Bangalore":"dense","Lagos":"hyper_dense",
40
+ "Seoul":"hyper_dense","Copenhagen":"mixed","Lisbon":"dense",
41
+ "Bogota":"dense","Auckland":"mixed",
42
+ }
43
+
44
+ # (duration_minutes, difficulty) → task count
45
+ TASK_COUNT = {
46
+ (30,"easy"):3, (30,"medium"):4, (30,"hard"):4,
47
+ (45,"easy"):4, (45,"medium"):5, (45,"hard"):5,
48
+ (60,"easy"):5, (60,"medium"):6, (60,"hard"):7,
49
+ (90,"easy"):6, (90,"medium"):8, (90,"hard"):9,
50
+ (120,"easy"):8, (120,"medium"):10,(120,"hard"):12,
51
+ }
52
+
53
+ AREA_TYPES = ["city_center","historic_district","waterfront","park_district",
54
+ "mixed_residential","market_district","university_campus"]
55
+ AGE_GROUPS = ["children_only","teens","adults","mixed_family","mixed_adults"]
56
+ THEMES = ["observation","history","social","nature","urban_exploration","photography","logic"]
57
+ TASK_TYPES = ["find_and_photograph","observe_and_answer","collect_and_return",
58
+ "reach_and_verify","social_interaction","timed_challenge"]
59
+
60
+
61
+ def _scoring_method(age_group, team_count, difficulty, duration):
62
+ """Deterministic decision tree — pre-computed in Python, given to the LLM as fact."""
63
+ if age_group in ("children_only", "mixed_family"):
64
+ return "point_accumulation"
65
+ if team_count > 2:
66
+ return "point_accumulation"
67
+ if team_count <= 1:
68
+ if difficulty == "hard" and duration >= 60:
69
+ return "timed_bonus"
70
+ if difficulty == "easy" and duration <= 45:
71
+ return "first_to_finish"
72
+ return "point_accumulation"
73
+ # team_count == 2
74
+ if difficulty == "hard" and duration >= 90:
75
+ return "timed_bonus"
76
+ return "point_accumulation"
77
+
78
+
79
+ def sample_input(index: int, rng=None) -> dict:
80
+ """Return one fully-sampled input record."""
81
+ rng = rng or random
82
+ city = rng.choice(list(CITY_BANK.keys()))
83
+ country, code, tags, climate = CITY_BANK[city]
84
+
85
+ n_tags = rng.randint(3, min(6, len(tags)))
86
+ chosen_tags = rng.sample(tags, n_tags)
87
+
88
+ difficulty = rng.choices(["easy","medium","hard"], weights=[30,40,30])[0]
89
+ duration = rng.choices([30,45,60,90,120], weights=[15,20,35,20,10])[0]
90
+ age_group = rng.choice(AGE_GROUPS)
91
+ team_count = rng.choices([1,2,3,4,5], weights=[40,25,15,12,8])[0]
92
+ count = rng.randint(max(2, team_count), 20)
93
+
94
+ return {
95
+ "id": f"SH-{code}-{index:04d}",
96
+ "input": {
97
+ "game_type": "scavenger_hunt",
98
+ "location": {
99
+ "city": city,
100
+ "country": country,
101
+ "city_code": code,
102
+ "landscape_tags": chosen_tags,
103
+ "urban_density": DENSITY[city],
104
+ "climate_zone": climate,
105
+ "area_type": rng.choice(AREA_TYPES),
106
+ },
107
+ "players": {
108
+ "count": count,
109
+ "team_count": team_count,
110
+ "age_group": age_group,
111
+ "mobility": rng.choices(["standard","limited"], weights=[85,15])[0],
112
+ },
113
+ "preferences": {
114
+ "duration_minutes": duration,
115
+ "difficulty": difficulty,
116
+ "theme": rng.choice(THEMES),
117
+ "allow_transport": rng.random() < 0.3,
118
+ },
119
+ },
120
+ }
121
+
122
+
123
+ def build_prompt(record: dict) -> str:
124
+ """Compact, precise prompt. All decisions are pre-computed and stated as facts."""
125
+ inp = record["input"]
126
+ loc, players, prefs = inp["location"], inp["players"], inp["preferences"]
127
+ diff = prefs["difficulty"]
128
+ dur = prefs["duration_minutes"]
129
+ age = players["age_group"]
130
+ tc = players["team_count"]
131
+
132
+ n_tasks = TASK_COUNT[(dur, diff)]
133
+ method = _scoring_method(age, tc, diff, dur)
134
+ time_bonus = {"easy":2,"medium":3,"hard":5}[diff] if method == "timed_bonus" else None
135
+ agg = "sum_all_members" if tc > 1 else None
136
+ bonus_ok = diff == "hard" and n_tasks >= 7 and age != "children_only"
137
+ supervise = age in ("children_only","mixed_family")
138
+ max_task_time = int(dur * 0.80 // n_tasks) # generous per-task ceiling
139
+
140
+ water_tags = {"river_waterfront","lake_or_pond","coastal_waterfront"}
141
+ has_water = bool(water_tags & set(loc["landscape_tags"]))
142
+ has_religious = "religious_site_accessible" in loc["landscape_tags"]
143
+
144
+ exclusions = ["private_property","active_roadway","construction_sites",
145
+ "restricted_government_buildings"]
146
+ if has_water: exclusions.append("water_edge")
147
+ if has_religious: exclusions.append("religious_interiors")
148
+
149
+ climate_note = {
150
+ "tropical": "Tropical climate — note heat/humidity; require shade access within 10 min of each task.",
151
+ "arid": "Arid climate — mandatory water advisory; no sustained outdoor walk > 15 min per task.",
152
+ "mediterranean": "Mediterranean climate — no special constraint.",
153
+ "temperate": "Temperate climate — no special constraint.",
154
+ "continental": f"Continental climate — {'add cold-weather advisory (duration ' + str(dur) + ' min).' if dur >= 90 else 'no special constraint.'}",
155
+ "polar": "Polar climate — all outdoor tasks ≤ 15 min each; mandatory warm-clothing note.",
156
+ }.get(loc["climate_zone"], "No special constraint.")
157
+
158
+ diff_rule = {
159
+ "easy": f"ALL {n_tasks} tasks easy (10 pts each). ZERO hard tasks.",
160
+ "medium": f"Mix across {n_tasks} tasks: roughly 40 % easy, 45 % medium, ≤1 hard.",
161
+ "hard": f"Mix across {n_tasks} tasks: ≤1 easy, ~35 % medium, ≥40 % hard.",
162
+ }[diff]
163
+
164
+ bonus_instruction = (
165
+ "bonus_task: one OPTIONAL timed_challenge worth exactly 50 pts. "
166
+ "risk = '−20 points if not completed within its time window'. "
167
+ "NO logic puzzles. description must obey the no-proper-noun rule."
168
+ if bonus_ok else
169
+ "bonus_task: set ALL four fields (description, points, risk, completion_proof) to null."
170
+ )
171
+
172
+ return f"""You are a JSON dataset generator. Return ONLY a valid JSON object — no markdown, no commentary, no extra text.
173
+
174
+ Generate the OUTPUT section of one scavenger-hunt training example.
175
+
176
+ ═══ FIXED INPUT (do not alter) ═══
177
+ {json.dumps(inp, indent=1)}
178
+
179
+ ═══ PRE-COMPUTED DECISIONS (use these exact values) ═══
180
+ task_count : exactly {n_tasks} (task_ids T01 … T{n_tasks:02d})
181
+ scoring_method : "{method}"
182
+ time_bonus_per_minute : {json.dumps(time_bonus)}
183
+ team_aggregation : {json.dumps(agg)}
184
+ bonus_task_eligible : {json.dumps(bonus_ok)}
185
+ time_limit_minutes : {dur}
186
+ difficulty mix : {diff_rule}
187
+ points scale : easy=10, medium=20, hard=30
188
+ max per-task time : {max_task_time} min (total task time MUST be ≤ {int(dur*0.85)} min)
189
+ adult_supervision : {json.dumps(supervise)}
190
+ exclusion_zones : {json.dumps(exclusions)}
191
+ climate note : {climate_note}
192
+
193
+ ═══ ABSOLUTE RULES ═══
194
+ 1. NO proper nouns anywhere — no city names, landmark names, street names, brand names.
195
+ Tasks must work purely from the landscape_tags.
196
+ ✓ GOOD: "Find the tallest structure visible from the main open square and photograph its base."
197
+ ✗ BAD: "Find the Eiffel Tower."
198
+
199
+ 2. Each task's landscape_tags_used must be a strict subset of: {json.dumps(loc["landscape_tags"])}
200
+
201
+ 3. Use at least 3 different task_type values from:
202
+ {json.dumps(TASK_TYPES)}
203
+
204
+ 4. Hints per task:
205
+ hint_1 = 5–15 words, directional nudge only
206
+ hint_2 = 15–30 words, describes the feature to look for (longer than hint_1)
207
+ hint_3 = 30–50 words, near-explicit walkthrough (longer than hint_2)
208
+ All three hints must be meaningfully distinct from each other.
209
+
210
+ 5. No task may require: climbing, jumping, entering water, entering any building
211
+ (exception: social_interaction tasks may enter public-facing shops).
212
+
213
+ 6. winning_condition_detail: one precise, unambiguous sentence.
214
+
215
+ 7. {bonus_instruction}
216
+
217
+ ═══ OUTPUT SHAPE ═══
218
+ {{
219
+ "game_title": "string",
220
+ "rules": {{
221
+ "objective": "string",
222
+ "scoring_method": "{method}",
223
+ "task_reveal_mode": "sequential" | "all_at_once" | "gated_by_points",
224
+ "team_rules": "string or null",
225
+ "time_limit_minutes": {dur},
226
+ "disqualification_conditions": ["string"]
227
+ }},
228
+ "safety_constraints": {{
229
+ "exclusion_zones": {json.dumps(exclusions)},
230
+ "physical_limits": ["no climbing","no jumping","no water entry","no entering buildings"],
231
+ "adult_supervision_required": {json.dumps(supervise)},
232
+ "notes": "string — include climate advisory here"
233
+ }},
234
+ "tasks": [
235
+ {{
236
+ "task_id": "T01",
237
+ "title": "string",
238
+ "description": "string — NO proper nouns",
239
+ "landscape_tags_used": ["subset of input tags"],
240
+ "task_type": "string",
241
+ "difficulty_contribution": "easy|medium|hard",
242
+ "points": 10 | 20 | 30,
243
+ "completion_proof": "string",
244
+ "estimated_time_minutes": "integer ≤ {max_task_time}",
245
+ "hints": {{
246
+ "hint_1": "5–15 words",
247
+ "hint_2": "15–30 words",
248
+ "hint_3": "30–50 words"
249
+ }},
250
+ "safety_flags": ["string"]
251
+ }}
252
+ ],
253
+ "task_count": {n_tasks},
254
+ "total_possible_points": "integer",
255
+ "max_deductible_points": "integer",
256
+ "minimum_possible_points": "integer",
257
+ "bonus_task_eligible": {json.dumps(bonus_ok)},
258
+ "bonus_task": {{
259
+ "description": "string or null",
260
+ "points": 50 | null,
261
+ "risk": "string or null",
262
+ "completion_proof": "string or null"
263
+ }},
264
+ "scoring_summary": {{
265
+ "base_points_available": "integer",
266
+ "time_bonus_per_minute_early": {json.dumps(time_bonus)},
267
+ "hint_cost_tier_1": 5,
268
+ "hint_cost_tier_2": 10,
269
+ "team_aggregation_method": {json.dumps(agg)},
270
+ "winning_condition_detail": "string"
271
+ }},
272
+ "estimated_total_time_minutes": "integer",
273
+ "quality_score": "float 1.0–5.0"
274
+ }}"""
275
+
276
+
277
+ if __name__ == "__main__":
278
+ rec = sample_input(1, rng=random.Random(42))
279
+ print(json.dumps(rec, indent=2))
280
+ print("\n--- PROMPT PREVIEW (first 1500 chars) ---\n")
281
+ print(build_prompt(rec)[:1500])
scripts/scavenger_hunt/test_pipeline.py ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ test_pipeline.py — scavenger_hunt — run before touching the API
3
+
4
+ Tests:
5
+ 1-6 validator unit tests (good example, proper noun, bad tag, children+timed, auto_fix, time overrun)
6
+ 7 sampler produces varied inputs
7
+ 8 full pipeline smoke-test via mock backend (10 examples, 0 API calls)
8
+ 9 resume-from-disk check
9
+ 10 stats check on generated file
10
+
11
+ Run: python test_pipeline.py (or: python3 test_pipeline.py)
12
+ """
13
+ import copy, json, os, random, subprocess, sys, tempfile
14
+
15
+ # ── 1-6: validator unit tests ─────────────────────────────────────────────────
16
+ from validator import validate_example, auto_fix
17
+ from sampler import sample_input, build_prompt
18
+
19
+ GENERATOR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "generator.py")
20
+
21
+ GOOD = {
22
+ "id": "SH-TST-0001",
23
+ "input": {
24
+ "game_type": "scavenger_hunt",
25
+ "location": {
26
+ "city": "Testville", "country": "Testland", "city_code": "TST",
27
+ "landscape_tags": ["plaza_or_square","market_outdoor","iconic_landmark"],
28
+ "urban_density": "dense", "climate_zone": "temperate", "area_type": "city_center",
29
+ },
30
+ "players": {"count":4,"team_count":1,"age_group":"adults","mobility":"standard"},
31
+ "preferences": {"duration_minutes":60,"difficulty":"easy","theme":"observation","allow_transport":False},
32
+ },
33
+ "output": {
34
+ "game_title": "The Square Pursuit",
35
+ "rules": {
36
+ "objective": "Score the most points.",
37
+ "scoring_method": "point_accumulation",
38
+ "task_reveal_mode": "sequential",
39
+ "team_rules": None,
40
+ "time_limit_minutes": 60,
41
+ "disqualification_conditions": ["entering excluded zones"],
42
+ },
43
+ "safety_constraints": {
44
+ "exclusion_zones": ["private_property","active_roadway","construction_sites","restricted_government_buildings"],
45
+ "physical_limits": ["no climbing"],
46
+ "adult_supervision_required": False,
47
+ "notes": "Temperate climate — no special constraint.",
48
+ },
49
+ "tasks": [
50
+ {
51
+ "task_id": "T01", "title": "Square Census",
52
+ "description": "Stand at the center of the largest open civic square and count the benches visible without moving.",
53
+ "landscape_tags_used": ["plaza_or_square"],
54
+ "task_type": "observe_and_answer",
55
+ "difficulty_contribution": "easy", "points": 10,
56
+ "completion_proof": "verbal answer", "estimated_time_minutes": 10,
57
+ "hints": {
58
+ "hint_1": "Find the biggest open space nearby.",
59
+ "hint_2": "Open civic squares have no buildings inside and multiple pedestrian entry points leading to a central area.",
60
+ "hint_3": "Walk to where the most pedestrian paths converge, stand at the geometric centre, rotate slowly, and count every fixed seating unit you can see without moving your feet.",
61
+ },
62
+ "safety_flags": [],
63
+ },
64
+ {
65
+ "task_id": "T02", "title": "Vendor Colours",
66
+ "description": "At an open-air market, photograph three vendor stalls whose canopies are three different colours.",
67
+ "landscape_tags_used": ["market_outdoor"],
68
+ "task_type": "find_and_photograph",
69
+ "difficulty_contribution": "easy", "points": 10,
70
+ "completion_proof": "photo", "estimated_time_minutes": 12,
71
+ "hints": {
72
+ "hint_1": "Follow the sounds of commerce.",
73
+ "hint_2": "Open-air markets cluster along wide pedestrian streets; look for rows of temporary stalls with overhead canopies.",
74
+ "hint_3": "Enter the market from its main entrance, walk one full aisle from start to finish, and photograph the first red, the first blue, and the first white canopy you pass.",
75
+ },
76
+ "safety_flags": [],
77
+ },
78
+ {
79
+ "task_id": "T03", "title": "Tallest Sighting",
80
+ "description": "Find a spot where the tallest structure in the area is fully visible. Photograph it with at least one other person in frame.",
81
+ "landscape_tags_used": ["iconic_landmark"],
82
+ "task_type": "reach_and_verify",
83
+ "difficulty_contribution": "easy", "points": 10,
84
+ "completion_proof": "photo", "estimated_time_minutes": 14,
85
+ "hints": {
86
+ "hint_1": "Look up from the most open ground you can find.",
87
+ "hint_2": "Tall structures are easiest to see from squares or wide streets with no overhead obstruction — position yourself so the full height is unblocked.",
88
+ "hint_3": "Return to the open square, face the direction where rooftops are at their lowest, and the tallest structure will dominate your skyline; frame it with a teammate visible in the foreground.",
89
+ },
90
+ "safety_flags": [],
91
+ },
92
+ ],
93
+ "task_count": 3,
94
+ "total_possible_points": 30,
95
+ "max_deductible_points": 30,
96
+ "minimum_possible_points": 0,
97
+ "bonus_task_eligible": False,
98
+ "bonus_task": {"description":None,"points":None,"risk":None,"completion_proof":None},
99
+ "scoring_summary": {
100
+ "base_points_available": 30,
101
+ "time_bonus_per_minute_early": None,
102
+ "hint_cost_tier_1": 5, "hint_cost_tier_2": 10,
103
+ "team_aggregation_method": None,
104
+ "winning_condition_detail": "The individual with the highest total points when the 60-minute limit expires wins; ties broken by earliest final submission.",
105
+ },
106
+ "estimated_total_time_minutes": 36,
107
+ "quality_score": 4.5,
108
+ },
109
+ }
110
+
111
+ passed = failed = 0
112
+
113
+ def check(name, condition, extra=""):
114
+ global passed, failed
115
+ if condition:
116
+ print(f" PASS {name}")
117
+ passed += 1
118
+ else:
119
+ print(f" FAIL {name} {extra}")
120
+ failed += 1
121
+
122
+ print("── Validator unit tests ──────────────────────────────")
123
+
124
+ ok, errs = validate_example(GOOD)
125
+ check("T1: good example passes", ok, errs)
126
+
127
+ bad = copy.deepcopy(GOOD)
128
+ bad["output"]["tasks"][0]["description"] = "Walk to the Eiffel Tower and count the benches."
129
+ ok, errs = validate_example(bad)
130
+ check("T2: proper noun caught", not ok and any("NOUN" in e for e in errs))
131
+
132
+ bad = copy.deepcopy(GOOD)
133
+ bad["output"]["tasks"][1]["landscape_tags_used"] = ["coastal_waterfront"]
134
+ ok, errs = validate_example(bad)
135
+ check("T3: tag not in input caught", not ok and any("TAGS" in e for e in errs))
136
+
137
+ bad = copy.deepcopy(GOOD)
138
+ bad["input"]["players"]["age_group"] = "children_only"
139
+ bad["output"]["rules"]["scoring_method"] = "timed_bonus"
140
+ bad["output"]["scoring_summary"]["time_bonus_per_minute_early"] = 2
141
+ ok, errs = validate_example(bad)
142
+ check("T4: children + timed_bonus caught", not ok and any("SCORING" in e for e in errs))
143
+
144
+ bad = copy.deepcopy(GOOD)
145
+ bad["output"]["total_possible_points"] = 9999
146
+ bad["output"]["task_count"] = 99
147
+ fixed = auto_fix(bad)
148
+ ok, errs = validate_example(fixed)
149
+ check("T5: auto_fix repairs arithmetic", ok, errs)
150
+
151
+ bad = copy.deepcopy(GOOD)
152
+ bad["output"]["tasks"][0]["estimated_time_minutes"] = 70
153
+ bad = auto_fix(bad)
154
+ ok, errs = validate_example(bad)
155
+ check("T6: time overrun caught", not ok and any("TIME" in e for e in errs))
156
+
157
+ # ── 7: sampler variety ────────────────────────────────────────────────────────
158
+ print("\n── Sampler tests ─────────────────────────────────────")
159
+ samples = [sample_input(i, rng=random.Random(i)) for i in range(1, 21)]
160
+ cities = [s["input"]["location"]["city"] for s in samples]
161
+ diffs = [s["input"]["preferences"]["difficulty"] for s in samples]
162
+ ages = [s["input"]["players"]["age_group"] for s in samples]
163
+ check("T7a: ≥8 different cities in 20 samples", len(set(cities)) >= 8, f"got {set(cities)}")
164
+ check("T7b: all 3 difficulties appear", set(diffs) == {"easy","medium","hard"})
165
+ check("T7c: all 5 age groups appear", set(ages) == {"children_only","teens","adults","mixed_family","mixed_adults"})
166
+
167
+ # verify prompt builds without error
168
+ prompt = build_prompt(samples[0])
169
+ check("T7d: prompt builds without error", len(prompt) > 500)
170
+
171
+ # ── 8: full pipeline mock (no API) ────────────────────────────────────────────
172
+ print("\n── Pipeline smoke-test (mock, no API) ────────────────")
173
+ with tempfile.TemporaryDirectory() as tmp:
174
+ out_path = os.path.join(tmp, "smoke.json")
175
+ result = subprocess.run(
176
+ [sys.executable, GENERATOR, "--n", "10", "--output", out_path, "--mock", "--seed", "7"],
177
+ capture_output=True, text=True
178
+ )
179
+ if result.returncode != 0:
180
+ print(" FAIL subprocess error:", result.stderr[-300:])
181
+ failed += 1
182
+ else:
183
+ try:
184
+ data = json.load(open(out_path))
185
+ check("T8a: generated 10 examples", len(data) == 10, f"got {len(data)}")
186
+ # re-validate every example from disk
187
+ bad_ids = [e["id"] for e in data if not validate_example(e)[0]]
188
+ check("T8b: all 10 pass re-validation", len(bad_ids) == 0, f"failed: {bad_ids}")
189
+ cities_seen = {e["input"]["location"]["city"] for e in data}
190
+ check("T8c: ≥5 different cities", len(cities_seen) >= 5, f"got {cities_seen}")
191
+ except Exception as exc:
192
+ print(f" FAIL could not load/check output: {exc}")
193
+ failed += 1
194
+
195
+ # ── 9: resume check ───────────────────────────────────────────────────────
196
+ print("\n── Resume-from-disk test ─────────────────────────────")
197
+ out2 = os.path.join(tmp, "smoke2.json")
198
+ # first run: 5 examples
199
+ subprocess.run([sys.executable,GENERATOR,"--n","5","--output",out2,"--mock","--seed","1"],
200
+ capture_output=True)
201
+ first_count = len(json.load(open(out2)))
202
+ # second run: 5 more
203
+ subprocess.run([sys.executable,GENERATOR,"--n","5","--output",out2,"--mock","--seed","2"],
204
+ capture_output=True)
205
+ second_count = len(json.load(open(out2)))
206
+ check("T9: resume adds to existing file", second_count == first_count + 5,
207
+ f"before={first_count}, after={second_count}")
208
+
209
+ # ── 10: stats helper ──────────────────────────────────────────────────────────
210
+ print("\n── Stats quick-check ─────────────────────────────────")
211
+ with tempfile.TemporaryDirectory() as tmp:
212
+ sp = os.path.join(tmp, "stats.json")
213
+ subprocess.run([sys.executable,GENERATOR,"--n","15","--output",sp,"--mock","--seed","99"],
214
+ capture_output=True)
215
+ data = json.load(open(sp))
216
+ from collections import Counter
217
+ diffs_out = Counter(e["input"]["preferences"]["difficulty"] for e in data)
218
+ check("T10: all 3 difficulties present in 15 examples",
219
+ len(diffs_out) == 3, dict(diffs_out))
220
+
221
+ # ── summary ───────────────────────────────────────────────────────────────────
222
+ print(f"\n{'─'*54}")
223
+ print(f" {passed} passed | {failed} failed | {passed+failed} total")
224
+ if failed == 0:
225
+ print(" All tests passed — pipeline is ready for real API calls.")
226
+ else:
227
+ print(" Fix failing tests before running against the real API.")
scripts/scavenger_hunt/validate_dataset.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ validate_dataset.py — scavenger_hunt
3
+ Re-runs validate_example() on every record in a dataset.json and reports
4
+ pass/fail counts plus failure-category breakdown. Also reports how many
5
+ examples were skipped during generation (dataset_errors.jsonl).
6
+
7
+ Usage:
8
+ python validate_dataset.py [path/to/dataset.json]
9
+ (defaults to app/data/scavenger_hunt/dataset.json)
10
+ """
11
+ import json
12
+ import os
13
+ import sys
14
+ from collections import Counter
15
+
16
+ HERE = os.path.dirname(os.path.abspath(__file__))
17
+ sys.path.insert(0, HERE)
18
+ from validator import validate_example
19
+
20
+ DEFAULT_PATH = os.path.normpath(os.path.join(
21
+ HERE, "..", "..", "app", "data", "scavenger_hunt", "dataset.json"))
22
+
23
+
24
+ def main():
25
+ path = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_PATH
26
+ if not os.path.exists(path):
27
+ print(f"File not found: {path}")
28
+ sys.exit(1)
29
+
30
+ data = json.load(open(path, encoding="utf-8"))
31
+ passed = failed = 0
32
+ failure_reasons = Counter()
33
+ failed_ids = []
34
+
35
+ for ex in data:
36
+ ok, errors = validate_example(ex)
37
+ if ok:
38
+ passed += 1
39
+ else:
40
+ failed += 1
41
+ failed_ids.append(ex.get("id", "?"))
42
+ for e in errors:
43
+ failure_reasons[e.split(":")[0]] += 1
44
+
45
+ print(f"\n── Validation report: {path} ──────────────────")
46
+ print(f" Total : {len(data)}")
47
+ print(f" Passed : {passed}")
48
+ print(f" Failed : {failed}")
49
+ if failed:
50
+ print("\n Failure categories:")
51
+ for reason, count in failure_reasons.most_common():
52
+ print(f" {reason:10s}: {count}")
53
+ print("\n Failed example ids:", failed_ids)
54
+
55
+ errors_path = path.replace(".json", "_errors.jsonl")
56
+ if os.path.exists(errors_path):
57
+ with open(errors_path, encoding="utf-8") as f:
58
+ n_skipped = sum(1 for _ in f)
59
+ print(f"\n Skipped during generation (logged in {errors_path}): {n_skipped}")
60
+ else:
61
+ print(f"\n No {os.path.basename(errors_path)} found — no examples were skipped during generation.")
62
+
63
+
64
+ if __name__ == "__main__":
65
+ main()
scripts/scavenger_hunt/validator.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ validator.py — scavenger_hunt
3
+ Validates one training example. Returns (is_valid: bool, errors: list[str]).
4
+ auto_fix() recomputes arithmetic fields in-place — never reject for math the model shouldn't be trusted with.
5
+ """
6
+ import re
7
+ from Levenshtein import distance as lev
8
+
9
+ # ── proper-noun blocklist ──────────────────────────────────────────────────────
10
+ _NOUNS = [
11
+ "Paris","Tokyo","New York","NYC","Cape Town","Marrakech","Buenos Aires",
12
+ "Mumbai","Berlin","Sydney","Nairobi","Istanbul","Mexico City","Amsterdam",
13
+ "Bangalore","Lagos","Seoul","Copenhagen","Lisbon","Bogota","Auckland",
14
+ # famous landmarks
15
+ "Eiffel","Louvre","Seine","Notre-Dame","Notre Dame","Montmartre",
16
+ "Shibuya","Senso-ji","Sensoji","Skytree","Harajuku",
17
+ "Central Park","Times Square","Brooklyn","Manhattan","Hudson",
18
+ "Table Mountain","Bosphorus","Hagia","Zocalo","Zócalo",
19
+ "Han River","Gyeongbokgung","Nyhavn","Alfama","Tagus",
20
+ "La Boca","Recoleta","Gateway of India","Marine Drive",
21
+ "Brandenburg","Tiergarten","Opera House","Harbour Bridge","Bondi",
22
+ "Big Ben","Statue of Liberty","Golden Gate","Taj Mahal",
23
+ "Colosseum","Vatican","Kremlin","Buckingham","Westminster",
24
+ ]
25
+ _NOUN_RE = re.compile(r"\b(" + "|".join(re.escape(n) for n in _NOUNS) + r")\b")
26
+
27
+ # ── controlled tag vocabulary ─────────────────────────────────────────────────
28
+ VALID_TAGS = {
29
+ "park_large","park_small","river_waterfront","lake_or_pond",
30
+ "coastal_waterfront","forest_urban","garden_formal","hill_or_elevation",
31
+ "dense_urban_grid","wide_boulevard","narrow_alley_network","market_outdoor",
32
+ "market_covered","plaza_or_square","bridge_pedestrian","train_station_major",
33
+ "port_or_harbour","iconic_landmark","historic_district",
34
+ "religious_site_accessible","museum_cluster","street_art_district",
35
+ "university_campus","shopping_street","cafe_dense","food_market",
36
+ "residential_neighbourhood",
37
+ }
38
+
39
+ POINTS = {"easy": 10, "medium": 20, "hard": 30}
40
+
41
+
42
+ def _nouns_in(text: str) -> list:
43
+ return _NOUN_RE.findall(text or "")
44
+
45
+
46
+ def validate_example(ex: dict) -> tuple[bool, list[str]]:
47
+ errors = []
48
+
49
+ # ── structural guard ──────────────────────────────────────────────────────
50
+ try:
51
+ inp = ex["input"]
52
+ out = ex["output"]
53
+ loc = inp["location"]
54
+ tasks = out["tasks"]
55
+ except (KeyError, TypeError) as e:
56
+ return False, [f"STRUCTURE: missing key {e}"]
57
+ if not tasks:
58
+ return False, ["STRUCTURE: tasks array is empty"]
59
+
60
+ input_tags = set(loc.get("landscape_tags", []))
61
+
62
+ # ── vocabulary ────────────────────────────────────────────────────────────
63
+ bad = input_tags - VALID_TAGS
64
+ if bad:
65
+ errors.append(f"VOCAB: unknown tags {sorted(bad)}")
66
+
67
+ # ── per-task ──────────────────────────────────────────────────────────────
68
+ types_seen, total_pts, total_time, diff_counts = [], 0, 0, {"easy":0,"medium":0,"hard":0}
69
+
70
+ for t in tasks:
71
+ tid = t.get("task_id", "T??")
72
+
73
+ # proper nouns in description
74
+ found = _nouns_in(t.get("description",""))
75
+ if found:
76
+ errors.append(f"NOUN: {tid} description contains {set(found)}")
77
+
78
+ # proper nouns in hints
79
+ for hk in ("hint_1","hint_2","hint_3"):
80
+ found = _nouns_in(t.get("hints",{}).get(hk,""))
81
+ if found:
82
+ errors.append(f"NOUN: {tid} {hk} contains {set(found)}")
83
+
84
+ # tags used must be subset of input tags
85
+ used = set(t.get("landscape_tags_used",[]))
86
+ extra = used - input_tags
87
+ if extra:
88
+ errors.append(f"TAGS: {tid} uses tags not in input {sorted(extra)}")
89
+
90
+ # points must match difficulty
91
+ d, p = t.get("difficulty_contribution"), t.get("points")
92
+ if d in POINTS and p != POINTS[d]:
93
+ errors.append(f"POINTS: {tid} is {d} but has {p} pts (want {POINTS[d]})")
94
+ if d in diff_counts:
95
+ diff_counts[d] += 1
96
+
97
+ # hint progression — distinct + hint_3 longer than hint_1
98
+ h1 = t.get("hints",{}).get("hint_1","")
99
+ h2 = t.get("hints",{}).get("hint_2","")
100
+ h3 = t.get("hints",{}).get("hint_3","")
101
+ if h1 and h2 and lev(h1.lower(), h2.lower()) < 8:
102
+ errors.append(f"HINTS: {tid} hint_1 ≈ hint_2 (too similar)")
103
+ if h2 and h3 and lev(h2.lower(), h3.lower()) < 8:
104
+ errors.append(f"HINTS: {tid} hint_2 ≈ hint_3 (too similar)")
105
+ if h1 and h3 and len(h3) <= len(h1):
106
+ errors.append(f"HINTS: {tid} hint_3 not more detailed than hint_1")
107
+
108
+ types_seen.append(t.get("task_type"))
109
+ total_pts += p if isinstance(p, int) else 0
110
+ total_time += t.get("estimated_time_minutes", 0)
111
+
112
+ # ── task-type diversity ───────────────────────────────────────────────────
113
+ needed = 3 if len(tasks) >= 4 else 2
114
+ if len(set(types_seen)) < needed:
115
+ errors.append(f"DIVERSITY: {len(set(types_seen))} task types, need {needed}")
116
+
117
+ # ── time budget ───────────────────────────────────────────────────────────
118
+ duration = inp.get("preferences",{}).get("duration_minutes", 0)
119
+ if total_time > duration:
120
+ errors.append(f"TIME: tasks sum {total_time}min > duration {duration}min")
121
+
122
+ # ── arithmetic checksums (informational — auto_fix handles these) ─────────
123
+ bonus_pts = (out.get("bonus_task") or {}).get("points") or 0
124
+ declared = out.get("total_possible_points")
125
+ expected = total_pts + bonus_pts
126
+ if declared not in (expected, total_pts):
127
+ errors.append(f"CHECKSUM: total_possible_points={declared}, computed={expected}")
128
+ if out.get("task_count") != len(tasks):
129
+ errors.append(f"CHECKSUM: task_count={out.get('task_count')} but {len(tasks)} tasks")
130
+
131
+ # ── scoring safety (hard rules only) ─────────────────────────────────────
132
+ age = inp.get("players",{}).get("age_group","")
133
+ method = out.get("rules",{}).get("scoring_method","")
134
+ tc = inp.get("players",{}).get("team_count", 1)
135
+ if age in ("children_only","mixed_family") and method == "timed_bonus":
136
+ errors.append(f"SCORING: {age} must not use timed_bonus")
137
+ if tc > 2 and method == "first_to_finish":
138
+ errors.append(f"SCORING: first_to_finish invalid with team_count={tc}")
139
+
140
+ # time_bonus field must match scoring_method
141
+ tb = out.get("scoring_summary",{}).get("time_bonus_per_minute_early")
142
+ if (tb is not None) != (method == "timed_bonus"):
143
+ errors.append(f"CHECKSUM: time_bonus_per_minute_early={tb} inconsistent with {method}")
144
+
145
+ # ── difficulty mix ────────────────────────────────────────────────────────
146
+ game_diff = inp.get("preferences",{}).get("difficulty","")
147
+ n = len(tasks)
148
+ if game_diff == "easy" and diff_counts["hard"] > 0:
149
+ errors.append(f"MIX: easy game has {diff_counts['hard']} hard tasks")
150
+ if game_diff == "hard" and n >= 4:
151
+ hard_pct = diff_counts["hard"] / n * 100
152
+ if hard_pct < 35:
153
+ errors.append(f"MIX: hard game only {hard_pct:.0f}% hard tasks (need ≥35%)")
154
+
155
+ return len(errors) == 0, errors
156
+
157
+
158
+ def auto_fix(ex: dict) -> dict:
159
+ """Recompute all arithmetic output fields in-place. Call before validate_example."""
160
+ try:
161
+ tasks = ex["output"]["tasks"]
162
+ for t in tasks:
163
+ d = t.get("difficulty_contribution")
164
+ if d in POINTS:
165
+ t["points"] = POINTS[d]
166
+ base = sum(t["points"] for t in tasks)
167
+ bonus_pts = (ex["output"].get("bonus_task") or {}).get("points") or 0
168
+ ex["output"]["task_count"] = len(tasks)
169
+ ex["output"]["total_possible_points"] = base + bonus_pts
170
+ ex["output"]["max_deductible_points"] = len(tasks) * 10
171
+ ex["output"]["minimum_possible_points"] = max(0, base + bonus_pts - len(tasks) * 10)
172
+ ex["output"]["estimated_total_time_minutes"] = sum(
173
+ t.get("estimated_time_minutes", 0) for t in tasks)
174
+ ex["output"]["scoring_summary"]["base_points_available"] = base + bonus_pts
175
+ except (KeyError, TypeError):
176
+ pass
177
+ return ex
scripts/system_prompt.txt DELETED
@@ -1,621 +0,0 @@
1
-
2
- ## CRITICAL DESIGN PRINCIPLE (READ BEFORE GENERATING)
3
-
4
- The model being trained must generalize to **any city it has never seen**.
5
- That means the training data must teach **feature-to-task reasoning**, NOT city-specific memorization.
6
-
7
- **Wrong approach (do NOT do this):**
8
- > Input: `city = "Paris"` → Output: `"Find the Eiffel Tower and take a photo"`
9
-
10
- **Correct approach:**
11
- > Input: `city = "Paris", landscape = ["iconic_landmark", "river_waterfront", "dense_urban"]`
12
- > → Output: `"Find the most recognizable vertical structure visible from multiple districts and photograph it from street level"`
13
-
14
- **KEY FIX #1: The Landmark Rule (from conflict audit fix #1)**
15
-
16
- Landmark tasks must reference observable structural properties, NOT tourist salience or proper nouns.
17
-
18
- **ALLOWED landmark task patterns:**
19
- - "Find the tallest vertical structure visible from the main plaza. Photograph its base from ground level. How many distinct building materials can you identify?"
20
- - "Locate the widest pedestrian path in the area. At its midpoint, photograph the most commonly-spoken languages on signs."
21
- - "Find a crossing point where two major pedestrian routes intersect. Stand at the intersection center and photograph the four cardinal directions."
22
-
23
- **FORBIDDEN landmark task patterns:**
24
- - "Find the Eiffel Tower" — proper noun, city-specific
25
- - "Find the most iconic structure" — "iconic" = tourist salience, city-specific
26
- - "Find the symbol of this city" — requires city identity knowledge
27
-
28
- **VERIFICATION:** For every task using the `iconic_landmark` tag, the task description must make sense if you erase the city name. No proper nouns allowed.
29
-
30
- ---
31
-
32
- ## SCHEMA DEFINITION
33
-
34
- ```json
35
- {
36
- "id": "string — format: SH-{CITY_CODE}-{INDEX:04d}, e.g. SH-TYO-0042",
37
-
38
- "input": {
39
- "game_type": "scavenger_hunt",
40
-
41
- "location": {
42
- "city": "string — city name",
43
- "country": "string — country name",
44
- "city_code": "string — 3-letter uppercase code, e.g. PAR, TYO, NYC, CPT",
45
- "landscape_tags": [
46
- "array of strings — structural environment types present in this city area.",
47
- "Choose 3–6 from the controlled vocabulary below.",
48
- "These tags are the PRIMARY signal the model learns task generation from."
49
- ],
50
- "urban_density": "string — one of: sparse | suburban | mixed | dense | hyper_dense",
51
- "climate_zone": "string — one of: tropical | arid | mediterranean | temperate | continental | polar",
52
- "area_type": "string — one of: city_center | historic_district | waterfront | park_district | mixed_residential | university_campus | market_district | industrial_repurposed"
53
- },
54
-
55
- "players": {
56
- "count": "integer — 2 to 20",
57
- "team_count": "integer — 1 to 5 (1 = solo/all vs all, >1 = team mode)",
58
- "age_group": "string — one of: children_only | teens | adults | mixed_family | mixed_adults",
59
- "mobility": "string — one of: standard | limited (limited = no stairs, no long distances)"
60
- },
61
-
62
- "preferences": {
63
- "duration_minutes": "integer — 30, 45, 60, 90, or 120",
64
- "difficulty": "string — one of: easy | medium | hard",
65
- "theme": "string — one of: observation | history | social | nature | urban_exploration | photography | logic",
66
- "allow_transport": "boolean — if false, all tasks must be walkable"
67
- }
68
- },
69
-
70
- "output": {
71
- "game_title": "string — a creative, thematic title for this specific game instance",
72
-
73
- "rules": {
74
- "objective": "string — one sentence describing the win condition",
75
- "scoring_method": "string — one of: first_to_finish | point_accumulation | timed_bonus (MUST match decision tree)",
76
- "task_reveal_mode": "string — one of: sequential | all_at_once | gated_by_points",
77
- "team_rules": "string or null — team-specific rule if team_count > 1, else null",
78
- "time_limit_minutes": "integer — matches preferences.duration_minutes",
79
- "disqualification_conditions": ["array of strings — specific actions that void a team/player"]
80
- },
81
-
82
- "safety_constraints": {
83
- "exclusion_zones": ["array of strings — types of locations tasks must never send players to (MUST include: private_property, active_roadway, construction_sites, restricted_government_buildings)"],
84
- "physical_limits": ["array of strings — physical actions that are prohibited (MUST include: no climbing, no jumping, no water entry, no restricted building entry)"],
85
- "high_risk_conditions": [
86
- {
87
- "condition": "string — type of risk (e.g., water_proximity, road_crossing, religious_site_exterior, steep_terrain)",
88
- "applies_if": "string — condition for this risk to apply",
89
- "task_restriction": "string — how tasks must be adapted",
90
- "supervision": "string — supervision guidance"
91
- }
92
- ],
93
- "climate_advisory": "string or null — climate-specific safety notes (from conflict fix #3)",
94
- "adult_supervision_required": "boolean",
95
- "notes": "string or null"
96
- },
97
-
98
- "tasks": [
99
- {
100
- "task_id": "string — format T{INDEX:02d}, e.g. T01, T02",
101
- "title": "string — short name for this task (shown on card UI)",
102
- "description": "string — full player-facing instruction. CRITICAL: Must NOT contain proper nouns (city names, landmarks, brands). Must be achievable using ONLY the landscape_tags present in input. (from conflict fix #1)",
103
- "landscape_tags_used": ["subset of input.location.landscape_tags that this task relies on"],
104
- "task_type": "string — one of: find_and_photograph | observe_and_answer | collect_and_return | reach_and_verify | social_interaction | timed_challenge",
105
- "difficulty_contribution": "string — one of: easy | medium | hard",
106
- "points": "integer — 10 | 20 | 30 | 50 based on difficulty_contribution",
107
- "completion_proof": "string — how a player proves this task is done (photo, verbal answer, physical item, GPS checkin, etc.)",
108
- "estimated_time_minutes": "integer — realistic time budget for this single task (max 20–30 min depending on type)",
109
- "hints": {
110
- "hint_1": "string — directional nudge, 5–15 words, vague location hint, NO proper nouns (from conflict fix #4)",
111
- "hint_2": "string — specific clue, 15–30 words, describes feature to look for, NO proper nouns",
112
- "hint_3": "string — near-explicit solution, 30–50 words, specific action to complete, NO proper nouns"
113
- },
114
- "safety_flags": ["array of strings — any safety notes specific to this task, empty array if none"]
115
- }
116
- ],
117
-
118
- "task_count": "integer — must equal tasks array length",
119
- "total_possible_points": "integer — sum of all tasks[].points (+ bonus if present)",
120
- "max_deductible_points": "integer — task_count × 10",
121
- "minimum_possible_points": "integer — total_possible_points − max_deductible_points",
122
- "bonus_task_eligible": "boolean — true only if difficulty=hard AND task_count >= 7 AND age_group != children_only",
123
- "bonus_task": {
124
- "description": "string or null — ONE optional high-risk TIMED CHALLENGE task worth 50 points. Only present if bonus_task_eligible=true. CRITICAL: MUST be timed_challenge type only, NOT logic puzzle (from conflict fix #6). Null otherwise.",
125
- "completion_proof": "string or null",
126
- "points": "integer or null — always 50 if present, null otherwise",
127
- "risk": "string or null — what the player risks by attempting it (e.g., time loss, −20 points on failure, not allowed to re-attempt)"
128
- },
129
-
130
- "scoring_summary": {
131
- "base_points_available": "integer — equals total_possible_points",
132
- "time_bonus_per_minute_early": "integer or null — only populated if scoring_method=timed_bonus (from conflict fix #2 decision tree). Null otherwise. Value: 2 for easy, 3 for medium, 5 for hard.",
133
- "hint_cost_tier_1": 5,
134
- "hint_cost_tier_2": 10,
135
- "team_aggregation_method": "string or null — only if team_count > 1. One of: sum_all_members | highest_individual | average_members. Null if team_count=1.",
136
- "winning_condition_detail": "string — explicit, unambiguous statement of how winner is determined, accounting for scoring_method and team_aggregation_method"
137
- },
138
-
139
- "estimated_total_time_minutes": "integer — sum of all tasks[].estimated_time_minutes. Must be ≤ preferences.duration_minutes.",
140
- "quality_score": "float — self-assessed score from 1.0 to 5.0 on task diversity, safety, city-agnostic generalizability"
141
- }
142
- }
143
- ```
144
-
145
- ---
146
-
147
- ## LANDSCAPE TAG CONTROLLED VOCABULARY
148
-
149
- Use **only** tags from this list. Do not invent new tags. Consistency is essential.
150
-
151
- **Natural / Green:**
152
- - `park_large` — large public park (>5 hectares)
153
- - `park_small` — pocket park or square
154
- - `river_waterfront` — navigable river with public access
155
- - `lake_or_pond` — standing water body in or near urban area
156
- - `coastal_waterfront` — sea or bay coastline
157
- - `forest_urban` — forested area within city limits
158
- - `garden_formal` — manicured botanical or palace garden
159
- - `hill_or_elevation` — elevated terrain with city views
160
-
161
- **Built Environment:**
162
- - `dense_urban_grid` — tight street grid, tall buildings
163
- - `wide_boulevard` — grand avenues with tree lines and wide pavements
164
- - `narrow_alley_network` — pedestrian laneways, medina-style, old town
165
- - `market_outdoor` — open-air market or bazaar
166
- - `market_covered` — enclosed market hall
167
- - `plaza_or_square` — large open civic space
168
- - `bridge_pedestrian` — walkable bridge over water
169
- - `train_station_major` — large transit hub with public spaces
170
- - `port_or_harbour` — working or heritage docklands
171
-
172
- **Cultural / Heritage:**
173
- - `iconic_landmark` — single dominant structure of civic/tourist significance (CRITICAL: must reference via observable features, not name)
174
- - `historic_district` — area of preserved historical architecture
175
- - `religious_site_accessible` — publicly accessible temple, church, mosque, etc.
176
- - `museum_cluster` — area dense with museums
177
- - `street_art_district` — neighbourhood known for murals and public art
178
- - `university_campus` — walkable academic grounds
179
-
180
- **Commerce / Social:**
181
- - `shopping_street` — pedestrian commercial strip
182
- - `cafe_dense` — neighbourhood with high café density
183
- - `food_market` — dedicated food vendor area
184
- - `residential_neighbourhood` — non-commercial walkable residential blocks
185
-
186
- ---
187
-
188
- ## POINTS CRITERIA & SCORING SYSTEM
189
-
190
- ### 1. Base Task Point Values
191
-
192
- | difficulty_contribution | Points |
193
- |---|---|
194
- | `easy` | 10 |
195
- | `medium` | 20 |
196
- | `hard` | 30 |
197
-
198
- **Distribution rule:** The mix of task difficulties within a game must match the overall game difficulty:
199
-
200
- | Game difficulty | Required task difficulty mix |
201
- |---|---|
202
- | easy | ≥ 70% easy tasks, 0% hard tasks |
203
- | medium | 30–50% easy, 30–50% medium, ≤ 20% hard |
204
- | hard | ≤ 20% easy, 30–40% medium, ≥ 40% hard |
205
-
206
- ### 2. Hint Deduction Costs
207
-
208
- | Hint tier | Cost to use |
209
- |---|---|
210
- | `hint_1` | **0 points** |
211
- | `hint_2` | **5 points** |
212
- | `hint_3` | **10 points** |
213
-
214
- Minimum per-task score after hints = 0 (never go negative).
215
-
216
- ### 3. Time Bonus (only when scoring_method = `timed_bonus`)
217
-
218
- | Game difficulty | Bonus per minute early |
219
- |---|---|
220
- | easy | +2 points per minute |
221
- | medium | +3 points per minute |
222
- | hard | +5 points per minute |
223
-
224
- **Constraint:** Only valid when `scoring_method = timed_bonus`. Never mix with `point_accumulation` or `first_to_finish`.
225
-
226
- ### 4. Scoring Method Selection (from conflict fix #2: Decision Tree)
227
-
228
- **DETERMINISTIC DECISION TREE (no ambiguity):**
229
-
230
- ```
231
- IF age_group IN {children_only, mixed_family}
232
- → MUST USE: point_accumulation
233
- ELSE IF team_count > 2
234
- → MUST USE: point_accumulation
235
- ELSE IF team_count IN {0, 1}
236
- IF difficulty = hard AND duration_minutes >= 60
237
- → PREFER: timed_bonus
238
- ELSE IF difficulty = easy AND duration_minutes <= 45
239
- → PREFER: first_to_finish
240
- ELSE
241
- → DEFAULT: point_accumulation
242
- ELSE IF team_count == 2
243
- IF difficulty = hard AND duration_minutes >= 90
244
- → PREFER: timed_bonus
245
- ELSE
246
- → DEFAULT: point_accumulation
247
- ```
248
-
249
- **Use this tree. No exceptions.**
250
-
251
- ### 5. Team Scoring Aggregation (when team_count > 1)
252
-
253
- | team_aggregation_method | When to use |
254
- |---|---|
255
- | `sum_all_members` | Cooperative team games where all members attempt all tasks |
256
- | `highest_individual` | Competitive team games where members split up and race |
257
- | `average_members` | Mixed format, fairness-weighted |
258
-
259
- Default: `sum_all_members` unless `task_reveal_mode = all_at_once` (which implies splitting up → use `highest_individual`).
260
-
261
- ### 6. Bonus Task Rules (hard games only, from conflict fix #6)
262
-
263
- If `bonus_task_eligible = true` (difficulty=hard AND task_count ≥ 7 AND age_group != children_only):
264
- - One optional bonus task is appended, always worth **50 points**
265
- - **CRITICAL (conflict fix #6):** bonus_task MUST be `timed_challenge` type ONLY. NO logic puzzles.
266
- - The bonus task must have a stated risk: attempting and failing incurs a **−20 point penalty**
267
- - The bonus task must NOT be required to complete the game
268
- - `bonus_task.description` must follow the same no-proper-noun rule as all other tasks
269
-
270
- If `bonus_task_eligible = false`: set `bonus_task` fields all to null.
271
-
272
- ### 7. Winning Condition Derivation
273
-
274
- `scoring_summary.winning_condition_detail` must be precise and unambiguous. Examples:
275
- - `"The team with the highest total points at time_limit wins. Ties broken by task completion time."`
276
- - `"The first individual player to submit proof for all tasks wins, regardless of point total."`
277
- - `"The player with the highest points at game end, adjusted for time bonus, wins. Hint deductions applied before bonus."`
278
-
279
- ---
280
-
281
- ## SAFETY RULES (from conflict fix #5: Risk Levels, Not Absolute Exclusions)
282
-
283
- ### Exclusion Zones (Hard Rules — No Negotiation)
284
-
285
- Always include:
286
- ```json
287
- "exclusion_zones": [
288
- "private_property",
289
- "active_roadway",
290
- "construction_sites",
291
- "restricted_government_buildings"
292
- ]
293
- ```
294
-
295
- Add conditionally:
296
- - If any water tag (`river_waterfront`, `lake_or_pond`, `coastal_waterfront`) is present:
297
- Add `"water_edge"` to exclusion_zones
298
- - If `religious_site_accessible` is in landscape_tags:
299
- Add `"religious_interiors"` to exclusion_zones
300
-
301
- ### High-Risk Conditions (Soft Rules — Flag but Allow)
302
-
303
- If landscape tags trigger a high-risk condition, the task MUST include a corresponding safety flag:
304
-
305
- **CONDITION 1: Water Proximity**
306
- ```json
307
- {
308
- "condition": "water_proximity",
309
- "applies_if": "river_waterfront OR lake_or_pond OR coastal_waterfront in landscape_tags",
310
- "task_restriction": "No tasks requiring entry into water. Photography from bank/dock OK. No swimming.",
311
- "supervision": "Adult supervision optional. Younger players benefit from it."
312
- }
313
- ```
314
-
315
- **CONDITION 2: Road Crossing**
316
- ```json
317
- {
318
- "condition": "road_crossing",
319
- "applies_if": "dense_urban_grid in landscape_tags",
320
- "task_restriction": "Road crossings allowed ONLY at marked crosswalks with traffic signals.",
321
- "supervision": "Adult supervision optional for age_group=mixed_family. Not required otherwise."
322
- }
323
- ```
324
-
325
- **CONDITION 3: Religious Site Exterior**
326
- ```json
327
- {
328
- "condition": "religious_site_exterior",
329
- "applies_if": "religious_site_accessible in landscape_tags",
330
- "task_restriction": "No entry into building interiors. Photography/observation from public exterior only. Do not photograph individuals. Respect prayer times.",
331
- "supervision": "Adult guidance recommended for mixed_family."
332
- }
333
- ```
334
-
335
- **CONDITION 4: Steep Terrain**
336
- ```json
337
- {
338
- "condition": "steep_terrain",
339
- "applies_if": "hill_or_elevation in landscape_tags",
340
- "task_restriction": "No climbing or jumping. Tasks must be completable via walking on marked paths only.",
341
- "supervision": "Adult supervision required if age_group=children_only or mobility=limited."
342
- }
343
- ```
344
-
345
- ### Physical Prohibitions (Hard Rules)
346
-
347
- No task may instruct:
348
- - Climbing
349
- - Jumping
350
- - Entering water
351
- - Entering restricted buildings
352
- - Blocking traffic
353
- - Crossing unmarked roads
354
- - Entering religious interiors
355
-
356
- ---
357
-
358
- ## CLIMATE ADVISORY (from conflict fix #3: Convert to Safety Notes, Not Task Constraints)
359
-
360
- Climate zones affect ONLY the `safety_constraints.climate_advisory` field, NOT task structure.
361
-
362
- | Climate | Safety Advisory |
363
- |---------|-----------------|
364
- | tropical | "Tropical climate: all outdoor tasks should have shade access within 10 minutes. Ensure sufficient water breaks. Avoid peak heat hours (11 AM–3 PM)." |
365
- | arid | "Arid climate: ensure shade/shelter access within 10 minutes of all tasks. Mandatory water carry. No sustained outdoor walking > 20 min per task." |
366
- | mediterranean | "Mediterranean climate: outdoor terrace and plaza activities encouraged. Seasonal variation in outdoor activity viability." |
367
- | temperate | "Temperate climate: no special constraints." |
368
- | continental | "Continental climate: if duration ≥ 90 min, include cold-weather advisory and recommend warm clothing." |
369
- | polar | "Polar climate: all outdoor tasks must be completable in ≤ 15 minutes. Mandatory warm clothing note. Break up outdoor time with indoor transitions." |
370
-
371
- ---
372
-
373
- ## HINT PROGRESSION (from conflict fix #4)
374
-
375
- Every task MUST have three distinct hints showing clear progression.
376
-
377
- **HINT PROGRESSION RUBRIC:**
378
-
379
- | Level | Characteristic | Word Count | Example |
380
- |-------|---|---|---|
381
- | Hint 1 | Directional nudge; identifies *area type*, not location | 5–15 words | "Start from the plaza. Look upward." |
382
- | Hint 2 | Observational clue; describes *feature to look for* | 15–30 words | "Face the direction with clearest sky. Highest point on horizon." |
383
- | Hint 3 | Near-explicit; describes *specific action* to complete | 30–50 words | "Walk toward plaza's north edge. Grey structure rising. Stop directly below." |
384
-
385
- **VALIDATION:**
386
- - All three hints must be distinct (Levenshtein distance > 8)
387
- - Hints must increase in word count: len(hint_2) > len(hint_1), len(hint_3) > len(hint_2)
388
- - NO proper nouns in any hint
389
- - NO direct repetition of task description
390
-
391
- **WORKED EXAMPLES:**
392
-
393
- **Example 1: find_and_photograph task**
394
- ```json
395
- {
396
- "task_id": "T03",
397
- "title": "Vertical Structure Photo",
398
- "description": "Locate the tallest vertical structure visible from the main plaza. Photograph its base from street level. In your photo, identify and list three distinct building materials you can see.",
399
- "hints": {
400
- "hint_1": "Start from the main plaza. Look upward.",
401
- "hint_2": "Face the direction with the clearest sky view. The structure will be the highest point on your horizon.",
402
- "hint_3": "Walk toward the plaza's north edge. The structure is the grey/metal framework rising above all surrounding buildings. Stop when directly below it."
403
- }
404
- }
405
- ```
406
-
407
- **Example 2: observe_and_answer task**
408
- ```json
409
- {
410
- "task_id": "T05",
411
- "title": "Crossing Point Language Survey",
412
- "description": "Find a major pedestrian crossing. At the intersection, photograph signs in all four cardinal directions. Report the three most common languages visible across your four photos.",
413
- "hints": {
414
- "hint_1": "Intersections are where major streets meet. Look for pedestrian crossings.",
415
- "hint_2": "You're looking for a place with signage in multiple languages. Walk to areas with visible shops and commercial activity.",
416
- "hint_3": "Find a crossing with painted zebra stripes or traffic signals. Stand at its center point. You should see shop signs in four directions."
417
- }
418
- }
419
- ```
420
-
421
- ---
422
-
423
- ## GENERATION STEPS
424
-
425
- ### Step 1: Sample the Input Space
426
-
427
- For each example, independently sample:
428
-
429
- | Field | Distribution |
430
- |---|---|
431
- | City | Uniform from the 20-city bank. No city > 15% of total examples. |
432
- | landscape_tags | 3–6 tags, plausible for the city. No coastal tags for landlocked cities. |
433
- | players.count | Uniform 2–20 |
434
- | players.team_count | 1: p=0.4, 2–3: p=0.4, 4–5: p=0.2 |
435
- | age_group | All 5 options with equal probability (20% each) |
436
- | difficulty | easy: 30%, medium: 40%, hard: 30% |
437
- | duration_minutes | 30 (15%), 45 (20%), 60 (35%), 90 (20%), 120 (10%) |
438
- | theme | All 7 options with equal probability |
439
- | allow_transport | true: p=0.3, false: p=0.7 |
440
-
441
- ### Step 2: Derive Task Count
442
-
443
- | Duration | Easy | Medium | Hard |
444
- |---|---|---|---|
445
- | 30 min | 3 | 4 | 4 |
446
- | 45 min | 4 | 5 | 5 |
447
- | 60 min | 5 | 6 | 7 |
448
- | 90 min | 6 | 8 | 9 |
449
- | 120 min | 8 | 10 | 12 |
450
-
451
- ### Step 3: Generate Tasks Using ONLY Landscape Tags
452
-
453
- **For each task:**
454
- - Pick 1–2 landscape tags from the input
455
- - The task description must make observational sense for those tag types generically
456
- - The description must NOT name any specific street, monument, building, or person by proper noun (from conflict fix #1)
457
- - The task must be physically completable in `estimated_time_minutes`
458
- - `task_type` distribution: at least 3 different task types must appear
459
- - `difficulty_contribution` distribution: must roughly match game difficulty (from conflict fix #8)
460
- - **Climate zone interaction (from conflict fix #3):** Climate zone affects ONLY safety notes, not task structure. No hard task constraints based on climate.
461
-
462
- ### Step 4: Apply Safety Rules (from conflict fix #5)
463
-
464
- 1. Always enforce exclusion_zones (hard rule)
465
- 2. Add high_risk_conditions conditionally (soft rule)
466
- 3. Ensure all tasks avoid physical_prohibitions (hard rule)
467
- 4. Add climate_advisory to safety_constraints (soft rule, not task constraints)
468
- 5. Set adult_supervision_required based on age_group
469
-
470
- ### Step 5: Validate Hints (from conflict fix #4)
471
-
472
- For each task, verify:
473
- - All three hints are distinct (Levenshtein > 8)
474
- - Length progression: len(hint_2) > len(hint_1), len(hint_3) > len(hint_2)
475
- - No proper nouns in any hint
476
- - Hints show progression from vague → specific → explicit
477
-
478
- ### Step 6: Score and Self-Assess
479
-
480
- Set `quality_score`:
481
- - 5.0: All tasks use different landscape tags, 3+ task types present, strong hint progression, all validation passes
482
- - 4.0: Minor tag redundancy, all safety rules met, hints clear
483
- - 3.0: Some near-duplicate tasks or vague hints
484
- - 2.0 or below: Any safety violation, any proper noun, any landscape tag inconsistency
485
-
486
- **Reject and regenerate if quality_score < 3.0.**
487
-
488
- ### Step 7: Validate All Arithmetic Checksums
489
-
490
- Verify these 9 invariants:
491
-
492
- 1. `total_possible_points` = Σ `tasks[i].points` (+ 50 if bonus_task is non-null)
493
- 2. `max_deductible_points` = `task_count` × 10
494
- 3. `minimum_possible_points` = `total_possible_points` − `max_deductible_points` (must be ≥ 0)
495
- 4. `estimated_total_time_minutes` = Σ `tasks[i].estimated_time_minutes` (must be ≤ `duration_minutes`)
496
- 5. `scoring_method` matches the decision tree (conflict fix #2)
497
- 6. `time_bonus_per_minute_early` is non-null **if and only if** `scoring_method = timed_bonus`
498
- 7. `team_aggregation_method` is non-null **if and only if** `team_count > 1`
499
- 8. `bonus_task` fields are non-null **if and only if** `bonus_task_eligible = true`
500
- 9. Task difficulty mix percentages match the game difficulty constraints
501
-
502
- **If any checksum fails, correct the output before emitting it.**
503
-
504
- ---
505
-
506
- ## ANTI-PATTERNS (Reject immediately if found)
507
-
508
- | Anti-pattern | Why it's fatal |
509
- |---|---|
510
- | Task says "go to the Eiffel Tower" | Proper noun = memorization, not generalization |
511
- | Task says "find a baguette shop" | City-specific cultural reference |
512
- | `landscape_tags_used` includes tags not in `input.location.landscape_tags` | Training signal contradiction |
513
- | All tasks are `find_and_photograph` | Degenerate task type distribution |
514
- | Hints are identical or near-duplicates | No training signal for hint quality |
515
- | `estimated_total_time_minutes` > `duration_minutes` | Physically impossible game |
516
- | Water-edge task without `water_edge` in exclusion_zones | Safety violation |
517
- | `scoring_method = timed_bonus` when `age_group IN {children_only, mixed_family}` | Age safety rule violation |
518
- | `scoring_method = first_to_finish` when `team_count > 2` | Unresolvable ties |
519
- | `bonus_task` is non-null when `difficulty != hard` | Mixed difficulty signal |
520
- | `total_possible_points` ≠ sum of `tasks[].points` | Checksum failure |
521
- | `climate_zone` has no expression in safety notes or task description | Dead input field |
522
- | `time_bonus_per_minute_early` populated when `scoring_method != timed_bonus` | Schema contradiction |
523
- | `bonus_task` type is `logical_puzzle` | Should be `timed_challenge` only (conflict fix #6) |
524
-
525
- ---
526
-
527
- ## BATCH REQUIREMENTS (for quota validation)
528
-
529
- Each batch MUST contain exactly 10 examples with:
530
-
531
- **City Distribution:**
532
- - All 10 cities must be different
533
-
534
- **Age Group Distribution (exact):**
535
- - children_only: 2
536
- - teens: 2
537
- - adults: 3
538
- - mixed_family: 2
539
- - mixed_adults: 1
540
-
541
- **Difficulty Distribution (exact):**
542
- - easy: 3
543
- - medium: 4
544
- - hard: 3
545
-
546
- **Scoring Method Distribution (exact):**
547
- - first_to_finish: 2
548
- - point_accumulation: 6
549
- - timed_bonus: 2
550
-
551
- **Duration Distribution (exact):**
552
- - 30 min: 1
553
- - 45 min: 2
554
- - 60 min: 4
555
- - 90 min: 2
556
- - 120 min: 1
557
-
558
- **Batch Summary Format (output after batch):**
559
- ```
560
- // BATCH N: VALID ✓ | Cities: [list] | Age: [counts] | Difficulty: [E:3, M:4, H:3]
561
- | Scoring: [FA:2, PA:6, TB:2] | Avg Quality: X.X | Checksum Pass: 10/10 | Proper Nouns: 0 |
562
- Hint Progression: 10/10
563
- ```
564
-
565
- ---
566
-
567
- ## CITY BANK
568
-
569
- Generate examples covering this geographic diversity:
570
-
571
- | City | Code | Key landscape characteristics |
572
- |---|---|---|
573
- | Paris | PAR | iconic_landmark, wide_boulevard, river_waterfront, garden_formal, historic_district |
574
- | Tokyo | TYO | hyper_dense, narrow_alley_network, religious_site_accessible, market_covered, iconic_landmark |
575
- | New York City | NYC | dense_urban_grid, park_large, iconic_landmark, coastal_waterfront, museum_cluster |
576
- | Cape Town | CPT | coastal_waterfront, hill_or_elevation, market_outdoor, historic_district |
577
- | Marrakech | MRK | narrow_alley_network, market_outdoor, religious_site_accessible, plaza_or_square |
578
- | Buenos Aires | BUE | wide_boulevard, plaza_or_square, cafe_dense, street_art_district, historic_district |
579
- | Mumbai | BOM | coastal_waterfront, market_outdoor, dense_urban_grid, religious_site_accessible |
580
- | Berlin | BER | park_large, street_art_district, museum_cluster, historic_district, wide_boulevard |
581
- | Sydney | SYD | coastal_waterfront, park_large, iconic_landmark, market_covered, bridge_pedestrian |
582
- | Nairobi | NBO | park_large, market_outdoor, residential_neighbourhood, dense_urban_grid |
583
- | Istanbul | IST | historic_district, religious_site_accessible, market_covered, coastal_waterfront, hill_or_elevation |
584
- | Mexico City | MEX | plaza_or_square, market_outdoor, museum_cluster, street_art_district, dense_urban_grid |
585
- | Amsterdam | AMS | river_waterfront, bridge_pedestrian, narrow_alley_network, cafe_dense, museum_cluster |
586
- | Bangalore | BLR | park_large, market_outdoor, cafe_dense, residential_neighbourhood |
587
- | Lagos | LOS | coastal_waterfront, market_outdoor, dense_urban_grid, food_market |
588
- | Seoul | SEO | dense_urban_grid, shopping_street, park_large, historic_district, narrow_alley_network |
589
- | Copenhagen | CPH | coastal_waterfront, cafe_dense, bridge_pedestrian, market_covered, park_small |
590
- | Lisbon | LIS | hill_or_elevation, narrow_alley_network, historic_district, coastal_waterfront, plaza_or_square |
591
- | Bogotá | BOG | street_art_district, market_outdoor, park_large, historic_district |
592
- | Auckland | AKL | coastal_waterfront, hill_or_elevation, park_large, market_outdoor |
593
-
594
- ---
595
-
596
- ## FINAL OUTPUT FORMAT
597
-
598
- Produce a JSON array: `[ {...}, {...}, ... ]`
599
-
600
- Each element is one complete training example matching the schema above.
601
- **No surrounding text. No markdown. No comments inside the JSON.**
602
- Return ONLY valid JSON.
603
-
604
- ---
605
-
606
- ## SUMMARY OF ALL FIXES
607
-
608
- This prompt incorporates solutions to all 10 critical conflicts from the audit:
609
-
610
- 1. ✓ **Landmark Rule**: Feature-based, no proper nouns
611
- 2. ✓ **Scoring Method Selection**: Deterministic decision tree, no ambiguity
612
- 3. ✓ **Climate Constraints**: Safety notes, not task constraints
613
- 4. ✓ **Hint Quality**: Worked examples + validation rubric + proper noun checks
614
- 5. ✓ **Safety Edge Cases**: Risk levels instead of absolute exclusions
615
- 6. ✓ **Bonus Task Logic**: Timed challenge only, no logic puzzles
616
- 7. ✓ **Batch Quotas**: Hard quotas, not soft targets
617
- 8. ✓ **Semantic Validation**: Proper noun regex, hint progression, landscape tag consistency
618
- 9. ✓ **City Name Masking**: Masking strategy specified for fine-tuning
619
- 10. ✓ **Dataset Scale**: Adjusted for 6-day timeline, 300–400 example target
620
-
621
- All conflicts are now **resolved and objective**. No ambiguity remains.