Bardic v0.7.1: Lots of Critical Fixes and Story Tracking
New Features
Passage Visit Counting: _visits
The _visits dict is now a first-class engine built-in. Every time a passage is entered, its count increments automatically. No setup, no boilerplate.
:: Tavern
@if _visits.get("Tavern", 0) == 0:
The smell of woodsmoke and something sour hits you first.
@else:
The regulars barely glance up. You're starting to become a fixture.
@endif
+ [Sit at the bar] -> TavernBar
+ [Head upstairs] -> TavernRoom
Or inline:
You've {_visits.get("Tavern", 0) > 1 ? been here before, and it shows | never set foot in a place like this}.
_visits is automatically included in undo/redo snapshots and save/load. Zero footprint, full persistence.
Turn Counter: _turns
_turns counts player choices (incremented by choose(), not goto()). This is the number of decisions made, which turns out to be exactly what you want for pacing, urgency, and scoring.
@if _turns >= 20:
You've been at this a long time. The candles are burning low.
@endif
+ {_turns < 5} [Take your time exploring] -> FreeExplore
+ {_turns >= 10} [The situation is getting urgent] -> UrgentPath
Use it for:
- Pacing gates: unlock content after N choices
- Urgency mechanics: tick-tock tension without a timer
- Scoring: shorter paths can reward efficiency
- "You've been here a while" flavor text
Both _visits and _turns are covered by 17 new tests.
Quest Tracking: bardic.stdlib.quest
A full quest system lands in stdlib. QuestJournal manages a collection of Quest objects, each with custom stages, completion/failure states, and narrative journal entries you can surface directly in prose.
from bardic.stdlib.quest import QuestJournal, Quest
journal = QuestJournal()
# Create a quest with named stages
q = Quest("find_the_key", "The Missing Key")
q.add_stage("search_room")
q.add_stage("check_drawer")
q.add_stage("find_key", is_final=True)
journal.add_quest(q)@py:
journal.advance("find_the_key", "check_drawer")
journal.add_journal_entry("find_the_key", "The drawer was empty, but the dust was disturbed.")
@endpy
@if journal.get_quest("find_the_key").current_stage == "check_drawer":
You crouch by the desk. Something's been moved recently.
@endif
Filtered views are clean and idiomatic:
for quest in journal.active_quests:
print(f"→ {quest.title}: {quest.current_stage}")Full to_dict()/from_dict() serialization. If it can't save, it doesn't exist.
32 new tests.
Full Stdlib Test Suite — 114 Tests
All five stdlib modules now have proper test coverage: dice, inventory, economy, relationship, and quest. Previously: zero stdlib coverage. That era is over.
Unknown Directive Detection with "Did You Mean?"
Lines starting with @ that don't match any known directive now raise a SyntaxError — loudly and helpfully — instead of silently becoming content text that confuses everyone for twenty minutes.
SyntaxError: Unknown directive '@elseif' on line 42
Did you mean: @elif
SyntaxError: Unknown directive '@python' on line 7
Did you mean: @py:
Common typos (@elseif, @iff, @python) get specific suggestions. Completely unknown directives list all valid options. 5 new error handling tests.
Breaking Change
Python 3.10+ Required
The codebase uses X | Y union syntax (3.10+) and lowercase generic types (3.9+). pyproject.toml, CLAUDE.md, and all tutorials now reflect the real minimum. If you were running on 3.9, this is the nudge.
Bug Fixes
Callables Corrupted on Save/Load (Critical)
Functions and other callables in engine state (create_session, get_artifact, random, anything you passed in) were falling through to __dict__-based serialization. They'd get saved as dicts. On load, calling them would throw TypeError: 'dict' object is not callable.
This would have fired mid-Arcanum-session at the worst moment. Fixed: callables are now skipped during serialization alongside classes and types.
One-Time Choice IDs Were Unstable
One-time choices (*) were tracking seen choices by rendered string while filtering by raw content arrays, so they'd reappear on revisit. Now tracking and filtering use the same representation. Your * choices are once-and-done, as they should be.
Circular Include Detection Was Broken
resolve_includes passed seen.copy() to recursive calls, giving each branch its own independent set. A→B→A cycles weren't detected. Fixed: all branches now share the same seen set.
Relationship.name Never Actually Assigned
The __init__ had self.name: str (a type annotation, not an assignment). AttributeError on any name access. Fixed: self.name = name.
Loop Exception Handler Returned Wrong Tuple
_render_loop error path returned (error_msg, None) instead of (error_msg, None, []). Caused unpacking errors on any loop exception. Fixed.
Tutorial & Docs Fixes
- Tutorial 03_5: 6 wrong
bardic.modules.*imports →bardic.stdlib.* - Tutorial 04:
GameUIrewritten to use actualBardCompiler/BardEngineAPI (compile_string,goto,submit_inputs,choose) - Tutorial 00b: Test example rewritten to use
BardCompiler.compile_string()+BardEngine(story_data)— no more nonexistent no-arg constructor - Tutorial 04: Removed standalone
@startthat was being parsed as content text - Cookbook
custom-classes.md:<<if>>/<<for>>→@if:/@for: - Stdlib docstrings:
inventory.pyandeconomy.pyreferencedbardic.modules.*instead ofbardic.stdlib.* - Coffee shop example:
alex.is_ready_for_deep_work→alex.is_ready_for_deep_conversation
By the Numbers
| Count | |
|---|---|
| New tests (total) | 168 |
| Stdlib test coverage | 114 tests across 5 modules |
_visits + _turns tests |
17 |
| Quest system tests | 32 |
| Error handling tests | 5 |
| Tutorial/doc files fixed | 6 |
| Critical runtime bugs fixed | 4 |
Upgrade Notes
pip install --upgrade bardicIf you're on Python 3.9, upgrade to 3.10+ first.
The callable serialization fix is transparent. No story changes needed, but your save files from 0.7.0 should be considered suspect if they were saved after any callable-heavy engine state. Fresh saves from 0.7.1 onward are clean.