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.

No comments: