Version 4.0.0 released - 400% more flexible, powerful, and embeddable

I am exited to announce that version 4.0.0 is officially released. This version includes huge changes and a lot of work to make the Elm Narrative Engine better than ever.

What’s changed?

Everything! All changes are geared towards making the engine way more flexible, powerful, and embeddable.

See the full docs. You can also experiment with a live example.

More flexible

Release 3.0 divorced the engine from the view and content, making it possible to make very different looking games. But the engine still controlled the state, and very narrowly defined what your world model contained, and how you could interact with it. You were restricted to “characters”, “items”, and “locations” and nothing else.

This release fully abstracts the world model, so authors can create any kind of world model they want. Like an escape-room game that has characters and items, but no locations. Or maybe “memories” are a big part of your world, and you want to be able to interact with them as a first-class concept. Or maybe you want to differentiate between humans and robots, or heroes and villains, or track suspects or romantic interests. Or you want to be able to create systems that understand moods, or weather, or light levels. Now you can.

How? The engine now sees the world as a collection of “entities.” But the secret sauce, is that you can add properties like tags, stats, and relations to each entity. Think of it as having “adjectives” at your disposal, where before you only had nouns. The best part is that you define these adjectives yourself, and you can query against them however you need. The engine no longer imposes any restrictions.

Here’s an example of a few entities with various properties:

entity "CAVE"
  |> tag "location"
  |> tag "dark"

entity "GOBLIN"
  |> tag "character"
  |> tag "sleeping"
  |> stat "IQ" 60
  |> link "location" "CAVE"

entity "BURNING_TORCH"
  |> tag "item"
  |> stat "illumination" 7
  |> link "location" "CAVE_ENTRANCE"

entity "BAG_OF_GOLD"
  |> tag "item"
  |> link "location" "CAVE"
  |> link "guarded_by" "GOBLIN"

More powerful

Since the world model is richer in data thanks to these new properties, we now can write more complex queries against it. We can use these when rendering our view, as well as in our rules. We can make our queries more or less specific, giving us different results. For example, it’s easy to write a query that selects “any dark location,” or a rule for “approaching an enemy when our fear is high.” This allows us be more expressive, and also makes it easier to be more general.

Since matching rules are weighted based on how specific they are, the most fitting rule will be selected based on what the player interacted with, and the current state of the game. This is sometimes referred to as a “salience-based” system. Also, the inclusion of stats, which are numeric values, allows for “quality-based” (or “stat-based”) systems. These are very powerful, and can open new approaches to designing an interactive game. You can read more about both of these systems on Emily Short’s blog post, or Choice of Games’s article.

Finally, links can model fairly complex relationships. Not only can you define the type of relationship, but you can write generic queries against it. For example, you can test if the player is “located in any location that is dark,” or find all the characters “who the player knows, who in turn, know any clue about any suspects.” (The last example would be a chain of 3 links with 3 generic layers, just to give a complex example!)

Here is an example of some queries:

inventory = query [ HasTag "item", HasLink "location" (Match "PLAYER" []) ]

rule "entering dark places with a light source"
    { trigger = MatchAny [ HasTag "location", HasTag "dark" ]
    , conditions =
        [ MatchAny
            [ HasStat "illumination" GT 5
            , HasLink "location" (Match "PLAYER" [])
            ]
        ]
    , changes = [ Update "PLAYER" [ SetLink "location" "$" ] ]
    }

(In the last example, “$” is a special character that will be replaced with the ID of the entity that triggered the rule, since you don’t know it when you declare the rule. You can use this trick anywhere you have a “generic trigger.”)

More embeddable

The Elm Narrative Engine started out as a framework that did everything for you, with little room for customization. In later releases, it relinquished some control, making room for some customization. In this release, the engine has been fully gutted, turning it into a plug-in or “narrative component” you can add to a larger code base. This lets you build your game however you want, and use this engine to add a narrative to it, with minimal requirements.

This is made possible by minimizing the state it tracks, and by exposing generic functions to call from your own architectural framework. The mentioned changes in the world model help make this work.

Most importantly, the engine follows the Entity Component System (ECS) design pattern. This is what I mean when I say the engine is a “narrative component.” Both entities and rules are defined with extensible records, which work great with the ECS pattern. This makes it easy to add other data and behavior to the entities in your world model, and also helps tie together entity definitions that get imported from different editing tools.

type alias DescriptionComponent a =
    { a | name : String, description : String }


type alias MyEntity =
    NarrativeComponent (DescriptionComponent {})

-- you can pass a `MyEntity` through here
getDescription : DescriptionComponent a -> String
getDescription entity =
    entity.description

What’s next?

I’m very pleased with where everything has landed. I am building a more involved game currently, and have been testing these improvements out. I feel like the functionality covers everything I can think of for now.

The area that I would like to improve next is making the authoring experience simpler and more natural. Right now, it feels a little clunky. I have plans for an authoring syntax, which will get parsed into the current types. You can preview the syntax if you like, and feedback is welcomed. I am also experimenting with a visual authoring tool and some debugging tools to catch story logic errors. I also have some ideas on adding some tangential helpers for adding dynamic narrative content, similar to Twine or Ink syntax.