Building a conversation engine with WebSharper

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.

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:

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:

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:

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.

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

↑ 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.

↑ 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.

↑ 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.