Conversations II: Closures and Events

27 04 2008

Ive been meaning to continue the post from last time but havent really had the time.

Having tested that the conversation structure could be easily deconstructed I decided to check how it would work in a more graphicy environment. But I had no game or graphics engine to use. So I decided to knock up something quickly using WinForms and GDI+. Most of the test code was held in the form and another class meant to read into the conversation data structure. Since much of a game deals with handling and working with various representational data structures, working in a language which can easily manipulate such makes many implementation details quite straight forward. A quick aside first.

One of my coding peeves is having to write nested for loops. So I decided to write a macro that was syntactic sugar inspired by list comprehensions. List comprehensions are a way of building lists that borrows from mathematical setbuilder notation. For example this is the set of all numbers whose squares are even : \{x \mid x \in R \wedge (x^2 \bmod 2 = 0)\}. Similarly such a list can be built using list comprehensions: [x | x <- [0..], x ** 2 % 2 == 0]. Something like this would be good for building sequences of operations which you wish to occur during certain conditions or are in nested loops. Also I wished to experiment further on with Nemerle macros. I was able to write a dozen line macro which allows the following code:

seq [ WriteLine(x), x in [0 .. 10], even(x) ]

Here seq takes a single expression and the conditions under which it should be run. Thus its definition is: seq[expr,list[expr]]. Being able to only take a single expression is not very flexible but there are work arounds. For example, instead of passing an expression I could pass a function:

def times9(z){def k = 9; WriteLine(k*z)}
seq [ times9(x), x in [0 .. 10], even(x) ]

The function passed thus allows sequences expressions of arbitrary complexity to be built. As well, so a function does not have to be written always, I also wrote a macro which itself looks like/is a single expression that takes a list of expressions and converts them to executable expressions: dothis[list[expr]].

seq [ dothis [mutable y = x, 
                      WriteLine(x*y)], x in [0 .. 10], even(x) ]
Output:
0
4
16
36
64
100

Thus either a function, a statement, or a list of statements converted treated like an expression using the dothis macro can be fed into seq to create simple to reason about but interesting combinations of nested loops and conditionals.

During compile time the following:

seq [ e.Graphics.DrawImage(grass, Point(x ,y )), x in [0,grass.Width..640], y in [0,grass.Height  ..480] ]
               
is expanded into:
    foreach(y in [0,grass.Height  ..480])
                foreach(x in [0,grass.Width..640])
                    e.Graphics.DrawImage(grass, Point(x ,y ));

To continue, in the form there are the following declareations:

mutable playerpos : Point 
hc : HandleTalk 
[Accessor (flags = WantSetter)] mutable isTalking : bool
[Accessor (flags = WantSetter)] mutable writing : string

In the onpaint event the grass is drawn and two images, a mage and a soldier, represented by you. Its position is altered on the KeyDown event. As well, when isTalking is true a blue rectangle is drawn as well as the text held in the writing Field.

when(isTalking)
                e.Graphics.DrawRectangle (Pen(Color.White), Rectangle(9, 9, 465, 151));                
                e.Graphics.FillRectangle (SolidBrush(Color.FromArgb(180,0,0,255)), Rectangle(10, 10, 464, 150));  
                e.Graphics.DrawString(writing , Font("Verdana",10),SolidBrush(Color.White), RectangleF(10,10,464,150))
...
private MainForm_KeyDown (sender : object,  e : System.Windows.Forms.KeyEventArgs) : void           
             
            unless(isTalking)             
                match(e.KeyCode)
                    | Keys.Down => playerpos.Y+= 20
                    | Keys.Left => playerpos.X-= 20
                    | Keys.Right => playerpos.X+= 20
                    | Keys.Up => playerpos.Y-= 20
                    | Keys.Space => 
                                        when(((400 - playerpos.X) ** 2)  + ((190 - playerpos.Y) ** 2)  < (100 ** 2))
                                           isTalking = true;  
                                           writing = hc.interact( Conversations.Guy1.Hello, this);
                    | _ => ();




The player cannot move when talking. When the space bar is pressed it checks if the player is near where I hardcoded the “mage”. If it is, converstion is initiated and the form is passed to conversation handler.

The HandleTalk class is similar to the one demonstrated in the last post.

class HandleTalk
GetResponses (ConversationList : list[string * (Npc->Conversation)], ResponseList : list[Npc-> Conversation], i : int, output:StringBuilder) : StringBuilder *list[Npc -> Conversation]
match(ConversationList)
| anItem::restOfList => def (theString, theFunction) = anItem;
output.AppendLine($”( $(i.ToString()) ): $theString”)
GetResponses(restOfList, theFunction::ResponseList, i + 1, output)
| [] => (output, ResponseList)

