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) ]

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.

                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           
                    | 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]
| 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”)

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)
| [] => 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.