# Build an Educational Quiz

> Build Pocket Quiz with SkillKit in three short lessons: a first reply, two languages, then a game with sounds and a score per player. Then ship it.

Source: https://docs.thalovant.com/developers/skillkit-tutorial/

---
Build **Pocket Quiz**, a voice game that asks a question, offers three choices, and explains the answer. You'll make it speak, teach it French, turn it into a game with sounds and a separate score for each player, and package it for a hub.

Allow about 15 minutes for the first two lessons after installing the dependencies, then take the game and service lessons at your own pace. You only need to recognise a Python method and a string; the guide explains each new piece as you use it.

Each lesson is one idea on top of a working program. You run it, read what it says, change one thing, and run it again. No hub, account, or microphone is needed until the very end.

## What You Will Build

Here is part of the finished game. These are spoken words, not terminal commands:

```text
You: play pocket quiz
Quiz: Let’s play Pocket Quiz! Answer with just one, two, or three.
Quiz: Question 1. How many legs does a butterfly have?
      One: Four. Two: Six. Three: Eight.
You: two
Quiz: That’s right! Butterflies are insects, and insects have six legs.
      Say next for the next question, or again to hear this one.
```

| Lesson | You add | You see |
| --- | --- | --- |
| 1 | A reply | Your first sentence, then your own |
| 2 | English and French | The same skill answering in both languages |
| 3 | The game, sounds, and scores | Three questions, a fact after each answer, a score |

Then you build the installable package and put it on a test hub.

## Set Up Your Computer

You need Python **3.11 or newer**, a text editor, and a terminal. The commands are for Bash on Linux or macOS; on Windows, use WSL.

[Download the lessons](/downloads/skillkit-pocket-quiz.zip?v=7c67a05bb60ea22a), extract the ZIP, open a terminal inside the `skillkit-pocket-quiz` folder, and give the project its own Python environment:

```bash
python3 -m venv .venv
source .venv/bin/activate
```

Keep this terminal open. The folder holds the finished quiz and `checkpoints.py`, which creates a fresh copy of the project for each lesson and never overwrites one you already made. The lessons use **SkillKit 0.24.2**. OVOS is the part that listens and speaks; SkillKit is the small set of building blocks this code uses to work with it.

## Lesson 1: Make It Speak

```bash
python checkpoints.py 1 --install
cd lessons/1/thalovant-skill-pocket-quiz
python -m thalovant_skill_pocket_quiz.replay
```

```text
en-US: Pocket Quiz asks three little questions.
fr-FR: Pocket Quiz asks three little questions.
```

That's the whole skill so far. Open `thalovant_skill_pocket_quiz/__init__.py`:

```python
class PocketQuizSkill(ThalovantSkill):
    def reply(self, utterance, lang, context):
        return "Pocket Quiz asks three little questions."
```

`reply` returns the text for this first preview. Lesson 2 adds the handler that answers a spoken command. Change the sentence, save the file, and run the replay again. Your words come back. The French line is still English because nothing is translated yet; that's lesson 2.

Now run the tests:

```bash
python -m pytest -q
```

One fails: `tests/test_lesson.py` still expects the old sentence. Put your new sentence there too and run the tests again until they pass. When you change what a program says, its test should say so as well.

## Lesson 2: Give It Two Languages

```bash
cd ../../..
python checkpoints.py 2 --install
cd lessons/2/thalovant-skill-pocket-quiz
python -m thalovant_skill_pocket_quiz.replay
```

```text
en-US: Pocket Quiz asks three little questions. Say play pocket quiz to play.
fr-FR: Pocket Quiz pose trois petites questions. Dis démarre pocket quiz pour jouer.
```

Two things changed. The sentence moved out of the code into a **dialog file**, one per language:

```text
thalovant_skill_pocket_quiz/locale/en-US/dialog/preview.dialog
thalovant_skill_pocket_quiz/locale/fr-FR/dialog/preview.dialog
```

And the skill got an **intent**: a file of example phrases. When someone says one of them, OVOS calls the handler.

```text
locale/en-US/intents/pocket_quiz.start.intent   →  play pocket quiz
locale/fr-FR/intents/pocket_quiz.start.intent   →  démarre pocket quiz
```

The code that ties them together is short:

```python
    def reply(self, utterance, lang, context):
        return self.dialog("preview", lang)

    @intent_handler("pocket_quiz.start.intent")
    def handle_start(self, message):
        lang = self.lang_of(message)
        self.speak_to(message, self.reply(self.utterance(message), lang, self.context_of(message)))
```

`self.dialog` picks the line for a language. `self.lang_of(message)` reads the language from the message, so a French question gets a French answer. `self.speak_to(message, text)` says it to the room that asked. Notice that the code never mentions English or French.