public interact(c : Npc -> Conversation, f : MainForm) : string

def interinner(ci,fi)
def p = Npc()
p.Name = “Daerax”
def hi = ci(p);

def resps = hi.arrows;
def l = GetResponses(resps, [], 1,StringBuilder($”$(hi.initial)\n\n”));
l[0].AppendLine($”( $( (l[1].Length+1).ToString()) + ) : $(hi.terminal()) \n”)
l

def dat = interinner(c,f)
def sayThis = dat[0]
mutable listOfOptions = dat[1]
def allowed = [“0″,”1″,”2″,”3″,”4″,”5″,”6″,”7”, “8”, “9”]
def capture(o : object , e: Windows.Forms.KeyPressEventArgs)
match(listOfOptions.Rev())
| [] => f.IsTalking = false; f.KeyPress -= capture;
();
| xs => seq[dothis[ def k = interinner(xs.Nth(select), f), //Conditions
f.Writing = k[0].ToString (),listOfOptions = k[1]], e.KeyChar.ToString() -> chara ,allowed.Contains(chara),
(int.Parse(chara) – 1) -> select,select < xs.Length] f.KeyPress += capture sayThis.ToString () [/source] What it does is when interact is first called it creates a closure about the function interinner. It then adds the function capture to the keypress event of the form which is called whenever a key is pressed. Thus whenever a key is pressed our closure remembers the last value of listOptions, calls our function representing the response, changes the contents of the writing field apropriately and exits. When the conversation is done the function is disconnected from the keypress event. This allows the rest of the world to go on without being held up by the conversing people. Note also the sequence in the second match: [source lang="python"]seq[dothis[ def k = interinner(xs.Nth(select), f), //Conditions f.Writing = k[0].ToString (), listOfOptions = k[1]], e.KeyChar.ToString() -> chara ,allowed.Contains(chara),
(int.Parse(chara) – 1) -> select,select < xs.Length] expands to: def chara = e.KeyChar.ToString(); when(allowed.Contains(chara)) def select = int.Parse(chara) - 1; when(select < xs.Length) def k = interinner(xs.Nth(select), f); f.Writing = k[0].ToString () ; listOfOptions = k[1][/source]The -> operator for assignment is not part of the language, nor in fact defined as a macro, it is actually only matched when the seq expression is deconstructed.

Advertisements




Conversations

15 04 2008

My last post was a prelude to this. This weekend I decided to I decided to tackle scripting in a game engine. Specifically being able to talk to an entity and having a flexible system which if necessary could handle Planescape level of jabbering complexity. Originally, I had thought to perhaps use a graph structure in conjuction with some IronPython to do this.

However, I decided why not just have the conversation structure be written in Nemerle as well. Then instead of various scripts and structures to be built at runtime I’d have a compiled dll which could be called into with the added benefit of having also been statically/compile time verified. I also wanted to experiment with how programming in a functional language would aid in game programming. For I have not actually deen any real game related programming that actually used these concepts (really i havent done anything beyond getting irrlicht to load some models in years). As well I wished to try out some of my ideas of Nemerle as a host language for a game. Of course I have no game engine so I decided to do some console based testing.

First I considered the most basic representation of a conversation as both a statement and the set of responses associated with the statement. I also wanted this model to be simple to extend. To start, I decided statements are of type string and responses can be of type () -> string. So each response is a function which when invoked returns the string to be displayed. This of course could not be implemented without entering into circular dependencies so I decided to create a Conversation type/class.

A conversation contains an initial object of type string, (for all c in C, i -> c does exist, sometimes via composition) a terminal object of type () -> string and arrows of type list[string * (Npc -> Conversation)]. I defined it this way in case I ever wanted to create a category theoretic treatment of conversations I had a loose analogy already in place.

So it looks like this:

public class Conversation
        public this()
            ()
        public this(s : string)
            initial = s
        public this(s : string, t : response)
            initial = s
            terminal = t
            
        public mutable initial : string
        public mutable terminal : response
        public mutable arrows : list[string * (Npc -> Conversation)]

Then conversation is always initiated with intial and can be ended at any time with terminal. Initial is then written out with arrows holding the list of responses (a list of functions) that will take one to another conversation and string displaying the current statement. This was then wrapped in a dll. All the conversations would then be held in one or more dlls. As a test I created a module to represent conversations that could be interacted with. The code goes:

namespace Conversations
    public module Guy1
        public Hello(n: Npc) : Conversation     
 	    def k = Conversation()
	    def k.initial = $"Hello $(n.Name), My name is Joe."
            def k.terminal = Bye
                       
            k.arrows = [("What are your plans", Future), ("How is life?",Terrible)]
            k              
        
        Future(n: Npc) : Conversation
            def k = Conversation("The future is a terrible place")
            def response1 = "Why?"
            def response2 = "What is your name?"
            
            k.terminal = Bye
            k.arrows = [(response1,Terrible), (response2,Hello)]            
            k       
           
	...
            
        Bye(): string       
            "Bye"

Then I could go Guy1.Hello() to initiate conversation and extract the required responses. Next I decided to code a DSL that would make conversations clearer to read. Based on the original code I decided a simple language that mostly served to remove boiler plate code and also make conversations clearer would do.

Nemerle has macros which allow one to do metaprogramming. Not C style but scheme like hygenic macros. The metaprogramming is in two or three parts. The first is of a style that people who have done C++ template metaprogramming would recognize. Namely various compile time execution of certain code that allow various optimizations and new behaviours. The other type is akin to MetaML or Template Haskell and Scheme. Which is the ability to operate on the language’s syntax tree using the language itself and to do various bits and bobs e.g. modify/auto generate code. It also has flexible syntax extension. It is also motivated by MetaML in the idea of having typed code and the notion of execution staging. Being able to operate on, pass around and modify code at compile time is a really nice concept that makes the language flexible. I am experimenting with extending this into runtime – is quite possible.

To continue I arrived at a small set of keywords based on what I had experienced: TlkOf, EndOn, ResponsesOf, Linkers,In, Is, intl, tmnl, put and sc_. sc_ is a short hand macro whose use is sc_[var, string, () ->string]. Which is equivalent to var = Conversation(string); var.terminal = () -> string. Here is a snippet acting as a test:

[Fp] Hello() : ifr 
                    
            TlkOf cnv 
                $"Hello $(n.Name), My name is Joe." 
            EndOn Bye
                       
            ResponsesOf cnv 
                ["What are your plans", "How is life?"]
            Linkers [Future, Terrible]   
[Cp] Terrible() : ifr
                       
            def k = Conversation($"I am dead, $(n.Name)")
            
            tmnl k Is Bye;
            put [] In k.arrows 
            k

A few things to explain are ifr and the [Cp], [Fp] tags. These names are tentative and are attributes that modify the compile time behaviour of the code. Nemerle does not have top level type inference so having to do all that annotating is annoying, especially as the number of parameters I wish to pass each conversation object increseases. ifr is just a type alias for void and is only there to reduce key strokes…

The Cp means conversaton path, it takes the body of the method, generates a new method with the same body as the old one but also binds whatever parameters to the method I require so I do not have to explicitly declare them in the conversation script – for now just n of type Npc (but will eventually be whatever other ones I decide to add later, this is useful because then scripts automatically benefit and functions wont break when i introduce new parameters – although i might do a few overloads for arrows) and makes the method of type Conversation and makes it public. It then clears the old method. Fp does the same but also initializes cnv (bound to the site) to Conversation() and makes sure the method returns cnv so I dont have to type that. When one decorates their methods with those attributes they must realize also be aware of what variables names have been bound (controlled unhygenics). Ofcourse their use is optional. The dsl thing is abt 50 lines of code. I have the Visual studio integration so I get free syntax highlighting as well.

To actually load the conversation I simply recurse through the paths till I terminate.

Module Program
  Main() : void 
    def p = Npc();
    p.Name  = "Daerax"    
    
    def say (b, l, i) : list[Npc -> Conversation]
        match(b)
            | s::ss => def (a, d) = s;
                        WriteLine($"( $(i.ToString()) ): $a" )
                        say(ss, d::l, i + 1)
            | [] => l  
                         
        def interact(c)
        def hi = c(p);
       
        WriteLine($"$(hi.initial)\n")
        def resps = hi.arrows;
        def l = say(resps, [], 1).Rev();
        WriteLine($"( $( (l.Length+1).ToString()) ) : $(hi.terminal()) \n");    
        def o = int.Parse(ReadLine());
        match(l)
            | [] => ()
            | xs => interact(xs.Nth(o-1))
    def k = 0;               
    interact(Guy1.Hello)

 public class Npc
        health : int 
        pos : position2D
        public mutable race: string
        [Accessor (flags = WantSetter)] mutable name : string;