← All selected work

Project 04 / Procedural narrative

Shadow Index.

A terminal roguelike where narrative prerequisites and persistent story flags connect one encounter to the next.

C++17 Narrative state Text serialization Make
Explore the repository

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

FIG. 05 / A choice becomes state Shadow Index
EVENT device_found A Strange Device
Pick it up + has_device
REQ has_device The Device Awakens
Eligible for selection
Leave it alone + ignored_device
has_device missing The Device Awakens
Excluded from selection
A real event chain from core_events.txt. Flags determine which events may appear; the RNG chooses from the eligible set.

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 Awakens

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

Event library external text
content
Check REQ all required
flags present?
Candidate set eligible events
only
RNG choice select one
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

Next project

Meridian