A pluggable terminal RPG framework for building skill-based learning games.
Project description
Quest Engine
██████╗ ██╗ ██╗███████╗███████╗████████╗
██╔═══██╗██║ ██║██╔════╝██╔════╝╚══██╔══╝
██║ ██║██║ ██║█████╗ ███████╗ ██║
██║▄▄ ██║██║ ██║██╔══╝ ╚════██║ ██║
╚██████╔╝╚██████╔╝███████╗███████║ ██║
╚══▀▀═╝ ╚═════╝ ╚══════╝╚══════╝ ╚═╝
A pluggable terminal RPG framework for building skill-based learning games.
Powers:
- NEXUS Quest — cyberpunk hacker RPG (Bash · SSH · Vim · Git · Docker · Postgres · Python · Regex · Linux · Kubernetes · AWS)
- The Young Lady's Illustrated Primer — children's educational adventure (Letters · Numbers · Science · Kindness · Geography · Math · History · Art · Coding)
What Is This?
Quest Engine is the pure game loop — XP, levels, saves, difficulty, daily challenges, star ratings, achievements, bookmarks, zone previews, completion certificates. It has zero game content of its own.
You bring the content (zones and challenges). The engine handles everything else.
Install
git clone https://github.com/thorski1/quest-engine
pip install -e ./quest-engine
Requirements: Python 3.10+, rich >= 13.0.0
Project Structure
Your game lives in its own repo alongside the engine:
quest-engine/ ← this package (engine only, no game content)
engine/
engine.py ← XP, levels, saves, achievements, speed records
ui.py ← all Rich TUI components
challenges.py ← quiz / fill-blank / live / ordered / arrange runner
skill_pack.py ← SkillPack dataclass + load_skill_pack()
campaign.py ← Campaign dataclass + CampaignSession
zone.py ← Zone dict helper
challenge.py ← Challenge dict helper
main.py ← run() and run_campaign() entry points
your-game/ ← your repo (content only)
skill-packs/
bash/
__init__.py ← exports SKILL_PACK = SkillPack(...)
zones.py ← all zone and challenge data
story.py ← narrative text (intros, outros, story)
campaigns/
my_campaign/
__init__.py ← exports CAMPAIGN = Campaign(...)
story.py ← chapter bridges, entry summaries
your_game/
main.py ← sets env vars, calls run() or run_campaign()
setup.cfg / pyproject.toml
Quick Start: Your First Pack
1. Install the engine
pip install -e ./quest-engine
2. Set the skill-packs directory
# your_game/main.py
import os
from pathlib import Path
from engine.main import run
_HERE = Path(__file__).parent.parent
os.environ.setdefault("QUEST_SKILL_PACKS_DIR", str(_HERE / "skill-packs"))
def main():
run("my_pack")
3. Create your skill pack
your-game/
skill-packs/
my_pack/
__init__.py
zones.py
story.py
4. Write your zones
skill-packs/my_pack/zones.py
from engine.zone import Zone
from engine.challenge import Challenge
ZONE_ORDER = ["intro_zone", "advanced_zone"]
ZONES = {
"intro_zone": Zone(
id="intro_zone",
title="Introduction",
description="The basics.",
challenges=[
Challenge(
id="q1",
type="quiz",
prompt="What is 2 + 2?",
options=["3", "4", "5", "6"],
answer="b",
explanation="2 + 2 = 4. The answer is B.",
hints=["Think about fingers on two hands.", "It's less than 5."],
xp=10,
),
Challenge(
id="q2",
type="fill_blank",
prompt="The capital of France is ___.",
answer="Paris",
explanation="Paris has been the capital of France since the 10th century.",
xp=10,
),
],
),
"advanced_zone": Zone(
id="advanced_zone",
title="Advanced Topics",
description="Going deeper.",
challenges=[
Challenge(
id="q3",
type="flag_quiz",
prompt="Which flag shows hidden files with ls?",
answers=["-a", "--all", "ls -a"],
explanation="`ls -a` includes dotfiles (hidden files beginning with `.`).",
hints=["The flag is a single letter.", "Think 'all'."],
xp=15,
),
],
),
}
5. Write your narrative
skill-packs/my_pack/story.py
INTRO_STORY = """
Your game's opening narrative. Shown once at the very beginning.
Can be multiple paragraphs. Supports Rich markup like [bold]bold[/bold].
"""
ZONE_INTROS = {
"intro_zone": "Text shown when the player first enters this zone.",
"advanced_zone": "Text shown when the player first enters the advanced zone.",
}
ZONE_COMPLETIONS = {
"intro_zone": "Text shown when the player finishes all challenges in this zone.",
"advanced_zone": "Well done — you've mastered the advanced zone.",
}
BOSS_INTROS = {
"intro_zone": "Text shown before the final challenge in the zone.",
"advanced_zone": "This is the hardest challenge yet.",
}
6. Define your SkillPack
skill-packs/my_pack/__init__.py
from engine.skill_pack import SkillPack
from .story import INTRO_STORY, ZONE_INTROS, ZONE_COMPLETIONS, BOSS_INTROS
from .zones import ZONES, ZONE_ORDER
SKILL_PACK = SkillPack(
id="my_pack",
title="My Pack",
subtitle="◈ My Subtitle ◈",
save_file_name="my_pack",
intro_story=INTRO_STORY,
quit_message="Come back soon.",
zone_order=ZONE_ORDER,
zones=ZONES,
zone_intros=ZONE_INTROS,
zone_completions=ZONE_COMPLETIONS,
boss_intros=BOSS_INTROS,
zone_achievement_map={
"intro_zone": "intro_done",
"advanced_zone": "advanced_done",
},
achievements={
"intro_done": ("First Steps", "Completed the intro zone"),
"advanced_done": ("Going Deep", "Mastered the advanced zone"),
},
level_titles=[
(1, "Novice"),
(5, "Learner"),
(10, "Scholar"),
(15, "Expert"),
(20, "Master"),
],
banner_ascii=r"""
__ ____ __ ____ _ _ ___ _ _
\ \/ /\ \ / / | _ \| | | |/ _ \| \| |
> < \ V / | |_) | |_| | (_) | .` |
/_/\_\ \_/ |____/ \___/ \___/|_|\_|
""",
)
7. Wire up setup.cfg
setup.cfg
[metadata]
name = my-game
version = 0.1.0
[options]
install_requires =
quest-engine
[options.entry_points]
console_scripts =
my-game = your_game.main:main
8. Run it
pip install -e ./my-game
my-game
Challenge Types — Full Reference
quiz — Multiple Choice
The player types A / B / C / D (or the option text, or 1/2/3/4).
Challenge(
id="q1",
type="quiz",
prompt="Which command lists directory contents?",
options=["ls", "cd", "pwd", "cat"],
answer="a", # correct option letter (a/b/c/d)
explanation="ls lists files and directories in the current directory.",
hints=["Think 'list'.", "Two letters."],
xp=10,
difficulty="easy", # easy / medium / hard / boss
)
fill_blank — Free Text Answer
The player types the exact answer (case-insensitive).
Challenge(
id="q2",
type="fill_blank",
prompt="The command ___ prints the current working directory.",
answer="pwd",
explanation="pwd stands for Print Working Directory.",
hints=["Three letters.", "P-W-D."],
xp=10,
)
flag_quiz — Multiple Valid Answers
Accepts any entry from the answers list. Great for command flags where ls -a and -a are both valid.
Challenge(
id="q3",
type="flag_quiz",
prompt="What flag shows hidden files with ls?",
answers=["-a", "--all", "ls -a", "ls --all"],
explanation="`-a` includes dotfiles. `ls -a` also accepted.",
hints=["Single letter flag.", "Think 'all'."],
xp=15,
)
live — Real Command in Sandbox
The player runs an actual shell command. The engine creates a temp directory, runs the command in it, and validates the result.
Challenge(
id="q4",
type="live",
prompt="Create a directory named 'output' inside the sandbox.",
explanation="mkdir creates a new directory: `mkdir output`",
hints=["Use mkdir.", "mkdir output"],
xp=20,
setup={
"dirs": ["src"], # pre-created directories
"files": {
"src/hello.txt": "hello world\n", # pre-created files
},
},
validation={"type": "dir_exists", "target": "output"},
)
Available validators:
| type | fields | passes when |
|---|---|---|
dir_exists |
target |
directory exists in sandbox |
file_exists |
target |
file exists in sandbox |
file_missing |
target |
file does NOT exist |
output_contains |
expected |
stdout or stderr contains expected string |
file_contains |
target, expected |
file content contains expected string |
file_executable |
target |
file has execute bit set |
file_perms |
target, expected_mode |
file permissions match (e.g. "755") |
multi |
checks: [...] |
all nested validators pass |
ordered — Sequence Ordering
The player types the correct order of steps as a space-separated number sequence.
Challenge(
id="q5",
type="ordered",
prompt="Put these git steps in order: commit, add, push, init",
items=["git commit -m 'msg'", "git add .", "git push", "git init"],
# correct order is: init(3), add(1), commit(0), push(2) — 0-based indices
answer=[3, 1, 0, 2],
explanation="init → add → commit → push is the correct git workflow.",
xp=20,
)
arrange — Matching Pairs
The player matches left items to right items using letters.
Challenge(
id="q6",
type="arrange",
prompt="Match each command to its purpose.",
pairs=[
{"left": "ls", "right": "List files"},
{"left": "cd", "right": "Change directory"},
{"left": "pwd", "right": "Print working directory"},
],
# right items may be shuffled for display; answer reflects the correct mapping
answer="A B C",
explanation="ls=List, cd=Change, pwd=Print.",
xp=20,
)
SkillPack — Full Field Reference
SkillPack(
# Required
id="my_pack", # unique string identifier
title="My Pack", # display title
zone_order=["zone_1", ...], # ordered list of zone IDs
zones={ # dict of zone_id → zone dict
"zone_1": {
"id": "zone_1",
"name": "Zone Display Name", # shown in UI
"description": "...", # shown in zone preview
"challenges": [...], # list of challenge dicts
},
},
# Narrative text
intro_story="...", # shown once at start
zone_intros={"zone_1": "..."}, # shown on first entry to each zone
zone_completions={"zone_1": "..."}, # shown on zone complete
boss_intros={"zone_1": "..."}, # shown before last challenge
# Save / display
save_file_name="my_pack", # filename (no spaces) for JSON save
subtitle="◈ Tagline ◈", # shown under banner
quit_message="See you later.", # shown on quit
default_player_name="Agent", # default name if player skips input
# Achievements
zone_achievement_map={"zone_1": "achievement_id"},
achievements={
"achievement_id": ("Short Title", "Longer description"),
},
# Progression
level_titles=[
(1, "Novice"), # (min_level, title)
(5, "Learner"),
(10, "Expert"),
],
# Display
banner_ascii=r"...", # raw string ASCII art for banner
kids_mode=False, # True → kid-friendly praise phrases
# Optional advanced
recommended_age="8+", # informational only
)
Campaign — Chaining Packs Into a Story
A Campaign sequences SkillPacks into chapters with connecting narrative.
campaigns/my_campaign/__init__.py
from engine.campaign import Campaign, ChapterDef
CAMPAIGN = Campaign(
id="my_campaign",
title="The Full Journey",
save_file_name="my_campaign",
intro_story="The opening of your campaign...",
final_story="The ending, shown after the last chapter.",
quit_message="Your journey continues...",
entry_summary_prefix="Last time, you...",
campaign_achievements={
"campaign_complete": ("Journey's End", "Finished all chapters"),
},
chapters=[
ChapterDef(
pack_name="intro_pack", # must match SkillPack.id
title="Chapter 1: Beginnings",
entry_summary="You are just starting out.",
intro_bridge="Transition text shown before this chapter starts.",
outro_bridge="Text shown after this chapter ends, before the next begins.",
recommended_age="8+", # optional
),
ChapterDef(
pack_name="advanced_pack",
title="Chapter 2: The Deep End",
entry_summary="Having mastered the basics, you push further.",
intro_bridge="You descend into the advanced section...",
outro_bridge="You have completed the journey.",
),
],
)
Entry point:
from engine.main import run_campaign
def main():
run_campaign("my_campaign") # loads CAMPAIGN from campaigns/my_campaign/__init__.py
Zone Helper — engine.zone.Zone
Zone is a dict subclass. Use it instead of plain dicts for cleaner zone definitions.
from engine.zone import Zone
Zone(
id="zone_1",
title="Zone One", # maps to dict key "name" (what the engine reads)
description="...",
challenges=[...],
# Optional display fields:
color="cyan",
icon="⚡",
subtitle="Subheader",
)
Challenge Helper — engine.challenge.Challenge
Challenge is a dict subclass. Use it for cleaner challenge definitions.
from engine.challenge import Challenge
Challenge(
id="ch_1",
type="quiz", # quiz / fill_blank / flag_quiz / live / ordered / arrange
prompt="...", # maps to dict key "question" (what the engine reads)
explanation="...", # maps to dict key "lesson"
answer="b", # for quiz/fill_blank
answers=["-a", "--all"], # for flag_quiz (multiple valid answers)
options=["A", "B", "C", "D"],
hints=["Hint 1", "Hint 2"],
xp=10,
difficulty="medium", # easy / medium / hard / boss
# For live challenges:
setup={"dirs": [...], "files": {...}},
validation={"type": "output_contains", "expected": "hello"},
# For ordered challenges:
items=["step A", "step B", "step C"],
# For arrange challenges:
pairs=[{"left": "cmd", "right": "description"}],
)
Engine Features
| Feature | Description |
|---|---|
| XP & Levels | Configurable level_titles per pack; XP scales with difficulty |
| Star Ratings | 1–3 stars per zone based on hints used and challenges skipped |
| Achievements | Auto-unlocked when zone is completed; displayed in stats |
| Daily Challenge | Deterministic pick per pack per day; 2× XP; streak tracking |
| Difficulty Modes | Easy (0.75× XP, free hints) / Normal / Hard (1.5× XP) |
| Speed Records | Per-challenge personal bests; new-record flash on screen |
| Bookmarks | Toggle with [b]; review from main menu |
| Zone Preview | Challenge list shown before entering zone |
| Help Screen | Full keybinding reference via [?] |
| Completion Certificate | ASCII grade art (S/A/B/C/D) on pack complete |
| Campaign Stats | Per-chapter star ratings, XP totals, overall grade |
| Kids Mode | kids_mode=True → kid-friendly praise pool and gentler UX |
| Save/Resume | JSON saves in ~/.quest_engine/<save_file_name>/ |
| Placement Quiz | Campaign can start with optional assessment to pick chapter |
In-Game Controls
[h] Hint Show next hint for current challenge
[b] Bookmark Toggle bookmark on current challenge
[d] Difficulty Switch difficulty (Easy / Normal / Hard)
[?] Help Full control reference
[s] Skip Skip current challenge (costs a star)
[q] Menu Return to main menu
Games Built on Quest Engine
| Game | Audience | Chapters | What You Learn |
|---|---|---|---|
| NEXUS Quest | Adults / Developers | 11 | Bash · SSH · Vim · Git · Docker · Postgres · Python · Regex · Linux · Kubernetes · AWS |
| The Primer | Children (5–12) | 9 | Letters · Numbers · Science · Kindness · Geography · Math · History · Art · Coding |
Requirements
- Python 3.10+
rich >= 13.0.0
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file quest_engine-1.1.0.tar.gz.
File metadata
- Download URL: quest_engine-1.1.0.tar.gz
- Upload date:
- Size: 55.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e350ca7d3a81825ce97231030ae461988dd101b26e64961ff58268129ca3ad47
|
|
| MD5 |
4ebab770ddd12ebb96709bc64809fa59
|
|
| BLAKE2b-256 |
23c0d8c39888c82271928b5afdd6e595ee647fee3c850450845b66386fd9f9ed
|
Provenance
The following attestation bundles were made for quest_engine-1.1.0.tar.gz:
Publisher:
publish.yml on thorski1/quest-engine
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
quest_engine-1.1.0.tar.gz -
Subject digest:
e350ca7d3a81825ce97231030ae461988dd101b26e64961ff58268129ca3ad47 - Sigstore transparency entry: 1186385804
- Sigstore integration time:
-
Permalink:
thorski1/quest-engine@7ddfb19d6db9a1e5d78d630c8a73bf49bb3527a0 -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/thorski1
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7ddfb19d6db9a1e5d78d630c8a73bf49bb3527a0 -
Trigger Event:
push
-
Statement type:
File details
Details for the file quest_engine-1.1.0-py3-none-any.whl.
File metadata
- Download URL: quest_engine-1.1.0-py3-none-any.whl
- Upload date:
- Size: 53.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d5b9b5808cb897329c80abd04d1d0a4a17b947bc0e8a51aba73d0558f88845dc
|
|
| MD5 |
0028038198fb96a6279ef5120345fdf4
|
|
| BLAKE2b-256 |
170ca2d8436b42b4a7a96fe304081ddfab474c2e959fccd3fcf585a414ef4db8
|
Provenance
The following attestation bundles were made for quest_engine-1.1.0-py3-none-any.whl:
Publisher:
publish.yml on thorski1/quest-engine
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
quest_engine-1.1.0-py3-none-any.whl -
Subject digest:
d5b9b5808cb897329c80abd04d1d0a4a17b947bc0e8a51aba73d0558f88845dc - Sigstore transparency entry: 1186385812
- Sigstore integration time:
-
Permalink:
thorski1/quest-engine@7ddfb19d6db9a1e5d78d630c8a73bf49bb3527a0 -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/thorski1
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7ddfb19d6db9a1e5d78d630c8a73bf49bb3527a0 -
Trigger Event:
push
-
Statement type: