Anonymous
Domino 2.0 Rich Internet Applications with IBM Lotus Notes/Domino
You are here: Today » A text adventure game written in XML and JavaScript
« Code to HTML with syntax highlighting
Q and other mysterious XHTML tags »

A text adventure game written in XML and JavaScript

My love for text adventures goes about 25 years back, when I discovered computing on a Commodore64. I got hold of The Quill, a program to write text adventures. Amazingly, it came with a complete manual that explained all the tables and even the floatchart of the code itself.

A few years ago, I had to create a complete XML driven web content management system in Domino, and in preparation I tried to find examples of good use of XML. Then I stumbled upon QML or 'QuestML' from Philipp Lenssen which inspired me to write a program like 'The Quill' in XML. My first version was Internet Explorer only and used XPath to processed the XML directly. Now I have reworked it so that it also works in Mozilla. It uses JavaScript to index the objects instead of Xpath, since Mozilla doesn't support XPath.

For the impatient, here is the Jungle adventure, and this is the XML that defines it.

Text based versus table based data structure

While a relational database data model consist of tables with rows and colums, XML can define a data model that is completely free-flowing, much like an HTML document. This free-flowing data model is exactly what we need for text adventures. In fact, my XML adventure system is a combination of both data models. The vocabulary, objects, locations and messages are much like tables, each entry having an unique key:

<!-- objects -->
<object key="apple" start="path" caption="an apple">A normal size delicious red apple. I wander how it ended up here in the middle of the jungle.</object>
<object key="tiger" start="clearing" caption="a tiger">An enormous tiger is standing here blocking your way.</object>
<object key="book" start="CARRIED" caption="a notebook">The book is called 'The Jungle Book: Tricks and Tips'. It also has your name on it.</object>
<object key="vine" start="path" caption="a vine">A particulary long and thick vine is hanging down just beside you.</object>
<object key="fungus" start="trees" caption="some fungus">Some kind of vaguely familiar fungus is growing here on a vine.</object>

<!-- messages -->
<message key="title">Lost in the Jungle</message>
<message key="exits">Visible exits:</message>
<message key="also">You can also see:</message>

Locations and actions however have a combination of text and processing instructions, defined as XML and carried out by JavaScript. They even have if-then-else branching:

<location key="clearing" caption="At the clearing">
    Here the jungle opens up a bit and the path takes you straight into a clearing. The path seems to continue on the south side of the clearing some fifty paces away.
    <exits>
        <exit go="north" to="path"/>
        <exit go="east,west" to="jungle"/>
        <exit go="south" to="camp">
            <if test="hero IS repelling">
                <do>
                    When you approaches the tiger it looks confused. Then it really takes in your smell. It suddenly bolts, turns and takes off into the jungle.
                    <destroy object='tiger'/>
                </do>
                <else>
                    The tiger opens its big mouth and lets out a terrifying growl. Apparently it won't let you pass.
                    <abort/>
                </else>
            </if>
        </exit>
    </exits>
</location>

The JavaScript

Although the code is just over 5 Kbytes, it would take too long to explain it all, but here are a few excerpts:

Initiating the game

init:function(){
    var m=this,v,o,i,a=m.data=G.xml(m.url),mv,mw,j,vv,p
    o=$$('message',a);v=m.message={};for(i=0;i<o.length;i++)v[$a(o[i],'key')]=G.text(o[i]).split('|')
    o=$$('word',a);mv=m.moves={};mw=m.voc={}
    for(i=0;i<o.length;i++){
        p=o[i];v=G.text(p).split(',');vv=v[0]
        if($a(p,'move')=='1')mv[v[0]]=vv
        for(j=0;j<v.length;j++)mw[v[j]]=vv
    }
    m.index('object','obj');m.index('location','loc');m.index('action','act')
    v=m.msg('title');G.d.title=v;m.put('adv-title',v)
    m.put('adv-button',m.msg('enter'));m.put('adv-footer',m.msg('footer'))
    document.onkeypress=G.adv.onKey
    m.newGame();m.doDesc()
},
index:function(a,b){var m=this,o=$$(a,m.data),v=m[b]={},i;for(i=0;i<o.length;i++)v[$a(o[i],'key')]=o[i]},
newGame:function(){
    var m=this,o=m.obj,i,v=m.vars={},n,p
    for(p in o)v[p]=$a(o[p],'start')
    m.current=$$('location',m.data)[0]
}

After loading the XML, the init() function creates indexes to the objects according to their key attribute. This makes it possible to access any object quickly, e.g. the message with key 'title' as m.message['title'].

Processing the user input

This function takes the input from the user and splits it into words. It first looks if it can recognize one word that exists in the vocabulary. If it is a movement, then it tries to move. If it is an action, it tries to find a second word that matches the vocabulary. If it finds one, the action 'word1 word2' is passed to the doAction() function. Else it tries an action with the first word only 'word1 *'.

onEnter:function(){
    var m=G.adv,v=$('inbox').value,o=v.split(' '),p=0,w1,w2,ww,loc
    if(m.pending)return m.doDesc()
    if(v=='')return m.doReady('waiting')
    while(p<o.length && !w1){w1=m.voc[o[p]];p++}
    if(!w1)return m.doReady('dontunderstand')
    if(m.moves[w1]){loc=m.exits[w1];return loc?m.doGoto(loc):m.doReady('cantmove')}
    while(p<o.length && !w2){w2=m.voc[o[p]];p++}
    m.secondWord=w2
    ww=w1;if(w2)ww+=' '+w2
    if(m.act[ww])m.doAction(ww);else m.doAction(w1+' *')
    m.doReady()
},

Executing actions

If no action exists, the message 'cantdo' is displayed. Else, all the child nodes of the action XML are processed:

doAction:function(a){
    var m=this,o=m.act[a];if(!o)return m.add(m.msg('cantdo'))
    m.add(m.getChildren(o));m.doReady()
}

Which leads to the processing engine getText(). Each node is processed according to its nodeName:

getText:function(a){
    var m=this
    switch(a.nodeName){
    case 'location':return '<h2>'+($a(a,'caption')||$a(a,'key'))+'</h2>'+m.getChildren(a)+m.getObjects(a)
    case '#text':return m.p(G.text(a))
    case 'br':return '<br/>'
    case 'goto':m.current=m.loc[$a(a,'location')];m.pending=true;return m.p(m.msg('continue'))
    case 'exits':return m.getExits(a)
    case 'quit':m.newGame();m.abort=true;m.pending=true;return m.p(m.msg('gameover'))
    case 'inventory':return m.doInventory()
    case 'abort':m.abort=true;return ''
    case 'describe':return m.doDesc()
    case 'examine':return m.doExamine()
    case 'if':return m.getChildren(m.getIf(a)?$$('do',a)[0]:$$('else',a)[0])
    case 'destroy':m.vars[$a(a,'object')]='NULL';return m.doDesc()
    case 'make':m.vars[$a(a,'object')]=$a(a,'value');return m.doDesc()
    case 'take':return m.doTake(a)
    case 'drop':return m.doDrop(a)
    }
    return '##'+a.nodeName+':'+G.text(a)+'##<br />'
}

Playing the game

As in a good adventure game, you should try everything. I've left an apple on the jungle path that you can examine (x), drop, take or eat. In your inventory (i) there's a book you can examine (x), drop, take, read. Try to turn pages.

If you are stuck: here's the spoiler:

When you come across a tiger, try kissing or hugging him. You won't get past him that way, but it's always fun trying something 'unexpected'.

Enjoy the Jungle adventure

Star rating

100%

Comments

To add a comment, log in or register as new user. It's free and safe.