Home

Awesome

Hinteractive: Interaction Fiction Game Engine

A game engine for creating text-based adventure games like Zork (under development).

Features:

Demo Game

The demo game showcases the engine's core capabilities.

stack build
stack exec hinteractive-exe

Architecture

The engine consists of two main components:

The aeson library is used for serializing the game state.

Lenses are used for easier data type manipulation.

In the future, another major component is planned—a language model. This model will parse natural language commands (English) and convert them into operations on the game state.

AdventureL Game Scenario Language

The AdventureL type is a Free monad type that describes available operations on the game state. Monadic scenarios are built from these operations and embedded into the game's locations (graph nodes).

An example scenario that prints "Hello, World" to the player, where printMessage is a monadic operation from the AdventureL language:

helloWorld :: AdventureL ()
helloWorld = printMessage "Hello, World"

This type is based on a Generalized ADT (AdventureLF), whose constructors encode the available operations. It is a GADT because operations on objects have more complex syntax and semantics (specifically, they need to be serializable).

-- | Free monadic game scenario language.
type AdventureL a = Free AdventureLF a

-- | Generalized ADT for the game mechanics language.
data AdventureLF next where
  -- | General game operations (input/output).
  GetUserInput :: (String -> next) -> AdventureLF next
  PrintMessage :: String  -> next  -> AdventureLF next

  -- | Inventory actions (stub: the Item type needs reworking).
  PutItem   :: Item -> next -> AdventureLF next
  DropItem  :: Item -> next -> AdventureLF next
  ListItems ::         next -> AdventureLF next

  -- | Actions on game object states.
  -- You can request an object's state and update it by its name.
  GetObjSt :: FromJSON a => ObjectName -> (a -> next) -> AdventureLF next
  PutObjSt :: ToJSON a   => ObjectName -> a -> next -> AdventureLF next

For convenience, the AdventureLF type is hidden, and "smart constructors" are provided in the AdventureL type:

-- Player interaction:

-- Get user input from the console.
getUserInput :: AdventureL String
-- Print a message to the console.
printMessage :: String -> AdventureL ()

-- Inventory actions (require rework):

-- Add an item to the inventory.
putItem :: String -> AdventureL ()
-- Drop an item.
dropItem :: AdventureL ()
-- List items in the inventory.
listItems :: AdventureL ()

There are also operations for interacting with generic game objects: getObject, getObject', and putObject. These operations have more complex signatures, so they will be described separately.

Gameplay involves transitioning between graph nodes and executing (interpreting) the scenarios in those nodes. When executed, the scenarios modify the game state in various ways.

The state of game objects is maintained at runtime using the State monad:

-- State of all game objects
type ObjectStates = Map.Map String BSL.ByteString

-- Player's inventory
type Inventory = Map.Map String Item

-- Runtime (game state)
data Runtime = Runtime
  { _inventory    :: Map.Map String Item
  , _objectStates :: ObjectStates
  }

-- Interpreter type for AdventureL: a monad stack of State and IO.
type Interpreter a = StateT Runtime IO a

The AdventureL language is pure monadic. Its interpreter is an impure monadic (working in the IO monad).

Game Object Mechanics

Game objects are objects with mutable state. Their state can change either due to player actions or based on predefined scenarios.

The state of a game object is described in a separate type in each game. This type is unknown to the engine. The only current requirement is that it must be serializable.

Example:

-- Serializable type - state of a mailbox (but not the object itself)
data MailboxSt = MailboxSt
  { _description :: String      -- ^ Text description of the mailbox
  , _container   :: Container   -- ^ "Container", which the mailbox is
  }
  deriving (Generic, ToJSON, FromJSON)

Here, MailboxSt is the state of a mailbox. The type classes Generic, ToJSON, and FromJSON make it serializable.

Since the mailbox is a container, it has a _container field of the special Container type. Containers can be open or closed and can hold other objects:

data ContainerState = Opened | Closed
  deriving (Generic, ToJSON, FromJSON)

-- Type describing a container.
-- Currently contains a stub for stored objects - the `Item` type.
data Container = Container
  { _state :: ContainerState
  , _items :: [Item]
  }
  deriving (Generic, ToJSON, FromJSON)

Example:

mailboxObj = MailboxSt
  { _description = "This is a small mailbox."
  , _container = Container Closed ["leaflet"]
  }

The type MailboxSt described above is just the state of the object, not the object itself. The current approach requires that for each such type, a "representative" ADT is specified, which allows for more convenient work with objects of that type and generic operations.

data MailboxType = MailboxType

You also need to describe the operations that can be performed on instances of this object. The operations (or Actions) will be defined later. For example, a mailbox can be opened and closed because it is a container. Types for object operations will be described later.

The final generic game object will be described by a complex Object type, with a phantom parameter passing the "representative" type as objType:

-- | Generic game object.
data Object objType objSt = Object
  { _name    :: ObjectName      -- ^ Name of the object (essentially, a key in the map of all game objects).
  , _state   :: objSt           -- ^ Object state.
  , _actions :: Actions objSt   -- ^ Available actions on the object.
  }

For the mailbox, Object will be parameterized by the following types:

objType :: MailboxType objSt :: MailboxSt

Since the Actions type is currently unserializable (it consists of functions and scenarios in the AdventureL monad), to bind an object's state to actions on it, you can't just retrieve objSt from the game state; you also need to know which actions are tied to this type. This is done using the ToObject type class and the "representative" objType:

class FromJSON objSt => ToObject objType objSt | objType -> objSt, objSt -> objType where
  object :: objSt -> Object objType objSt

An example for the mailbox:

instance ToObject MailboxType MailboxSt where
  object objSt = Object "mailbox" objSt $ Map.fromList
    [ ("open",  Action openContainer  onMailboxOpenSuccess  onMailboxOpenFail  )
    , ("close", Action closeContainer onOpenCloseSuccess onMailboxCloseFail )
    ]

Here, the list contains actions executed on specific commands ("open", "close"). The actions consist of a main operation (openContainer, closeContainer) and success or failure handlers (onMailboxOpenSuccess, onMailboxOpenFail).

Finally, to work with generic objects in scenarios, there are getObject, getObject', and putObject functions, which retrieve an object's state from the game state, deserialize it, and allow you to work with it in a scenario; or serialize the object's state back into the game state. These functions use the ToObject type class and the representative objType to deserialize the object to the correct objSt type:

getObject
  :: (FromJSON objSt, ToObject objType objSt)
  => ObjectName  -- ^ Object name as a key
  ->

 AdventureL (Object objType objSt)
getObject name = do
  objSt <- liftF $ GetObjSt name id
  pure $ object objSt

getObject'
  :: (FromJSON objSt, ToObject objType objSt)
  => objType     -- ^ "Representative" type
  -> ObjectName  -- ^ Object name as a key
  -> AdventureL (Object objType objSt)
getObject' _ = getObject

putObject
  :: ToJSON objSt
  => ObjectName   -- ^ Object name as a key
  -> objSt        -- ^ Object state
  -> AdventureL ()
putObject name objSt = liftF $ PutObjSt name objSt ()

An example scenario that retrieves a mailbox and tells the player what’s inside:

describeMailbox :: AdventureL ()
describeMailbox = do
  mailbox :: Mailbox <- getObject "mailbox"
  when showMailbox $ printMessage $ describeObject mailbox  -- A special operation that prints the object's _description.

In the future, it is planned to replace the representative objType with string type literals.

Actions

TODO

Creating the Game (Locations, Transitions)

TODO