jschomay/elm-narrative-engine/3.0.1

NOTE: the latest version is 6.0.3

Engine

The story engine handles storing and advancing the state of the "world model" by running through your story rules on each interaction and updating the world model appropriately. It is designed to be embedded in your own Elm app, allowing for maximum flexibility and customization.

You can base your app on the interactive story starter repo.

type Model

The opaque type that holds all of the "world model" state, such as where each item and character is, what the current location and scene are, etc.

Embedding the engine

{ items : List String
, locations : List String
, characters : List String
}
-> Rules
-> Model

Initialize the Model for use when embedding in your own app. Provide your "manifest" (a list of ids) of all of the items, characters, and locations in your story, and the rules that govern the story.

You will most likely want to call changeWorld immediately after init to setup your initial story state (the current scene, location, and any initial placements of items or characters and known locations).

changeWorld : List ChangeWorldCommand -> Model -> Model

A way to change the story world directly, rather than responding to a player's interaction.

For example, you could change the current location in the story based on browser geolocation events, or respond to a network event, etc. This is also used to set up any initial story state.

If you are simply responding to a player's interaction, use update instead.

-> List { a | conditions : List Condition }
-> Maybe { a | conditions : List Condition }

Given a list of choices, this will return only the choice that matches the associated conditions, if any. Useful for conditional descriptions, for example, where an item has a different description depending on where it is.

This uses the same weighting scale as the rules (below), and likewise will return the first match if there is a tie, so specify your conditions carefully.

Accessors

The engine exposes many accessor functions for each part of the story world. Note that each of these only return ids. It is up to the client to map an id to the appropriate associated display content. It is useful to follow an "Entity Component System" pattern in the client for this purpose (see the story starter).

This gets the current scene to display

Get the current location to display

Get a list of the items in the current location to display

Get a list of the characters in the current location to display

getItemsInInventory : Model -> List String

Get a list of the items in your inventory to display

getLocations : Model -> List String

Get a list of the known locations to display

getEnding : Model -> Maybe String

Get the story ending, if it has ended. (Set with EndStory)

Story rules

Rules are how you progress the story. They are made up of conditions to match against and commands to perform if the rule matches. On each call of update, the engine will run through all of the rules to find the best match. If no rules match, the framework will perform a default command, which is usually just to narrate the description of what was interacted with, or to move you to that location or take that item. If multiple rules match the current state of the world, the "best" choice will be selected based on the following weighting criteria:

  1. a currentSceneIs condition has the highest weight
  2. with has more weight than the broader withAny* matchers
  3. each additional condition adds more weight

In the case of a tie, the first candidate will be chosen, which is something you want to avoid, so design your rules carefully

Anatomy of a rule

  1. A matcher against what interactable story element id the user clicked on

  2. A list of conditions that all must match for the rule to match

  3. A list of changes to make if the rule matches

     rules =
        [ { interaction = with "River"
          , conditions =
               [ currentLocationIs "Cottage"
               , itemIsInInventory "Cape"
               , itemIsInInventory "Basket of food"
               ]
          , changes =
               [ moveTo "River"
               , moveCharacterToLocation "Little Red Riding Hood" "River"
               ]
          }
       -- etc
       ]
type alias Rule =
Rule

A declarative rule, describing how to advance your story and under what conditions.

type alias Rules =
Rules

All the rules in your story.

update : String -> Model -> ( Model, Maybe String )

This is how you progress the story. Call it with the id of what ever was just "interacted" with. This will apply the best matching rule for the current context, or if it does not find a matching rule, it will perform some sensible default changes such as adding an item to inventory or moving to a location if no rules match. It also adds the interaction to the history (which is used for hasPreviouslyInteractedWith, save/load, and undo).

This will also return the id of the matching rule (if there was one). Normally the client would look up some associated narrative by this id to display, though it could respond in any other way as well.

Interaction matchers

The following interaction matchers can be used in the interaction part of the rule record.

type alias InteractionMatcher =
InteractionMatcher
with : String -> InteractionMatcher

Will only match the interaction part of a story rule if the player interacted with the specified entity id.

withAnything : InteractionMatcher

Will match the interaction part of a story rule every time (be careful about the conditions of your rules since this matcher is so broad).

withAnyItem : InteractionMatcher

Will match the interaction part of a story rule if the player interacted with any item (be careful about the conditions of your rules since this matcher is so broad).

