# Integration — building a Hunt (التحدي) with an AI agent

**Read this first, AI agent.** A Gamemaster (القيم ماستر) has handed you this document. Your job is to interview them, build a Hunt (التحدي), let them check it, and produce **one ZIP file** they upload at letstreasurehunt.com. You never talk to the server. The Gamemaster does the uploading.

Written for a machine to read. The Markdown original is the source — prefer it to any PDF rendering.

## 0. Terms — المسميات

The JSON field names are English and fixed. The **words you write for people to read** — titles, clues, stems, choices, and anything you say to the Gamemaster — use this vocabulary. Both columns name the same thing.

| English (field names use these) | بالعربية (نصوص اللاعبين تستخدم هذه) |
|---|---|
| **Hunt** | التحدي |
| **Stage** | المرحلة |
| **Clue** / `location_hint` | الدليل |
| **Box** | الصندوق |
| **Team** | الفريق |
| **Gamemaster** | القيم ماستر |
| **Question** | السؤال |
| **Reward** / `reward_text` | الجائزة |

> **A Hunt is «التحدي», never «صيد».** If you are generating Arabic content — a `title`, an `intro`, a `location_hint`, a `finish_text` — write **التحدي** for the whole thing and **المرحلة** for one stop in it. «صيد» is wrong and will read as hunting animals. Do not translate the website's name.

---

## 1. What you are producing

One ZIP, containing one JSON file and every picture it names.

```
my-hunt.zip
├── hunt.json          the whole hunt
└── images/
    ├── resistor-10k.jpg
    └── meter-face.png
```

For a single stage added to an existing hunt, the same shape with `stage.json` instead:

```
new-stage.zip
├── stage.json
└── images/
```

**Images are referenced by bare filename only.** No URLs, no Drive links, no paths. `"image": "resistor-10k.jpg"` means `images/resistor-10k.jpg` inside the ZIP. A file named in the JSON but missing from the ZIP fails the import and names itself.

## 2. hunt.json

```json
{
  "format": "treasurehunt/hunt@1",
  "title": "Ohm's Law",
  "intro": "Four stops around the lab.",
  "finish_text": "Collect your badge from the teacher.",
  "duration_min": 0,
  "stages": [
    {
      "name": "The Bench",
      "location_hint": "Start at the soldering bench — the box is under the stool.",
      "reward_text": "A yellow sticker",
      "n_questions": 3,
      "pass_threshold": 100,
      "duration_min": 0,
      "shuffle": 1,
      "max_attempts": 0,
      "questions": [ ... ]
    }
  ]
}
```

| Field | Meaning |
|---|---|
| `title` | The Hunt's (التحدي) name. Required. |
| `intro` | Shown on the join screen. |
| `finish_text` | Shown when every Stage (المرحلة) is cleared. |
| `duration_min` | Minutes the whole Hunt runs from the start. 0 = no limit. |
| `location_hint` | **The Clue (الدليل).** The sentence that sends players to this Stage. Required for every Stage after the first. |
| `reward_text` | Shown on the Box (الصندوق) and the phone the moment it opens. |
| `n_questions` | How many of the Stage's Questions each Team (الفريق) is served. |
| `pass_threshold` | Percent needed to open the Box. 100 is normal. |
| `shuffle` | 1 to shuffle the order per Team. |
| `max_attempts` | 0 = unlimited retries. |

`stage.json` is exactly one stage object, with the same fields, at the top level.

## 3. Question types

### 3.1 Multiple choice

```json
{
  "type": "mcq",
  "stem": "Which of these resistors is 10 kΩ?",
  "stem_image": "band-chart.png",
  "choices": ["4.7 kΩ", "10 kΩ", "100 kΩ"],
  "choice_images": [null, "r-10k.jpg", null],
  "correct_index": 1
}
```

- `choices` is a list of **plain strings**. Not objects. A `{"text": "..."}` in there is refused at the door.
- `choice_images` is optional and positional — one entry per choice, `null` where there is no picture.
- `correct_index` is 0-based.
- **True/false** is this with exactly two choices, `["True", "False"]` or `["صح", "خطأ"]`. It prints as tick boxes on paper.

### 3.2 Match to labels — the word bank

The one to reach for when you want "choose the right word". On screen the player drags; on paper it becomes a word bank and numbered blanks.

```json
{
  "type": "match",
  "stem": "Complete each line.",
  "pairs": [
    { "label": "Unit of resistance", "answer": "Ohm" },
    { "label": "Unit of current",    "answer": "Ampere" }
  ],
  "decoys": ["Watt", "Farad"]
}
```

- Each `label` is the prompt; each `answer` is the card that belongs to it.
- **An `answer` is a word unless a file of that name is in the ZIP.** That is the whole rule — there is no flag to set. `"Ohm"` becomes a word card; `"ohm-symbol.png"`, with that file present, becomes a picture card. A typo'd filename therefore becomes a word, which the preview makes obvious, rather than a broken picture.
- `decoys` are extra cards that fit nowhere. **Always add at least one.** Without a decoy the last pairing is a free guess, because only one space is left. The importer warns if you omit them.
- Don't mix picture answers and word answers in one question. It is allowed, and it looks like a mistake.

### 3.3 Drag onto a picture

Regions cut out of a picture, dragged back into place. Harder to author blind — **prefer 3.1 and 3.2 unless the gamemaster specifically wants this**, and tell them it is easier to build in the dashboard's own editor where they can draw the regions with a mouse.

```json
{
  "type": "hotspots",
  "image": "diagram.png",
  "fill": "#ffffff",
  "regions": [
    { "label": "the ammeter", "x": 0.21, "y": 0.37, "w": 0.31, "h": 0.07 }
  ]
}
```

