Saturday, March 29, 2014

Random Hell

Programming with Elm so far has been quite nice, but now that I'm starting on combat, I'm hitting an awkward spot.

The problem is that, since this is an RPG, it relies heavily on random number generation.  A random number generator can't be used directly by a pure function, so all RNG must come over a signal.

Suppose we have a simple procedure like this:

Check if the character is standing in a room with a pit feature.
If so, roll 1d20 and compare it to the sum of the character's dexterity and elven boots bonus.
If it is greater, inflict 3d6 damage on the character, check for death, and drop him a level deeper in the dungeon if he survives.

This procedure requires up to 4 rolls: 1 for the dexterity check and 3 for the damage taken.

My first pass at the problem was to have a random signal triggered every 1/100 second and a ROLL game state that waits for the next random signal and passes it to a provided continuation.

So for the above example, there would ultimately be four continuations, one for the remaining tasks to perform after each roll.  This is just a very simple procedure.  There are others that take dozens of continuations to express.  It started getting gross quickly.  A complaint against typical asynchronous programming is "Callback Hell," but this was feeling like a different incarnation of the same thing. Maybe it was "Random Hell."

I ended up making some helper functions like

roll10 : (Float -> Float -> Float -> Float -> Float -> Float -> Float -> Float -> Float -> Float -> GameState -> GameState) -> GameState -> GameState

that would handle batching up 10 rolls in a single continuation.

This made it a bit less obnoxious, but there were still problems.  Constantly updating the game state 100 times a second, which would then recalculate the scene graph and check for updates, was using enough CPU to get my laptop's fan going pretty hard.  I then sampled the graphics output only 10 times a second, which lightened the load, but introduced some noticeable delay between key presses and updates.

I ended up abandoning this idea and going with a new one that is based a bit more on real life.

Consider something like the weather.  We might model it as a stochastic process with many independent random inputs.  There is not really a concept of a single randomness generator and a sequence of "rolls" that are done.  Instead, there are many sources of randomness that all combine in parallel to influence the weather.

I took that idea and turned it into a "randomness context" or RandomCtx in the game state.  The idea was that any state transition would no longer need to only partially complete with a continuation since every possible random event would have its own randomness source's current value in the RandomCtx.

For the pit example, there would be four named independent events in the RandomCtx: for example, clumsinessAroundPits, pitReactionTime, distraction,  and groundSoftness.  clumsinessAroundPits could be the randomness source in the calculation if the character falls in or not and the other 3 can be the 3 randomness sources that determine how much damage is taken after falling in.

Now, 100 times a second, a random signal can fire, but it is folded back on itself to gradually fill up the whole RandomCtx before it loops around and starts filling up another one.  The resulting signal is filtered so only complete contexts remain, which slows the rate down to about 10 times a second currently.  Sampling the graphics output is no longer necessary, and there are no more continuations. Everything has what it needs all at once.

So far it seems a lot better, although it does make it impossible to have potentially infinite processes like the teleport procedure that keeps teleporting the character as long as he keeps failing a 20% chance roll. In principle this can continue any amount of time, although the limit of the probability approaches zero as the number of teleports approaches infinity.  I think I can give up some game features to simplify most of them, though.  I can always put the continuation-based approach back in for the features that need an unbounded number of rolls to implement.

No comments:

Post a Comment