withAnyLocation : InteractionMatcher

Will match the interaction part of a story rule if the player interacted with any location (be careful about the conditions of your rules since this matcher is so broad).

withAnyCharacter : InteractionMatcher

Will match the interaction part of a story rule if the player interacted with any character (be careful about the conditions of your rules since this matcher is so broad).

Conditions

The following condition matchers can be used in the conditions part of the rule record.

type alias Condition =
Condition
itemIsInInventory : String -> Condition

Will only match if the supplied item is in the inventory.

characterIsInLocation : String -> String -> Condition

Will only match if the supplied character is in the supplied location.

The first String is a character id, the second is a location id.

itemIsInLocation : String -> String -> Condition

Will only match if the supplied item is in the supplied location.

The first String is a item id, the second is a location id.

currentLocationIs : String -> Condition

Will only match when the supplied location is the current location.

itemIsNotInInventory : String -> Condition

Will only match if the supplied item is not in the inventory.

hasPreviouslyInteractedWith : String -> Condition

Will only match if the supplied interactable has already been interacted with.

hasNotPreviouslyInteractedWith : String -> Condition

Will only match if the supplied interactable has not already been interacted with.

currentSceneIs : String -> Condition

Will only match when the supplied location is not the current location.

characterIsNotInLocation : String -> String -> Condition

Will only match if the supplied character is not in the supplied location.

The first String is a character id, the second is a location id.

itemIsNotInLocation : String -> String -> Condition

Will only match if the supplied item is not in the supplied location.

The first String is a item id, the second is a location id.

currentLocationIsNot : String -> Condition

Will only match when the supplied location is not the current location.

Changing the story world

You cannot change the story directly, but you can supply "commands" describing how the story state should change.

type alias ChangeWorldCommand =
ChangeWorldCommand
moveTo : String -> ChangeWorldCommand

Changes the current location.

addLocation : String -> ChangeWorldCommand

Adds a location to your list of known locations. Any location on this list is available for the player to click on at any time. This avoids clunky spatial navigation mechanics, but does mean that you will need to make rules to prevent against going to locations that are inaccessible (with appropriate narration).

removeLocation : String -> ChangeWorldCommand

Removes a location from your list of known locations. You probably don't need this since once you know about a location you would always know about it, and trying to go to a location that is inaccessible for some reason could just give some narration telling why. But maybe you will find a good reason to use it.

moveItemToInventory : String -> ChangeWorldCommand

Adds an item to your inventory (if it was previously in a location, it will be removed from there, as items can only be in one place at once). If the item is "fixed" this will not move it (if you want to "unfix" an item, use moveItemOffScreen or MoveItemToLocation first).

moveCharacterToLocation : String -> String -> ChangeWorldCommand

Adds a character to a location, or moves a character to a different location (characters can only be in one location at a time, or off-screen). (Use moveTo to move yourself between locations.)

The first String is a character id, the second is a location id.

moveCharacterOffScreen : String -> ChangeWorldCommand

Moves a character "off-screen". The character will not show up in any locations until you use moveCharacterToLocation again.

moveItemToLocation : String -> String -> ChangeWorldCommand

Move an item to a location. If it was in another location or your inventory before, it will remove it from there, as items can only be in one place at once.

The first String is an item id, the second is a location id.

moveItemToLocationFixed : String -> String -> ChangeWorldCommand

Move an item to a location and set it as "fixed." Fixed items are like scenery, they can be interacted with, but they cannot be added to inventory.

If it was in another location or your inventory before, it will remove it from there, as items can only be in one place at once.

The first String is an item id, the second is a location id.

moveItemOffScreen : String -> ChangeWorldCommand

Moves an item "off-screen" (either from a location or the inventory). The item will not show up in any locations or inventory until you use placeItem or addInventory again.

loadScene : String -> ChangeWorldCommand

Scenes are a way to further constrain rules. You could have a scene for each leg of your story to make sure only the rules for that scene will apply. Or you may start a scene at a turning point in your story to "activate" special rules that apply to that scene. This is how you start or switch to a new scene. Note that you can only have one scene active at at time.

endStory : String -> ChangeWorldCommand

Sets a flag that the story has ended. The string you provide can be used to signify the "type" of story ending ("good", "bad", "heroic", etc), or how many moves it took to complete, or anything else you like. This has no effect on the framework, but you can use it in your client code how ever you like (change the view, calculate a score, etc).