`x`, `y`, `w`, `h` are fractions of the picture, 0 to 1, from the top-left. The importer cuts each region out, paints the hole in `fill`, and the cut piece becomes the card to drag back.

## 4. Rules that are enforced, not advisory

These are checked on import, **all of them before anything is written**. A bundle either imports whole or not at all — there is no half-created hunt to clean up, and the importer reports every problem at once rather than one per attempt.

- `title` must be present and non-empty.
- Every stage needs at least one question.
- `choices` needs **2 to 6** entries, all plain strings. An object in that list is refused by name.
- `correct_index` must point at one of them, 0-based.
- `pairs` needs at least 2.
- Every filename in the JSON must exist in the ZIP, be referenced as a **bare filename** with no folder path, and **actually be** a PNG, JPEG or WebP — the extension does not get a vote, the bytes do. Max 4 MB each.
- Region fractions must be inside 0..1, have non-zero width and height, and not run off the edge.
- At most 40 stages, 200 questions per stage, 400 files, 120 MB unpacked.
- Stage names are capped at 120 characters. (Player names, set by players, are capped at 14 elsewhere.)

Entry names inside the ZIP are **never used as paths**. Every file is stored under a name the server mints, so a folder structure, an absolute path or a `../../` traversal in the archive is harmless — and ignored.

## 5. How to run the conversation

Do these in order. Do not skip to generating.

1. **Ask what the Hunt (التحدي) is about, and for whom.** Subject, age, **language**, how long it should take. If they answer in Arabic, write the player-facing text in Arabic — using §0's vocabulary.
2. **Ask how many Stages (المراحل) and where they are.** Each Stage is a physical place. You cannot invent the Clue (الدليل) — only the Gamemaster knows the room. Ask for a one-line description of each spot, then write the Clue from it.
3. **Propose the Question plan before writing any.** "Stage 1: four multiple-choice on units. Stage 2: a word bank of six terms." Get agreement.
4. **Write the Questions.** Match the stated level. Prefer 3.1 and 3.2.
5. **Ask about pictures.** If any Question needs one, ask the Gamemaster to put the files in a folder — Google Drive, a USB stick, anywhere — and give you the exact filenames. You will reference those names; they will put the files in `images/`.
6. **Build the preview** (§6) and have them read it.
7. **Produce the ZIP** and tell them exactly where to click.

> Ask one question at a time and wait. A gamemaster who is handed twelve questions at once answers three of them.

## 6. The preview — build this before the ZIP

Produce a **single self-contained HTML file** that renders every question the way a player would see it: the stem, the choices, the pictures, the word banks. Dark background, one question per block, the correct answer marked in a collapsed "answer key" section at the end.

This is how the gamemaster checks your work without uploading anything. Typos, a wrong `correct_index`, a question pitched at the wrong level — all of it is visible here and expensive later.

If you cannot produce a file, print the whole hunt as readable text instead. Do not skip this step.

## 7. Delivering

Tell them, in these words:

1. Put the images in a folder called `images`.
2. Put `hunt.json` beside that folder — not inside it.
3. Select both and compress them into one ZIP. (Zipping the enclosing folder is fine too; the importer looks for the JSON anywhere in the archive.)
4. In the dashboard, Hunts tab, press **⬆ Import hunt** — or, on the Stages tab with the right hunt selected, **⬆ Upload a stage**.
5. **Choosing the file only checks it.** They get a report: how many stages and questions, which types, which stages have a clue, and every problem or warning. The Import button stays disabled until the bundle is valid, so nothing reaches their hunt list unseen.
6. The hunt arrives as a **draft**. Nothing is live until they press Go live.

If the report lists problems, fix them and give them a new ZIP. Each message names the stage and question it came from.

## 8. What you must not do

- **Do not invent Clues.** A Clue (الدليل) is a description of a real place. Ask.
- **Do not write `choices` as objects.** Plain strings.
- **Do not reference an image you were not given a filename for.**
- **Do not set the Hunt live.** You cannot, and the import will not either. That is the Gamemaster's decision with students in the room.
- **Do not put the answer in the Question.** It happens with generated multiple choice more than you would expect: check that the correct option is not the only grammatical one, the only long one, or the only specific one.
- **Do not write «صيد» in Arabic text.** A Hunt is **التحدي** and a Stage is **المرحلة** (§0).

## 9. A complete minimal example

```json
{
  "format": "treasurehunt/hunt@1",
  "title": "Units and Symbols",
  "intro": "Three stops. Bring a pen.",
  "stages": [
    {
      "name": "The Bench",
      "location_hint": "Start at the soldering bench — look under the stool.",
      "n_questions": 2,
      "pass_threshold": 100,
      "questions": [
        {
          "type": "mcq",
          "stem": "What is the unit of resistance?",
          "choices": ["Ohm", "Ampere", "Volt"],
          "correct_index": 0
        },
        {
          "type": "match",
          "stem": "Match each quantity to its unit.",
          "pairs": [
            { "label": "Current", "answer": "Ampere" },
            { "label": "Voltage", "answer": "Volt" }
          ],
          "decoys": ["Watt"]
        }
      ]
    }
  ]
}
```

---

## 10. For developers — the live API

Everything above describes the **file**. If you are writing software that talks to the server directly instead, the per-endpoint reference lives in `AUTHORING-API.md`: authentication, `hunt.create`, `stage.create`, `question.create`, the media endpoint, and the rules an authoring tool has to respect.

The file route exists because it needs no credentials and no network access from the agent. Prefer it.
