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.
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
Section titled “What You Will Build”Here is part of the finished game. These are spoken words, not terminal commands:
You: play pocket quizQuiz: 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: twoQuiz: 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
Section titled “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, extract the ZIP, open a terminal inside the skillkit-pocket-quiz folder, and give the project its own Python environment:
python3 -m venv .venvsource .venv/bin/activateKeep 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
Section titled “Lesson 1: Make It Speak”python checkpoints.py 1 --installcd lessons/1/thalovant-skill-pocket-quizpython -m thalovant_skill_pocket_quiz.replayen-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:
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:
python -m pytest -qOne 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
Section titled “Lesson 2: Give It Two Languages”cd ../../..python checkpoints.py 2 --installcd lessons/2/thalovant-skill-pocket-quizpython -m thalovant_skill_pocket_quiz.replayen-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:
thalovant_skill_pocket_quiz/locale/en-US/dialog/preview.dialogthalovant_skill_pocket_quiz/locale/fr-FR/dialog/preview.dialogAnd the skill got an intent: a file of example phrases. When someone says one of them, OVOS calls the handler.
locale/en-US/intents/pocket_quiz.start.intent → play pocket quizlocale/fr-FR/intents/pocket_quiz.start.intent → démarre pocket quizThe code that ties them together is short:
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:
python -m thalovant_skill_pocket_quiz.replay fr-CATry a change. Add begin pocket quiz on its own line in the English intent file, then create tests/test_my_phrase.py:
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
Section titled “Lesson 3: Build The Game”cd ../../..python checkpoints.py 3 --installcd lessons/3/thalovant-skill-pocket-quizpython -m thalovant_skill_pocket_quiz.replaypython -m thalovant_skill_pocket_quiz.replay --lang fr-FRpython -m pytest -qThe 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:
{ "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
Section titled “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
Section titled “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
Incorrect answer
Finished quiz
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
Section titled “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
explains twins and the Python helpers.
Test It The Way OVOS Runs It
Section titled “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 covers the test bus and MiniCroft in more depth.
Ship It
Section titled “Ship It”From the lesson 3 folder:
thalovant-skillkit checkpython -m buildcheck 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 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:
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
Section titled “Put It On A Test Hub”Follow 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
Section titled “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.
cd ../../..python checkpoints.py 4 --installcd lessons/4/thalovant-skill-pocket-quizpython -m thalovant_skill_pocket_quiz.demo_provider --port 8765That terminal now serves questions. In a second terminal, activate the same .venv, enter the lesson 4 folder, and run:
python -m thalovant_skill_pocket_quiz.replay --provider http://127.0.0.1:8765/questionsThe 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 explains those pieces.
Add A Third Language
Section titled “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 shows.
Done When
Section titled “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 checkandpython -m buildpass, 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
Section titled “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
Section titled “Where Next”What SkillKit Gives You helps you choose a helper by task. Writing a Skill is the handbook, and Keep Your Skill Responsive covers services, sound libraries, and caches.