A regional language works without another folder. Ask for Canadian French:

```bash
python -m thalovant_skill_pocket_quiz.replay fr-CA
```

**Try a change.** Add `begin pocket quiz` on its own line in the English intent file, then create `tests/test_my_phrase.py`:

```python
from thalovant_skill_pocket_quiz import PocketQuizSkill

def test_my_new_start_phrase():
    assert PocketQuizSkill().locale_resources.matches_literal_intent(
        "begin pocket quiz", "pocket_quiz.start", "en-US"
    )
```

Run `python -m pytest -q`: one more test passes. Keep the skill's name in the phrase so it has a clear job among the other skills on a hub.

## Lesson 3: Build The Game

```bash
cd ../../..
python checkpoints.py 3 --install
cd lessons/3/thalovant-skill-pocket-quiz
python -m thalovant_skill_pocket_quiz.replay
python -m thalovant_skill_pocket_quiz.replay --lang fr-FR
python -m pytest -q
```

The replay now plays a whole game: three questions, a fact after each answer, the name of each sound cue, and the score. The English game answers two, one, then three and finishes with **3 out of 3**; the French one answers deux, un, trois.

The finished skill is three files inside the `thalovant_skill_pocket_quiz` folder:

| File | Job |
| --- | --- |
| `__init__.py` | Talk to OVOS: receive commands, speak, play a cue, stop. |
| `game.py` | Keep the questions and the score, and decide the next reply. |
| `locale/<language>/questions.json` | The questions, choices, answers, and explanations. |

Open the first question in `locale/en-US/questions.json`:

```json
{
  "question": "How many legs does a butterfly have?",
  "choices": ["Four", "Six", "Eight"],
  "answer": 1,
  "explanation": "Butterflies are insects, and insects have six legs."
}
```

Python counts from **zero**, so `"answer": 1` is the second choice, Six. The player says "two"; the game compares that with position 1.

**Try a change.** Replace one question and its explanation in both language files. Keep three choices and an answer from 0 to 2, update the test that checks that question, and run the replay again.

### Keep Each Player's Game Separate

Alex has answered two questions while Sam has just started. One shared score would mix their games together. The **session ID** on each message is the name on that player's scorecard: `SessionStateStore` in `game.py` keeps one game per ID and forgets a game after 90 seconds without an answer. `stop_session` ends one player's game; `stop` ends them all when OVOS asks.

The tests cover two players at once, a game that expires, Stop for one player only, and a sentence such as "set a timer for one minute" being left to another skill.

### Hear The Sound Cues

The skill ships three short WAV files:

| Cue | Meaning |
| --- | --- |
| `correct.wav` | You found the answer. |
| `try-again.wav` | Not this time. Listen to the explanation, then say next. |
| `finish.wav` | The quiz is complete. |

**Correct answer**

<audio controls preload="none" aria-label="Correct answer sound"><source src="/downloads/pocket-quiz/correct.wav?v=80ed0c513e4e65b2" type="audio/wav" /></audio>

**Incorrect answer**

<audio controls preload="none" aria-label="Incorrect answer sound"><source src="/downloads/pocket-quiz/try-again.wav?v=8aa563c977edea33" type="audio/wav" /></audio>

**Finished quiz**

<audio controls preload="none" aria-label="Finished quiz sound"><source src="/downloads/pocket-quiz/finish.wav?v=5820260ea37ec8fe" type="audio/wav" /></audio>

To hear them during the replay, add `--play-sounds`. On a hub, `self.play_audio(...)` sends the cue to the device that asked, and the `sounds_enabled` setting turns cues off. `tools/generate_sounds.py` shows how the cues were made, if you want different notes.

### Give The Listener Time To Think

Open `locale/en-US/dialog/question.ssml` beside `question.dialog`. Both contain
the same words; the `.ssml` twin adds `<break time="250ms"/>` between choices.
The French files do the same. A compatible voice pauses briefly, while the
screen and text replay keep ordinary text.

