Skip to content

Bardic 0.9.0: Engine Refactor and De-Forking

Choose a tag to compare

@katelouie katelouie released this 12 Mar 18:02
· 26 commits to main since this release

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 bardic

No 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.