DevTalk.net

ActiveMesa's software development blog. MathSharp, R2P, X2C and more!

Building a conversation engine with WebSharper

with 2 comments

Anyone who has ever played a role-playing game (yes, I admit: I did play CRPGs once) is familiar with scripted dialogues. You enter a dialogue with someone, they say something, you get a list of choices, and the conversation goes on until it ends, with your actions determining what happens next. Now, what if you could implement the same kind of interaction pattern using JavaScript?

Well, admittedly, writing the pure JavaScript by hand is probably less than pleasant. However, seeing how I dabble in WebSharper from time to time, I figured this would be a good opportunity to try something out. In this case, I decided to build a conversation system that would let us define the conversation graph1 and the site user can then iteract with a site in the form of a scripted dialogue.

Click here to see the demo

Defining Conversations

A conversation is a sequence of speeches (can’t think of a better word) that people deliver in turn. Each conversation has the text that someone says, but it also involves a couple of other things. Take a look at the definition of a conversation item:

type ConversationItem = {
  mutable Enabled : bool
  Id : int
  PartyId : int
  Speech : string
  EnableList : int list
  DisableList : int list
  }

Each item has an Id used to uniquely identify it among others. It also has an Enabled field used to indicate whether this conversation item is relevant at the current point in time. Then there’s the PartyId indicating who the conversation item belongs to (in our case – either the Client or the Server).

And then there are the two lists – EnableList and DisableList. These hold list of Ids of items that get enabled or disabled when this conversation item is ‘fired’ (said). Since people don’t typically repeat themselves, the item is itself disabled right after it is used2.

With this model in mind, let’s try building the conversation engine.

Conversation Engine

The conversation engine consists of the conversation graph – a graph of all possible conversation items and the relationships between them – and a number of utility functions for getting the client and server responses or ‘speeches’.

Let’s look at the graph itself. The graph is simply a list of all possible conversation items:

[<JavaScript>]
let private ConversationGraph = [
    {server with Id = 1; Enabled = true; Speech = "Welcome to a demo of Conversations. Please select one of the options below"}
    {client with Id = 2; Enabled = true; Speech = "Tell me what this is all about."; EnableList=[6]; DisableList = [1]}
    {client with Id = 3; Enabled = true; Speech = "Tell me about the technology used to implement this"; EnableList=[7]; DisableList=[1]}
    {client with Id = 4; Enabled = true; Speech = "Actually... I'm not that interested"; EnableList = [5]; DisableList = [1;2;3]}
    {server with Id = 5; Speech = "Well, okay, fair enough"; DisableList = [2;3;4]}
    {server with Id = 6; Speech = "Well, essentially, it's all about using F# to generate awesome JavaScript"}
    {server with Id = 7; Speech = "This demo uses pure HTML and JavaScript courtesy of WebSharper and F#"}
  ]

The graph is built upon two prototypes called client and server. These simply set the correct PartyId fields and also a number of useful defaults.

[<JavaScript>]
let private ServerId = 1
[<JavaScript>]
let private ClientId = 2;
 
[<JavaScript>]
let private server = { Enabled = false; Id = ServerId; Speech = ""; PartyId = 1; EnableList = []; DisableList = [] }
  
[<JavaScript>]
let private client = { server with PartyId = ClientId; }

With the conversation graph fully defined, we can write methods to provide the server and client items relevant at a particular point in the conversation:

[<JavaScript>]
let ServerSpeech() =
  ConversationGraph 
  |> List.find (fun f -> f.PartyId = ServerId && f.Enabled)
 
[<JavaScript>]
let ClientSpeech() =
  ConversationGraph
  |> List.filter (fun f -> f.PartyId = ClientId && f.Enabled)

Last but not least, we need a method for when a speech item gets fired. This is where we disable the item just fired, and also apply its enable/disable lists to other items.

[<JavaScript>]
let SpeechItemFired id =
  let item = ConversationGraph |> List.find(fun f -> f.Id = id)
  item.Enabled <- false // assume all fired items are not re-used
  // enable and disable all items with this id
  ConversationGraph
  |> List.iter(fun f -> 
    // if contains in the enable list, enable it
    item.EnableList  |> List.iter(fun en -> if en = f.Id then f.Enabled <- true)
    item.DisableList |> List.iter(fun de -> if de = f.Id then f.Enabled <- false)
    )

And that concludes the ConversationEngine module in all its simplistic beauty. We now have a pure JS-driven conversation definition, so it’s time to make use of it on the page.

Presenting Conversations

Let’s start with a simple thing – the conversation layout. For brevity’s sake, we’ll have two Divs one on top of another.

[<JavaScript>]
let Conversation() =
  Div [Attr.Style "width: 700px; height: 450px; border: 1px solid gray"] -< [
    Div [Attr.Style "height: 300px"] -< [
      P [Id "serverSpeech"; ConversationEngine.ServerSpeech().Speech |> Text]
    ]
    Div [Attr.Style "height: 150px"] -< [
      WiredClientSpeech()
    ]
  ]