In `initialize`, the skill passes `dialog=self.dialog` to `Quiz`. That lets
SkillKit find the translation, fill its values safely, and carry both speech
forms. The game still works with a plain reader during text-only replays.
Try changing all three pauses to `400ms`. Update the expected pause in
`tests/test_ovos.py` too: it checks that the spoken question carries the pauses
you chose. Run the tests, then try it on your test speaker.
The replay's sound option plays cues only; it does not synthesize this speech.
[The speech guide](/developers/writing-a-skill/#add-pauses-and-pronunciation)
explains twins and the Python helpers.

### Test It The Way OVOS Runs It

The game tests ask whether the code chose the right reply. `tests/test_ovos.py` asks whether OVOS delivers the command and the speech: it starts the real skill on a test bus, sends `play pocket quiz`, and checks that the answer comes back in the right language and that the cue goes to the right player. It ran as part of `pytest` above. [The handbook](/developers/writing-a-skill/#exercise-the-real-ovos-bus) covers the test bus and MiniCroft in more depth.

## Ship It

From the lesson 3 folder:

```bash
thalovant-skillkit check
python -m build
```

`check` validates your resources and checks for phrases that another skill might also answer. Read any warnings before continuing. `build` writes the installable **wheel** and a source archive into `dist/`. Use SkillKit’s [built-package check](/developers/writing-a-skill/#check-what-you-built) to verify that the questions, dialogs, and sounds are included.

To see the wheel work away from your project, install it in a throwaway environment and play a French game from there:

```bash
quiz_wheel="$(pwd)/dist/thalovant_skill_pocket_quiz-0.1.0-py3-none-any.whl"
quiz_check_dir="$(mktemp -d)"
python -m venv "$quiz_check_dir/venv"
"$quiz_check_dir/venv/bin/python" -m pip install --pre "$quiz_wheel"
(
  cd "$quiz_check_dir"
  "$quiz_check_dir/venv/bin/python" -m thalovant_skill_pocket_quiz.replay --lang fr-FR
)
```

### Put It On A Test Hub

Follow [Add Your Own Skill](/manage/add-your-own-skill/) to install from a published package or a Git repository; the hub fetches it from there, not from your laptop. Then ask `play pocket quiz`, answer `two`, and say `next`. Do it again in French with `démarre pocket quiz`. Check the explanations, the cues, the score, and Stop, with two clients if you can.

## Optional: Ask A Question Service

Lesson 4 in the download fetches questions from a small web service and falls back to the packaged questions when the service is slow, wrong, or gone. That's the shape of every skill that calls something online.

```bash
cd ../../..
python checkpoints.py 4 --install
cd lessons/4/thalovant-skill-pocket-quiz
python -m thalovant_skill_pocket_quiz.demo_provider --port 8765
```

That terminal now serves questions. In a second terminal, activate the same `.venv`, enter the lesson 4 folder, and run:

```bash
python -m thalovant_skill_pocket_quiz.replay --provider http://127.0.0.1:8765/questions
```

The first question becomes "Which animal is a mammal?". Stop the service with **Ctrl+C**, run the same replay again, and the packaged butterfly question is back. `provider.py` is the part to read: SkillKit's `JsonServiceClient` sends the request, limits how long it waits and how much it accepts, and leaves a failing service alone for a while. [Keep Your Skill Responsive](/developers/skillkit-runtime/#give-online-work-a-time-budget) explains those pieces.

## Add A Third Language

Copy `locale/en-US` to a new folder such as `locale/es-ES`. Translate the questions, the dialog files and their `.ssml` twins, the intent phrases, and the answer words, and keep names inside braces such as `{score}` exactly as they are. Add `es-ES` to `locale/supported.json`, then add tests that play a game in that language. Regional variants such as `fr-CA` already work without a folder; add one only for wording that differs, as the [handbook](/developers/writing-a-skill/#support-regional-variations) shows.

## Done When

- All three lessons run and their tests pass.
- The quiz explains right and wrong answers in English and in French.
- Two players keep separate scores, and Stop affects only the player who said it.
- Cues play when enabled; the replay and tests stay silent unless asked.
- `thalovant-skillkit check` and `python -m build` pass, and the wheel plays a game from another environment.
- A test hub answers and plays the cue on the client that asked.

## If Something Goes Wrong

| What you see | Next action |
| --- | --- |
| `python3` is not found | Install Python, reopen the terminal, and check `python3 --version`. |
| The lesson folder already exists | `checkpoints.py` never overwrites; delete the folder or pass a new path. |
| Python imports an earlier lesson | Activate this project's `.venv`, enter the current lesson folder, and run `checkpoints.py` with `--install` again. |
| A changed dialog or question is ignored | Restart the replay; resources are cached while a program runs. |
| The replay has no sound | Add `--play-sounds` and check your computer's audio output. Silence is the default. |
| A French reply comes back in English | Check the message language and the `fr-FR` files. |
| The hub cannot find the skill | Check its installation status, the package version, and the entry point in `pyproject.toml`. |

## Where Next

[What SkillKit Gives You](/developers/skillkit-capabilities/) helps you choose a helper by task. [Writing a Skill](/developers/writing-a-skill/) is the handbook, and [Keep Your Skill Responsive](/developers/skillkit-runtime/) covers services, sound libraries, and caches.
