Showing posts with label continuation. Show all posts
Showing posts with label continuation. Show all posts

Tuesday, May 13, 2008

Day 103: Blogging - "Kuiper Dev Blog"

Today's subject likens to that two-line fix I did today, #35. That's right, when editing a module the game will change its window title to "Kuiper - Editing 'Your Game Title Here'", and when playing it'll be "Kuiper: Your Game Title Here". This ticket was put in a long time ago because I couldn't tell my editing and testing windows apart. I had some spare time at the end of the day and hacked it in.

Sadly, that was the only easy part of today. Most of the work was on #135, getting sectors to do what Planets were doing easily - in this particular case, that's handling missions. The main problem was that planets' checking was simple, they had to do it once because a new planet GUI is re-created every time you land. The sector GUI sticks around when you go to other states.

The 'return from another state' code looks like this:

def awaken
@continue.call if continue

do_mission_checks
end

It has to be like that, because do_mission_checks requires the continuation support in order to work. But the problem is, if any other state sets @continue (and they all do), then it's called and the mission checking never happens.

The fix is, take every state you're transitioning to with a continuation and, when it comes back, force a call to awaken:

def land
callcc do |continue|
@continue = continue
@driver << planet
end
# We'll resume here
simulate_passage_of_time
awaken unless @continue
end

That way we call awaken when we need it to process mission events, but not if calling it would call a continuation. All told I wrote about 8 lines of code, net. If only all days were as simple as yesterday!

Sunday, May 11, 2008

Day 101: Time Travel

FINALLY, I got #137 done. It feels like I've been working on it forever, probably because of all the work I had to do yesterday.

The fun thing is, today I ripped out most of that work and replaced it with an entirely different way of working. One that was more unit-testable, for one, which made it a lot easier, and what I ended up doing made a lot more sense.

Let's say you were going to land on a planet that had a plot mission for you. Here's how it would work before:

  • For every plot mission, find out if it's awardable:


    • Return false immediately if there's any condition we can determine right now is failing

    • Otherwise, return a list of each condition that's going to need user input.


  • If we got a list instead of 'true', go through that list and pop up questions for all of it. If they're all answered right, then go on to award the mission.


    • If any of these actions is going to need to display something on the screen, return them right now before awards are done.

    • Otherwise, do the actions


  • Again, if we got a list instead of 'true', we have to go through the list and make sure the user sees all of the dialog. Then call the award function again but tell it to ignore calls to pop stuff up.


This was incredibly complicated and error-prone, as demonstrated yesterday. I kept thinking of how handy it'd be to be able to, when we see a condition or action that requires user input, stop right there and do it, then come back. If only there was a mechanism that let me do that which I'd mentioned before!

So, continuations to the rescue! The hardest thing about reimplementing the system to use continuations is that my analogy from the other day was more apt than I knew. Continuations are a time machine. To explain, let me show you some code I'd played with in irb:

def contFun2
puts "Is this fun?"
callcc { |x| return x }
return 'yes'
end

irb(main):048:0> z = contFun2
Is this fun?
=> #<Continuation:0xb7c99c14&rt;
irb(main):049:0> z
=> #<Continuation:0xb7c99c14&rt;

So far so good; when I call the function, I get a continuation from the middle of it. This continuation remembers things like variables and, most importantly for our purposes,the call stack. Now watch what happens when I use it:

irb(main):050:0> y = z.call
=> "yes"
irb(main):051:0> y
=> nil
irb(main):052:0> z
=> "yes"

You might be surprised to see that - z.call doesn't appear to return anything, and z itself somehow turned from a continuation to a string!

What actually happened behind the scenes is that, when I called the continuation, the stack as it was vanished, replaced with what it had been. And what had been was a call to my function, just after the callcc line. Thus, the function returns "yes", and that yes is assigned to z, because that assignment was on the stack before the function call. Once I understood what was going on there, it wasn't hard to make a new mission evaluator:


  • For every plot mission, find out if it's awardable:

    • Call mission.awardable? It'll return true, false, or, a [ condition, continuation ] pair - pop up the condition and when we're done with that, call the continuation. Repeat until we get true or false.


  • If it was awardable, award it.


    • Call mission.award. It'll return true or a [condition, continuation] pair. Handle this exactly as above.


  • There's no step three!



Tomorrow: Using all of this so you can get missions just from flying into a sector.

Saturday, May 10, 2008

Day 100: Wild and Crazy Ride

100 days of programming! Barring a week where my sinuses were killing me and a month where I bought and moved into a new house, I've been programming pretty much nonstop.

Today's story is, I got quite a bit through having missions able to pop up information to the player, which is the primary way of getting the plot of the game across. I shall now explain how this came about.

