A dungeon engine
root
Antisplice
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
Rooms
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.
Stereos
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 theStereo
feature (and relationNear
,Carried
orWorn
) - 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 forChattyDungeonM ()
, just some monadic Haskell function that may have side effects. Instances: Functor, Applicative, MonadPrerequisite
: alias forChattyDungeonM Bool
, just some monadic Haskell function that represents a condition and may not have side effects. Instances: Functor, Applicative, MonadPredicate
: alias forChattyDungeonM (Maybe ReError)
, also represents a condition, but instead ofTrue
, it returnsNothing
for success, and aJust ReError
for failure. Instances: Functor, Applicative, MonadHandlerBox
,PrerequisiteBox
andPredicateBox
are boxed versions ofHandler
orPredicate
, to circumvent type system restrictions. Instances: Semigroup, Monoid, IsActionAction
: A combination of a handler and a predicate. The handler can only be run if the predicate succeeds. Instances: Semigroup, Monoid, IsActionIsAction
: A typeclass providing combinators for Actions, PredicateBoxes andPrerequisiteBoxes
corresponding to boolean operators.Invokable
: AHandler
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
: APredicate
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 aConsumer
, i.e. Skill, Recipe, HandlerBox, Condition, PredicateBox, ActionSkill
: 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.