A dungeon engine



Antisplice is an engine for text-based dungeons. The world is modeled in rooms and characters can move through it using text commands. In theory, both single-user and multi-user dungeons are supported, but to my knowledge, no multi-user dungeons are using it.

For inspiration and examples, it might be a good idea to have a look at Ironforge.

Game design


The world is organized in rooms. Rooms are connected by paths, which can be uni- or bidirectional. In total, they form a graph. Paths that are associated with cardinal directions (or "up" or "down") are called "exits", but not all paths are exits. Other paths can be taken by entering an object, or casting a spell. Exits can be guarded by gates, i.e. they open as soon as a requirement is fulfilled.


A stereo is a set of traits that may be attached to a player, object or room. It may:

  • arbitrary modify player stats (examples: an equipment piece could add a constant number of points to your agility, or there might be a sanctuary room where everyone's strength is 0)

  • add extra skills (examples: only some players may oink, in some rooms you can purchase something, and you can only pray if you wear)

  • add extra recipes and construction methods (examples: not everyone knows how to bake pancakes)

Stereos may be attached to:

  • players, using addStereo
  • objects, using addFeature with the Stereo feature (and relation Near, Carried or Worn)
  • rooms, by adding an invisible object with a Stereo Near feature

Skills and recipes

Dungeon construction

Dungeon monads

Input masks

Input masks are heterogenous lists that describe the syntax of commands, skills, recipes, etc, in a type-safe way.

The following reads as "whenever the command line only consists of the verb 'quit', throw QuitError":

Verb "quit" :-: Nil #->> throwError QuitError

On the left of the #->> operator, there is a mask describing that the input line only consists of the verb "quit". On the right, there is a Handler. Together, they form a Consumer.

Input masks can also pass parameters to the handler, for example by matching against available objects.

Verb "enter" :-: AvailableObject :-: Nil #-> \o -> objectTriggerOnEnterOf o

Whenever you pass an argument from the mask to the handler, you use the #-> operator. #->> is like #->, in cases where there is no argument to pass. The "quit" example could have also been written like this:

Verb "quit" :-: Nil #-> \() -> throwError QuitError

In a similar style as input masks, there are also preprocessing masks, that may modify your parsed arguments before passing them to the handler.

Verb "drop" :-: CarriedObject :-: Nil #- objectIdOf :-: Nil &-> dropObject

In the above case, CarriedObject matches against an object from the player's inventory and returns the ObjectState. But because dropObject only expects an id, we use objectIdOf as a postprocessing mask. In this case, it is equivalent to the following:

Verb "drop" :-: CarriedObject :-: Nil #-> dropObject . objectIdOf

In case we already use post-processing masks, we can also add a predicate mask. The following example makes sure you can only acquire objects that are actually acquirable.

Verb "acquire" :-: SeenObject :-: Nil #- objectIdOf :-: Nil &-> acquireObject +? Acquirable :-: Nil

Finally, there are also combined post-processing and predicate masks, built on Either:

Verb "go" :-: CatchFixe :-: Nil
              #- (\case
                    "north" -> Right North
                    "south" -> Right South
                    "east" -> Right East
                    "west" -> Right West
                    "northeast" -> Right NorthEast
                    "northwest" -> Right NorthWest
                    "southeast" -> Right SouthEast
                    "southwest" -> Right SouthWest
                    "up" -> Right Up
                    "down" -> Right Down
                    s -> Left $ Unint 0 ("\""++s++"\" is not a direction.")) :-: Nil &?-> changeRoom

Here's an overview of those weird arrow operators:

input_mask #-> \args -> handler
input_mask #->> handler
input_mask #- postproc_mask &-> \args -> handler
input_mask #- postproc_mask &-> (\args -> handler) +? predicate_mask
input_mask #- combi_mask &?-> \args -> handler

Types of invokables

There are quite many types in this library that represent something invokable. This can be confusing, but in fact most of them are only used internally, and not meant to be used directly. There are also typeclasses like IsConsumer and Extensible that automatically convert the types in the right way.

You should know Handler, Prerequisite, Action and Consumer though.

  • Handler: alias for ChattyDungeonM (), just some monadic Haskell function that may have side effects. Instances: Functor, Applicative, Monad

  • Prerequisite: alias for ChattyDungeonM Bool, just some monadic Haskell function that represents a condition and may not have side effects. Instances: Functor, Applicative, Monad

  • Predicate: alias for ChattyDungeonM (Maybe ReError), also represents a condition, but instead of True, it returns Nothing for success, and a Just ReError for failure. Instances: Functor, Applicative, Monad

  • HandlerBox, PrerequisiteBox and PredicateBox are boxed versions of Handler or Predicate, to circumvent type system restrictions. Instances: Semigroup, Monoid, IsAction

  • Action: A combination of a handler and a predicate. The handler can only be run if the predicate succeeds. Instances: Semigroup, Monoid, IsAction

  • IsAction: A typeclass providing combinators for Actions, PredicateBoxes and PrerequisiteBoxes corresponding to boolean operators.

  • Invokable: A Handler with a string list as parameters. The string list contains the tokens from command line. It does not make sense to create Invokables manually, consider using masks.

  • InvokableP: A Predicate with a string list as parameters. Checks whether the string list is in a correct syntax and applicable to an associated Invokable etc. This process is normally hidden behind the masks.

  • Condition: A boxed InvokableP. Also mostly hidden behind the masks.

  • Consumer: A combination of an Invokable and a Condition. This type is very frequent, but never created manually; once again: use masks or toConsumer.

  • ToConsumer: Typeclass for anything that can be converted to a Consumer, i.e. Skill, Recipe, HandlerBox, Condition, PredicateBox, Action

  • Skill: A consumer with a name. Skills can be attached to stereos, and directly called from the user command line.

  • Recipe: A consumer with a name and a recipe method. Methods can be something like cooking, baking, carpentering, building, drawing, etc, just ways of creating new objects. For instance, Ironforge has two recipe methods, cooking and building.