Project 04 / Procedural narrative
Shadow Index.
A terminal roguelike where narrative prerequisites and persistent story flags connect one encounter to the next.
Procedural doesn’t have to mean disconnected
Shadow Index is a C++17 terminal roguelike built around narrative events and player state. Its central problem is simple: how can encounters vary between runs while still responding to earlier choices?
The event system uses prerequisites and state-changing flags. An encounter can introduce an object, a choice can record that the player kept it, and a later event can require that state before it is allowed into the selection pool.
A real choice, traced through the data
In device_found, picking up the box adds has_device. Leaving it adds ignored_device. The follow-up, device_activation, declares REQ has_device.
EVENT device_found
TITLE A Strange Device
# Choice excerpt
CHOICE Pick it up (+1 Curiosity)
ADDFLAG has_device
EFFECT clarity 1
EVENT device_activation
REQ has_device
TITLE The Device AwakensExcerpts from core_events.txt; the comment is an annotation. The choice’s display label says “Curiosity”; its implemented effect updates Clarity.
This dependency makes the follow-up eligible after that choice. It does not force it to be the next event: the random selection still operates over all currently eligible encounters.
Filter first. Then sample.
content
flags present?
only
encounter
EventLibrary::GetRandomEvent scans the loaded events, checks each prerequisite against GameState, and collects valid candidates. The RNG selects an index from that vector. If the vector is empty, the function returns no event.
IDs are indexed through an unordered_map for direct lookups. Random selection still scans the event collection; it is not a constant-time graph traversal. The current implementation also does not track a visited-event set.
The flag dependencies express a branching narrative. The implementation does not perform general DAG validation or guarantee non-repeating encounters.
Let the terminal expose the state
The renderer builds its own ASCII borders and text wrapping. It receives a ViewModel containing room text, options, stats, inventory information, and recent log entries, keeping presentation separate from game state.
+----------------------------------+
| SHADOW INDEX |
+----------------------------------+
| 1. New Run |
| 2. Load Run |
| 3. Quit |
+----------------------------------+
> Condensed reconstruction of the menu in Renderer.cpp; line width is adapted for this page.
The game’s stat fields are Resolve, Dread, Clarity, Pride, and Compassion. Narrative choices apply effects to those fields and may add flags or equipment rewards.
What the current system does—and leaves open
External event files let narrative content change without recompiling. The seeded generator makes new runs reproducible from an initial seed under the same sequence of actions.
The current save format stores the seed, depth, stats, inventory, and story flags. On load, the RNG is re-seeded; its full internal state is not serialized. That means loading a run does not guarantee an identical continuation of the random sequence.
A visited-event policy, graph validation, and full RNG-state persistence would each strengthen different guarantees. Those are possible extensions, not features claimed by this implementation.
Inspect the event engine
Build with a C++17 compiler and Make, then run from the repository root so the event data can be found:
make
./shadow_index-
data/events/core_events.txt ↗
Actual narrative nodes, choices, flags, and effects. -
src/EventLibrary.cpp ↗
Prerequisite filtering and random event selection. -
src/ContentLoader.cpp ↗
The external event format parser. -
src/Game.cpp ↗
Applying choices and assembling the view model. -
src/GameState.cpp ↗
Save/load behavior and RNG re-seeding. -
src/Renderer.cpp ↗
ASCII borders, wrapping, and terminal presentation.
Next project