quest-engine 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,619 @@
1
+ Metadata-Version: 2.4
2
+ Name: quest-engine
3
+ Version: 1.0.0
4
+ Summary: A pluggable terminal RPG framework for building skill-based learning games.
5
+ Author: thorski1
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/thorski1/quest-engine
8
+ Project-URL: Repository, https://github.com/thorski1/quest-engine
9
+ Project-URL: Bug Tracker, https://github.com/thorski1/quest-engine/issues
10
+ Keywords: terminal,game,rpg,learning,education,cli
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Education
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Games/Entertainment :: Role-Playing
21
+ Classifier: Topic :: Education
22
+ Classifier: Topic :: Terminals
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ Requires-Dist: rich>=13.0.0
26
+
27
+ # Quest Engine
28
+
29
+ ```
30
+ ██████╗ ██╗ ██╗███████╗███████╗████████╗
31
+ ██╔═══██╗██║ ██║██╔════╝██╔════╝╚══██╔══╝
32
+ ██║ ██║██║ ██║█████╗ ███████╗ ██║
33
+ ██║▄▄ ██║██║ ██║██╔══╝ ╚════██║ ██║
34
+ ╚██████╔╝╚██████╔╝███████╗███████║ ██║
35
+ ╚══▀▀═╝ ╚═════╝ ╚══════╝╚══════╝ ╚═╝
36
+ ```
37
+
38
+ A pluggable terminal RPG framework for building skill-based learning games.
39
+
40
+ **Powers:**
41
+ - [NEXUS Quest](https://github.com/thorski1/nexus-quest) — cyberpunk hacker RPG (Bash · SSH · Vim · Git · Docker · Postgres · Python · Regex · Linux · Kubernetes · AWS)
42
+ - [The Young Lady's Illustrated Primer](https://github.com/thorski1/primer) — children's educational adventure (Letters · Numbers · Science · Kindness · Geography · Math · History · Art · Coding)
43
+
44
+ ---
45
+
46
+ ## What Is This?
47
+
48
+ 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.
49
+
50
+ You bring the content (zones and challenges). The engine handles everything else.
51
+
52
+ ---
53
+
54
+ ## Install
55
+
56
+ ```bash
57
+ git clone https://github.com/thorski1/quest-engine
58
+ pip install -e ./quest-engine
59
+ ```
60
+
61
+ **Requirements:** Python 3.10+, `rich >= 13.0.0`
62
+
63
+ ---
64
+
65
+ ## Project Structure
66
+
67
+ Your game lives in its own repo alongside the engine:
68
+
69
+ ```
70
+ quest-engine/ ← this package (engine only, no game content)
71
+ engine/
72
+ engine.py ← XP, levels, saves, achievements, speed records
73
+ ui.py ← all Rich TUI components
74
+ challenges.py ← quiz / fill-blank / live / ordered / arrange runner
75
+ skill_pack.py ← SkillPack dataclass + load_skill_pack()
76
+ campaign.py ← Campaign dataclass + CampaignSession
77
+ zone.py ← Zone dict helper
78
+ challenge.py ← Challenge dict helper
79
+ main.py ← run() and run_campaign() entry points
80
+
81
+ your-game/ ← your repo (content only)
82
+ skill-packs/
83
+ bash/
84
+ __init__.py ← exports SKILL_PACK = SkillPack(...)
85
+ zones.py ← all zone and challenge data
86
+ story.py ← narrative text (intros, outros, story)
87
+ campaigns/
88
+ my_campaign/
89
+ __init__.py ← exports CAMPAIGN = Campaign(...)
90
+ story.py ← chapter bridges, entry summaries
91
+ your_game/
92
+ main.py ← sets env vars, calls run() or run_campaign()
93
+ setup.cfg / pyproject.toml
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Quick Start: Your First Pack
99
+
100
+ ### 1. Install the engine
101
+
102
+ ```bash
103
+ pip install -e ./quest-engine
104
+ ```
105
+
106
+ ### 2. Set the skill-packs directory
107
+
108
+ ```python
109
+ # your_game/main.py
110
+ import os
111
+ from pathlib import Path
112
+ from engine.main import run
113
+
114
+ _HERE = Path(__file__).parent.parent
115
+ os.environ.setdefault("QUEST_SKILL_PACKS_DIR", str(_HERE / "skill-packs"))
116
+
117
+ def main():
118
+ run("my_pack")
119
+ ```
120
+
121
+ ### 3. Create your skill pack
122
+
123
+ ```
124
+ your-game/
125
+ skill-packs/
126
+ my_pack/
127
+ __init__.py
128
+ zones.py
129
+ story.py
130
+ ```
131
+
132
+ ### 4. Write your zones
133
+
134
+ **`skill-packs/my_pack/zones.py`**
135
+
136
+ ```python
137
+ from engine.zone import Zone
138
+ from engine.challenge import Challenge
139
+
140
+ ZONE_ORDER = ["intro_zone", "advanced_zone"]
141
+
142
+ ZONES = {
143
+ "intro_zone": Zone(
144
+ id="intro_zone",
145
+ title="Introduction",
146
+ description="The basics.",
147
+ challenges=[
148
+ Challenge(
149
+ id="q1",
150
+ type="quiz",
151
+ prompt="What is 2 + 2?",
152
+ options=["3", "4", "5", "6"],
153
+ answer="b",
154
+ explanation="2 + 2 = 4. The answer is B.",
155
+ hints=["Think about fingers on two hands.", "It's less than 5."],
156
+ xp=10,
157
+ ),
158
+ Challenge(
159
+ id="q2",
160
+ type="fill_blank",
161
+ prompt="The capital of France is ___.",
162
+ answer="Paris",
163
+ explanation="Paris has been the capital of France since the 10th century.",
164
+ xp=10,
165
+ ),
166
+ ],
167
+ ),
168
+ "advanced_zone": Zone(
169
+ id="advanced_zone",
170
+ title="Advanced Topics",
171
+ description="Going deeper.",
172
+ challenges=[
173
+ Challenge(
174
+ id="q3",
175
+ type="flag_quiz",
176
+ prompt="Which flag shows hidden files with ls?",
177
+ answers=["-a", "--all", "ls -a"],
178
+ explanation="`ls -a` includes dotfiles (hidden files beginning with `.`).",
179
+ hints=["The flag is a single letter.", "Think 'all'."],
180
+ xp=15,
181
+ ),
182
+ ],
183
+ ),
184
+ }
185
+ ```
186
+
187
+ ### 5. Write your narrative
188
+
189
+ **`skill-packs/my_pack/story.py`**
190
+
191
+ ```python
192
+ INTRO_STORY = """
193
+ Your game's opening narrative. Shown once at the very beginning.
194
+ Can be multiple paragraphs. Supports Rich markup like [bold]bold[/bold].
195
+ """
196
+
197
+ ZONE_INTROS = {
198
+ "intro_zone": "Text shown when the player first enters this zone.",
199
+ "advanced_zone": "Text shown when the player first enters the advanced zone.",
200
+ }
201
+
202
+ ZONE_COMPLETIONS = {
203
+ "intro_zone": "Text shown when the player finishes all challenges in this zone.",
204
+ "advanced_zone": "Well done — you've mastered the advanced zone.",
205
+ }
206
+
207
+ BOSS_INTROS = {
208
+ "intro_zone": "Text shown before the final challenge in the zone.",
209
+ "advanced_zone": "This is the hardest challenge yet.",
210
+ }
211
+ ```
212
+
213
+ ### 6. Define your SkillPack
214
+
215
+ **`skill-packs/my_pack/__init__.py`**
216
+
217
+ ```python
218
+ from engine.skill_pack import SkillPack
219
+ from .story import INTRO_STORY, ZONE_INTROS, ZONE_COMPLETIONS, BOSS_INTROS
220
+ from .zones import ZONES, ZONE_ORDER
221
+
222
+ SKILL_PACK = SkillPack(
223
+ id="my_pack",
224
+ title="My Pack",
225
+ subtitle="◈ My Subtitle ◈",
226
+ save_file_name="my_pack",
227
+ intro_story=INTRO_STORY,
228
+ quit_message="Come back soon.",
229
+ zone_order=ZONE_ORDER,
230
+ zones=ZONES,
231
+ zone_intros=ZONE_INTROS,
232
+ zone_completions=ZONE_COMPLETIONS,
233
+ boss_intros=BOSS_INTROS,
234
+ zone_achievement_map={
235
+ "intro_zone": "intro_done",
236
+ "advanced_zone": "advanced_done",
237
+ },
238
+ achievements={
239
+ "intro_done": ("First Steps", "Completed the intro zone"),
240
+ "advanced_done": ("Going Deep", "Mastered the advanced zone"),
241
+ },
242
+ level_titles=[
243
+ (1, "Novice"),
244
+ (5, "Learner"),
245
+ (10, "Scholar"),
246
+ (15, "Expert"),
247
+ (20, "Master"),
248
+ ],
249
+ banner_ascii=r"""
250
+ __ ____ __ ____ _ _ ___ _ _
251
+ \ \/ /\ \ / / | _ \| | | |/ _ \| \| |
252
+ > < \ V / | |_) | |_| | (_) | .` |
253
+ /_/\_\ \_/ |____/ \___/ \___/|_|\_|
254
+ """,
255
+ )
256
+ ```
257
+
258
+ ### 7. Wire up setup.cfg
259
+
260
+ **`setup.cfg`**
261
+
262
+ ```ini
263
+ [metadata]
264
+ name = my-game
265
+ version = 0.1.0
266
+
267
+ [options]
268
+ install_requires =
269
+ quest-engine
270
+
271
+ [options.entry_points]
272
+ console_scripts =
273
+ my-game = your_game.main:main
274
+ ```
275
+
276
+ ### 8. Run it
277
+
278
+ ```bash
279
+ pip install -e ./my-game
280
+ my-game
281
+ ```
282
+
283
+ ---
284
+
285
+ ## Challenge Types — Full Reference
286
+
287
+ ### `quiz` — Multiple Choice
288
+
289
+ The player types A / B / C / D (or the option text, or 1/2/3/4).
290
+
291
+ ```python
292
+ Challenge(
293
+ id="q1",
294
+ type="quiz",
295
+ prompt="Which command lists directory contents?",
296
+ options=["ls", "cd", "pwd", "cat"],
297
+ answer="a", # correct option letter (a/b/c/d)
298
+ explanation="ls lists files and directories in the current directory.",
299
+ hints=["Think 'list'.", "Two letters."],
300
+ xp=10,
301
+ difficulty="easy", # easy / medium / hard / boss
302
+ )
303
+ ```
304
+
305
+ ### `fill_blank` — Free Text Answer
306
+
307
+ The player types the exact answer (case-insensitive).
308
+
309
+ ```python
310
+ Challenge(
311
+ id="q2",
312
+ type="fill_blank",
313
+ prompt="The command ___ prints the current working directory.",
314
+ answer="pwd",
315
+ explanation="pwd stands for Print Working Directory.",
316
+ hints=["Three letters.", "P-W-D."],
317
+ xp=10,
318
+ )
319
+ ```
320
+
321
+ ### `flag_quiz` — Multiple Valid Answers
322
+
323
+ Accepts any entry from the `answers` list. Great for command flags where `ls -a` and `-a` are both valid.
324
+
325
+ ```python
326
+ Challenge(
327
+ id="q3",
328
+ type="flag_quiz",
329
+ prompt="What flag shows hidden files with ls?",
330
+ answers=["-a", "--all", "ls -a", "ls --all"],
331
+ explanation="`-a` includes dotfiles. `ls -a` also accepted.",
332
+ hints=["Single letter flag.", "Think 'all'."],
333
+ xp=15,
334
+ )
335
+ ```
336
+
337
+ ### `live` — Real Command in Sandbox
338
+
339
+ The player runs an actual shell command. The engine creates a temp directory, runs the command in it, and validates the result.
340
+
341
+ ```python
342
+ Challenge(
343
+ id="q4",
344
+ type="live",
345
+ prompt="Create a directory named 'output' inside the sandbox.",
346
+ explanation="mkdir creates a new directory: `mkdir output`",
347
+ hints=["Use mkdir.", "mkdir output"],
348
+ xp=20,
349
+ setup={
350
+ "dirs": ["src"], # pre-created directories
351
+ "files": {
352
+ "src/hello.txt": "hello world\n", # pre-created files
353
+ },
354
+ },
355
+ validation={"type": "dir_exists", "target": "output"},
356
+ )
357
+ ```
358
+
359
+ **Available validators:**
360
+
361
+ | type | fields | passes when |
362
+ |------|--------|-------------|
363
+ | `dir_exists` | `target` | directory exists in sandbox |
364
+ | `file_exists` | `target` | file exists in sandbox |
365
+ | `file_missing` | `target` | file does NOT exist |
366
+ | `output_contains` | `expected` | stdout or stderr contains expected string |
367
+ | `file_contains` | `target`, `expected` | file content contains expected string |
368
+ | `file_executable` | `target` | file has execute bit set |
369
+ | `file_perms` | `target`, `expected_mode` | file permissions match (e.g. `"755"`) |
370
+ | `multi` | `checks: [...]` | all nested validators pass |
371
+
372
+ ### `ordered` — Sequence Ordering
373
+
374
+ The player types the correct order of steps as a space-separated number sequence.
375
+
376
+ ```python
377
+ Challenge(
378
+ id="q5",
379
+ type="ordered",
380
+ prompt="Put these git steps in order: commit, add, push, init",
381
+ items=["git commit -m 'msg'", "git add .", "git push", "git init"],
382
+ # correct order is: init(3), add(1), commit(0), push(2) — 0-based indices
383
+ answer=[3, 1, 0, 2],
384
+ explanation="init → add → commit → push is the correct git workflow.",
385
+ xp=20,
386
+ )
387
+ ```
388
+
389
+ ### `arrange` — Matching Pairs
390
+
391
+ The player matches left items to right items using letters.
392
+
393
+ ```python
394
+ Challenge(
395
+ id="q6",
396
+ type="arrange",
397
+ prompt="Match each command to its purpose.",
398
+ pairs=[
399
+ {"left": "ls", "right": "List files"},
400
+ {"left": "cd", "right": "Change directory"},
401
+ {"left": "pwd", "right": "Print working directory"},
402
+ ],
403
+ # right items may be shuffled for display; answer reflects the correct mapping
404
+ answer="A B C",
405
+ explanation="ls=List, cd=Change, pwd=Print.",
406
+ xp=20,
407
+ )
408
+ ```
409
+
410
+ ---
411
+
412
+ ## SkillPack — Full Field Reference
413
+
414
+ ```python
415
+ SkillPack(
416
+ # Required
417
+ id="my_pack", # unique string identifier
418
+ title="My Pack", # display title
419
+ zone_order=["zone_1", ...], # ordered list of zone IDs
420
+ zones={ # dict of zone_id → zone dict
421
+ "zone_1": {
422
+ "id": "zone_1",
423
+ "name": "Zone Display Name", # shown in UI
424
+ "description": "...", # shown in zone preview
425
+ "challenges": [...], # list of challenge dicts
426
+ },
427
+ },
428
+
429
+ # Narrative text
430
+ intro_story="...", # shown once at start
431
+ zone_intros={"zone_1": "..."}, # shown on first entry to each zone
432
+ zone_completions={"zone_1": "..."}, # shown on zone complete
433
+ boss_intros={"zone_1": "..."}, # shown before last challenge
434
+
435
+ # Save / display
436
+ save_file_name="my_pack", # filename (no spaces) for JSON save
437
+ subtitle="◈ Tagline ◈", # shown under banner
438
+ quit_message="See you later.", # shown on quit
439
+ default_player_name="Agent", # default name if player skips input
440
+
441
+ # Achievements
442
+ zone_achievement_map={"zone_1": "achievement_id"},
443
+ achievements={
444
+ "achievement_id": ("Short Title", "Longer description"),
445
+ },
446
+
447
+ # Progression
448
+ level_titles=[
449
+ (1, "Novice"), # (min_level, title)
450
+ (5, "Learner"),
451
+ (10, "Expert"),
452
+ ],
453
+
454
+ # Display
455
+ banner_ascii=r"...", # raw string ASCII art for banner
456
+ kids_mode=False, # True → kid-friendly praise phrases
457
+
458
+ # Optional advanced
459
+ recommended_age="8+", # informational only
460
+ )
461
+ ```
462
+
463
+ ---
464
+
465
+ ## Campaign — Chaining Packs Into a Story
466
+
467
+ A Campaign sequences SkillPacks into chapters with connecting narrative.
468
+
469
+ **`campaigns/my_campaign/__init__.py`**
470
+
471
+ ```python
472
+ from engine.campaign import Campaign, ChapterDef
473
+
474
+ CAMPAIGN = Campaign(
475
+ id="my_campaign",
476
+ title="The Full Journey",
477
+ save_file_name="my_campaign",
478
+ intro_story="The opening of your campaign...",
479
+ final_story="The ending, shown after the last chapter.",
480
+ quit_message="Your journey continues...",
481
+ entry_summary_prefix="Last time, you...",
482
+ campaign_achievements={
483
+ "campaign_complete": ("Journey's End", "Finished all chapters"),
484
+ },
485
+ chapters=[
486
+ ChapterDef(
487
+ pack_name="intro_pack", # must match SkillPack.id
488
+ title="Chapter 1: Beginnings",
489
+ entry_summary="You are just starting out.",
490
+ intro_bridge="Transition text shown before this chapter starts.",
491
+ outro_bridge="Text shown after this chapter ends, before the next begins.",
492
+ recommended_age="8+", # optional
493
+ ),
494
+ ChapterDef(
495
+ pack_name="advanced_pack",
496
+ title="Chapter 2: The Deep End",
497
+ entry_summary="Having mastered the basics, you push further.",
498
+ intro_bridge="You descend into the advanced section...",
499
+ outro_bridge="You have completed the journey.",
500
+ ),
501
+ ],
502
+ )
503
+ ```
504
+
505
+ **Entry point:**
506
+
507
+ ```python
508
+ from engine.main import run_campaign
509
+
510
+ def main():
511
+ run_campaign("my_campaign") # loads CAMPAIGN from campaigns/my_campaign/__init__.py
512
+ ```
513
+
514
+ ---
515
+
516
+ ## Zone Helper — `engine.zone.Zone`
517
+
518
+ `Zone` is a `dict` subclass. Use it instead of plain dicts for cleaner zone definitions.
519
+
520
+ ```python
521
+ from engine.zone import Zone
522
+
523
+ Zone(
524
+ id="zone_1",
525
+ title="Zone One", # maps to dict key "name" (what the engine reads)
526
+ description="...",
527
+ challenges=[...],
528
+ # Optional display fields:
529
+ color="cyan",
530
+ icon="⚡",
531
+ subtitle="Subheader",
532
+ )
533
+ ```
534
+
535
+ ---
536
+
537
+ ## Challenge Helper — `engine.challenge.Challenge`
538
+
539
+ `Challenge` is a `dict` subclass. Use it for cleaner challenge definitions.
540
+
541
+ ```python
542
+ from engine.challenge import Challenge
543
+
544
+ Challenge(
545
+ id="ch_1",
546
+ type="quiz", # quiz / fill_blank / flag_quiz / live / ordered / arrange
547
+ prompt="...", # maps to dict key "question" (what the engine reads)
548
+ explanation="...", # maps to dict key "lesson"
549
+ answer="b", # for quiz/fill_blank
550
+ answers=["-a", "--all"], # for flag_quiz (multiple valid answers)
551
+ options=["A", "B", "C", "D"],
552
+ hints=["Hint 1", "Hint 2"],
553
+ xp=10,
554
+ difficulty="medium", # easy / medium / hard / boss
555
+ # For live challenges:
556
+ setup={"dirs": [...], "files": {...}},
557
+ validation={"type": "output_contains", "expected": "hello"},
558
+ # For ordered challenges:
559
+ items=["step A", "step B", "step C"],
560
+ # For arrange challenges:
561
+ pairs=[{"left": "cmd", "right": "description"}],
562
+ )
563
+ ```
564
+
565
+ ---
566
+
567
+ ## Engine Features
568
+
569
+ | Feature | Description |
570
+ |---------|-------------|
571
+ | **XP & Levels** | Configurable `level_titles` per pack; XP scales with difficulty |
572
+ | **Star Ratings** | 1–3 stars per zone based on hints used and challenges skipped |
573
+ | **Achievements** | Auto-unlocked when zone is completed; displayed in stats |
574
+ | **Daily Challenge** | Deterministic pick per pack per day; 2× XP; streak tracking |
575
+ | **Difficulty Modes** | Easy (0.75× XP, free hints) / Normal / Hard (1.5× XP) |
576
+ | **Speed Records** | Per-challenge personal bests; new-record flash on screen |
577
+ | **Bookmarks** | Toggle with `[b]`; review from main menu |
578
+ | **Zone Preview** | Challenge list shown before entering zone |
579
+ | **Help Screen** | Full keybinding reference via `[?]` |
580
+ | **Completion Certificate** | ASCII grade art (S/A/B/C/D) on pack complete |
581
+ | **Campaign Stats** | Per-chapter star ratings, XP totals, overall grade |
582
+ | **Kids Mode** | `kids_mode=True` → kid-friendly praise pool and gentler UX |
583
+ | **Save/Resume** | JSON saves in `~/.quest_engine/<save_file_name>/` |
584
+ | **Placement Quiz** | Campaign can start with optional assessment to pick chapter |
585
+
586
+ ---
587
+
588
+ ## In-Game Controls
589
+
590
+ ```
591
+ [h] Hint Show next hint for current challenge
592
+ [b] Bookmark Toggle bookmark on current challenge
593
+ [d] Difficulty Switch difficulty (Easy / Normal / Hard)
594
+ [?] Help Full control reference
595
+ [s] Skip Skip current challenge (costs a star)
596
+ [q] Menu Return to main menu
597
+ ```
598
+
599
+ ---
600
+
601
+ ## Games Built on Quest Engine
602
+
603
+ | Game | Audience | Chapters | What You Learn |
604
+ |------|----------|----------|----------------|
605
+ | [NEXUS Quest](https://github.com/thorski1/nexus-quest) | Adults / Developers | 11 | Bash · SSH · Vim · Git · Docker · Postgres · Python · Regex · Linux · Kubernetes · AWS |
606
+ | [The Primer](https://github.com/thorski1/primer) | Children (5–12) | 9 | Letters · Numbers · Science · Kindness · Geography · Math · History · Art · Coding |
607
+
608
+ ---
609
+
610
+ ## Requirements
611
+
612
+ - Python 3.10+
613
+ - `rich >= 13.0.0`
614
+
615
+ ---
616
+
617
+ ## License
618
+
619
+ MIT