Bardic 0.9.0: Engine Refactor and De-Forking
I killed the engine fork for the browser bundling build. Finally. The engine has also been refactored from a god function to something more sane. Added more engine tests and a word counter for the linter.
New Features
One Engine, Two Environments
# Desktop (existing behavior, no changes)
engine = BardEngine(story_data)
# Browser
engine = BardEngine(story_data, environment="browser")Browser mode does two things: it excludes __import__ from safe builtins (modules must be pre-bundled, as Pyodide requires), and it automatically attaches BrowserStorageAdapter for localStorage save/load. Everything else (hooks, @join, @prev, undo/redo, all of it) works identically in both environments.
No more maintaining two codebases. No more "does this work in browser?" being a different question from "does this work?"
BrowserStorageAdapter: localStorage Save/Load
# Automatically attached in browser mode
engine.save("slot_1")
engine.load("slot_1")
saves = engine.list_saves() # ["slot_1", "slot_2"]
engine.delete_save("slot_1")The new bardic.runtime.browser module wraps the engine's StateManager with a clean localStorage interface. Save anywhere in the story. Load back in. It uses the same serialization as desktop saves, so if you ever bridge to a server-side persistence layer, the data shape is already right.
Word Count & Play Time in bardic lint
✓ story compiled successfully (12 passages, 3 @includes)
~8,420 words · ~52 min play time
No issues found.
The summary line now shows approximate word count and estimated play time, calculated from reading speed (~200 wpm) plus decision time (~10 seconds per choice). Both values are included in --json-output as words and play_time_minutes for dashboards, CI output, or just satisfying curiosity about whether a session is actually 20 minutes or secretly 45.
hasattr, getattr, map, filter in Safe Builtins
These four were missing from the story code builtins and have been added, unifying the desktop and browser builtin sets. If you've been working around their absence with @py: blocks, you can simplify.
Internal Architecture
The Monolith Is Dead
engine.py was 2,153 lines. It is now 771.
The rest has been decomposed into seven focused modules, each independently testable:
| Module | Lines | Responsibility |
|---|---|---|
engine.py |
771 | Facade: navigation, composition, event triggering |
renderer.py |
604 | Content rendering, expression evaluation, loops, conditionals, choice filtering |
executor.py |
427 | Command execution, variable assignment, Python blocks, imports, builtins |
state.py |
401 | Undo/redo stacks, save/load serialization, GameSnapshot |
directives.py |
241 | @render directive processing, argument binding, React output |
browser.py |
127 | localStorage adapter |
types.py |
95 | PassageOutput, GameSnapshot dataclasses |
hooks.py |
75 | HookManager for event hook registration |
The public API is unchanged. BardEngine is the same class with the same methods. This is purely internal. Existing stories, existing integrations, nothing breaks.
What changes: each module can be tested in isolation, read without context-switching, and modified without touching the others. This is what 0.9.0 makes possible for every release after it.
bardic bundle Ships Real Modules
Browser bundles now include bardic/runtime/*.py (all 8 modules) instead of the old monolithic engine_browser.py. The Pyodide init loads them into the virtual filesystem via standard Python imports. All engine features work automatically in browser builds because it's the same engine.
Undo/Redo/Load Uses In-Place Mutation
GameSnapshot.restore_to() and StateManager.load_state() now use clear() + update() on shared containers (state, used_choices, _join_section_index) instead of replacing them. This preserves references held by subsystems that were initialized pointing at the original containers. Previously, an undo or load could leave the executor and renderer looking at stale state. Fixed, quietly, permanently.
Tests
149 new tests across 6 new test files:
| File | Coverage |
|---|---|
test_hooks_manager.py |
HookManager registration, firing, cleanup |
test_state_manager.py |
Undo/redo stacks, save/load serialization |
test_directives.py |
@render processing, argument binding |
test_executor.py |
Command execution, Python blocks, builtins |
test_renderer.py |
Content rendering, conditionals, loops, choice filtering |
test_browser.py |
BrowserStorageAdapter, localStorage round-trips |
Total test count: 529.
Upgrade Notes
pip install --upgrade bardicNo story file changes needed. The public API is unchanged. BardEngine(story_data) works exactly as before.
If you're targeting browser deployment, switch to BardEngine(story_data, environment="browser") and rebuild your bundle with bardic bundle. The old engine_browser.py is gone — but if you were importing from it directly (you probably weren't), that's the only thing to update.