Notice how we’re using ServerSpeech() directly, but the client-side part refers to something called WiredClientSpeech(). This is necessary because server speech is simply paragraph text, but client speech is a bunch of links with OnClick() event handlers attached to them.

Here comes a major F# dilemma: links update conversations, but a conversation update forces the replacement of these links (and whatever is holding them). How can we handle this? Well, in F# there’s no other way but using mutually recursive functions, which sounds scary, but is actually fairly simple. So, we have a function called WiredClientSpeech() – let’s take a look at the way it’s defined:

[<JavaScript>] WiredClientSpeech() =
  let stuff =
    ConversationEngine.ClientSpeech()
    |> List.map(fun f ->
       let id = f.Id
       LI [ 
         A [HRef "#"] -< [Text f.Speech]
         |>! OnClick(fun x y -> UpdateConversation id |> ignore)
       ] )
  OL [Id "clientSpeech"] -< stuff

Okay, so what happens here is we get a list of LI elements which are then fed into an OL, which we also give a specific id. The slight peculiarity of the way the above is defined is due to the fact that you cannot simply place a list of LIs inside an OL – you will experience type inferencing failure. That’s right – F# isn’t perfect! Anyways, it’s easily solvable with the handy -< operator, or you could just pipe it via |> OL. Of course, we also needed to provide an Id, thus things are defined the way they are.

In the above, you see a call to UpdateConversation with a cached (to prevent closure mishaps) id parameter. UpdateConversation() is the function that uses the conversation manager and actually puts converation items into the DOM.

let rec UpdateConversation id =
  ConversationEngine.SpeechItemFired id
  let srv = ConversationEngine.ServerSpeech()
  JQuery.Of("#serverSpeech").Text(srv.Speech) |> ignore
  ConversationEngine.SpeechItemFired srv.Id
  JQuery.Of("#clientSpeech").ReplaceWith((WiredClientSpeech() :> Element).Body)

So, what’s going on here? Well, we tell the conversation engine that a speech item has been fired, we get the server item first and then simply replace paragraph text with the new item. Then, we also inform that a server item has been fired. Finally, we do something tricky – we replace the #clientSpeech element completely, substituting a Dom.Element that we create synthetically in WiredClientSpeech(). Note that a cast is necessary here because, once again, type inference seeks to mess with us.3

Conclusion

The above problem is something that is very difficult (but doable) in ordinary JavaScript. However, with a little help from WebSharper, we managed to orchestrate the whole conversation process in terms of F#. There are a few caveats though, as always:

  • The site isn’t exactly small due to the JS infrastructure support. A large number of files was dragged in. They’re necessary though – especially when the site is more ‘real’, in the sense of communicating to servers via JSON or whatnot.

  • The F#→JS transformation is very picky. You can easily mess it up and end up crashing the compiler. To be fair, Web# is beta, but it’s still important to remember that not everything can be translated to JavaScript.

  • F#‘s preference for code to be ’in order’ is most inconvenient here. Mutually recursive functions are one thing, but I’ve also been forced to move the conversation engine into a separate module, because keeping it in the main client module caused JavaScript to be emitted in the wrong order, thus spoiling the party.

Overall, I’m happy with my first little foray into the world of HTML sitelets. Now that Web# actually supports all of this, it makes sense to experiment with client-server REST interoperation. I’m actually curious as to how Web# can help here, but for the server side there’s always WCF REST or OpenRasta and the like.

Please check out the demo! And stay tuned for more F#/WebSharper goodness!

Notes

  1. I’m stretching things a little when calling a scripted conversation a graph. It’s really more like a state machine. The kind of conversations you have in games have much greater complexity, and they also have a measure of reentrancy and context-keeping that a simple graph doesn’t support.
  2. It’s worth noting that in a real-world interaction scenario, conversation choices affect a lot more than whether some item is enabled or disabled. However, given that we are working with a virtually object-oriented representation, it’s fairly easy to attach any sort of context-oriented action to particular conversation items. I leave this as an exercise for the reader.
  3. I must admit that the process of organizing this methods was far from intuitive. I had to try a number of things before I figured out how to externalize everything and get it all to work. Admittedly, the code I got at the end isn’t as readable or well-organized as it could be, but this is mainly due to F#‘s limitations.

Written by Dmitri Nesteruk

February 10th, 2011 at 8:51 am

Posted in FSharp

Tagged with

  • http://twitter.com/t0yv0 Anton Tayanovskyy

    I’d like to get a copy of the code that emits JS in the wrong order to fix it:

    https://bugs.intellifactory.com/websharper

    Thanks,

    A

    • http://devtalk.net Dmitri

      I no longer have it. But I’ll be sure to post bugs now that I know where to post them.