First, a note: #127 was a minor bug that annoyed me. You'd click on a button in one state, and any buttons in that same location in the parent state would end up clicked too. I'll come back to that.

My main work was on #137. Presenting the player with messages when they're given a mission turned out to be very easy, so long as I was doing only that. It actually displayed the correct message the first time I ran the program, which almost never happens for anything. I was very happy!

I'd made the dialog box which displays this general enough that it could handle yes/no questions as well, so I figured I'd go on to the next part, which is having conditions ask questions. So you could land on a planet, get a question asking "Do you want to be recruited into this planet's defense force?", and if you answer yes you get the mission.

This worked pretty much first thing as well. I was quite happy, but noticed something awry. Specifically, I clicked 'yes' to the mission, but didn't get the follow-up "You accepted the mission" message I'd put in the test mission.

An aside: I'd unit tested everything I could about these new conditions and actions, but as they displayed items on screen and rely on the user for input, this turned out to not be much.

I ended up having to make the evaluator a full-fledged state rather than the 'plugin' sort of existence it had before, because it made handling the continuations that much easier. This ended up not helping and I'm considering reverting that particular change (because a plugin is /far/ more useful in more situations than a full-fledged state). I figured something was going wrong with my continuations. As mentioned before, I love continuations but I'll freely admit they're somewhat of a black art to me. I know enough to be dangerous and get some things done, but when it starts falling apart I'm not sure where to look.

Eventually, print statements are everywhere. What they tell me, in every single case, is that the message dialog is, in fact, being displayed, despite the fact that I can't see it. I spend a good full hour doing nothing but putting tracer code in my entire setup to find out when this is happening.

Suddenly, it worked! I immediately realized that I hadn't done anything. I frowned, horrible suspicion dawning in my mind. I ran the program again, clicked the button in the exact middle, and didn't see the message. One more time through, clicking the button off to the side where there was no corresponding button in the message displayer, however? That worked fine.

I'd spent an hour tracking down a bug I already knew existed. Thanks, #127.

The rest of my time was spent fixing #127, which proved surprisingly difficult. I figured that there was a situation where the mouse-up would trigger a state change before the mouse-up was done fully processing, and so the new state would get it. But that turned out to not be the case. My army of print statements helped here, because what I saw happen was something like this: (output made up because it's fixed now and I don't have anything to paste here)

Beginning event handling loop
Got mouse down event#<MouseDown #7544>
Got mouse up event #<MouseUp #4556>
State transitioned
Ended event handling loop
...
Beginning event handling loop
Got mouse down event#<MouseDown #7544>
Got mouse up event #<MouseUp #4556>

I wasn't getting duplicate mouse-ups that weren't handled, this was two separate times through the event loop, yet somehow I was getting the exact same events as before.

It was this that led me to the 'time machine' metaphor for continuations. They don't just return you to where they left off, they change the variables and such to be exactly like they were then. This is handy because otherwise I wouldn't be able to refer to them, but putting continuations in the middle of my event-handling loop ended up rewinding and then replaying it!

I fixed this by having the event loop just build an array of MouseUp events, then later - once outside that loop - going through them and calling the actual functions. It seems to work; that loop itself never gets rewound or if it does it's not causing the same problems.

But man! What is it about the stuff that's supposed to be easy that makes it so hard?

Monday, May 5, 2008

Day 95: Make Mine Mini

Today I made the first addition to the engine.rb file in a very long time; I moved the Waker module over to it. The Waker module is what implements continuations, which if I'd known about them when I originally started this project, would have been even more integrated into the engine.

Basically, here's what TraderMissions code for handling a dialog would look like, if TM was in Ruby:

# Called every time this state becomes
# the active one.
def activate
if @dialog.yes?
do_things
end
if @other_dialog.stuff?
do_other_things
end
end

The activate function has to check every possible state that it might transition into, see if it was called and if so what its result was, and deal with that. This rapidly becomes horrible. Whereas with continuations:

def activate
@continue.call if @continue
end

And that's set up when I originally transition to the other state:

def verify_cheese
cv = CheeseVerifier.new
callcc do | cont |
@continue = cont
@driver << cv
end

enable_cheese if cv.yes?
end

@continue.call moves execution back to after the block where I originally set @continue, thus letting me pick up where I had transitioned away from the current state. Hooray for continuations!

Oh, right, the subject. I'd actually done that continuation stuff months ago, the Waker thing was me moving it. Most of the day was me creating a simple object selector. I thought to myself "I need a MiniBuilder equivalent for those children which only have one object. Thus:

THE MINICHOOSER!