re-Learning Math – Roguelike Design part 9


When I was a child, I started programming on an Atari 400. This was essentially an Atari 2600, but with a keyboard and some additional inputs for accessories. Mine had a tape drive which gave me working storage. It used standard cassette tapes which were abundant at the time. I received the 400 from a relative along with a BASIC cartridge and a book titled 101 BASIC Computer Games. There were no common storage devices at this time, so this book didn’t come with a CD or floppy containing source code. This book was the source code. Pages and pages of source code.

BLKJAC Program Example

If you wanted to play Black Jack, you loaded up your BASIC disk, ran the executable, and started typing out the instructions from the book starting at line 10. A while later, you ran the program, and if you were lucky, you hadn’t made any errors. If you encountered an error, you tried again, going back through the code and trying to find your typo.

I was about 8 years old. I was hooked.

Over the next 10 years, I learned every programming language I could. I downloaded programming manuals and printed them on my tractor-feed dot matrix printer. I created all sorts of programs. Games, dice rollers, notepads, games… did I say games already? I probably wrote a few hundred little text-based adventure games. I learned everything I could about game programming and randomness.

Using random number generators (RNG) is the key to the game-programming kingdom. Learning how to make random numbers… less random is the real magic. Randomness can make things fun. Too much randomness can make them seem buggy, too easy, or too difficult. It’s hard to plan for too much randomness. In the real-world we use polyhedral dice to create more or less randomness as needed. Rolling a 1 on a six-sided dice is frequent… 1 in 6 rolls. Rolling a one on a 20 sided dice is much more difficult. So, I learned all sorts of ways of evening out the randomness to produce more uniform results… then I joined the Marine Corps.

All I ever wanted to be was a programmer… and I got to be one in the Marines. I wrote software to manage inventory and shippping/receiving processes in a large warehouse. It was CRUD (that’s programmer speak for Create Read Update Delete – a program that just stores and retrieves data) but in the real world, the vast majority of software is just CRUD. During that time, I lost my passion for software, and I unlearned all that game programming knowledge.

Here I am, 20 years later, trying to figure this all out again. I knew what I wanted, but my Google-Fu was failing me. I didn’t have the terminology for what I was trying to achieve:

Given a number (x) between y and z, return a random number, along a bell curve, where the peak of the curve is at x and y is the minimum and x is the maximum.

This is called Normal Distribution. I know that now. I had to contact a friend who is a math professor to walk me through this. He tried. I couldn’t comprehend what he was saying, so I wrote my “hack” version of this idea. It looked something like this.

$swing = rand(0,100);
switch (true) {  // determine how much variance
  case ($swing >= 80):
    $variance = 2; // occasionally, tier will be adjusted by 2 levels
    break;
  case ($swing >= 50):
    $variance = 1;
    break;
  default:
    $variance = 0;
}

First, why?

This game will be randomly generated. I want to trust my code as much as possible to make the game fun. I don’t want to have to write in “give the player this” after each combat. That’s the whole point of writing this engine in the first place! But, I don’t just want to say “give me a level 1 piece of gear” and that’s what I get. That’s just not fun. There should be a chance to get better (or worse) gear than you deserve. Think of any modern RPG with gear levels (common, rare, legendary). I want there to be a chance that you get something awesome, even at level one. That’s what the code above was doing, just in a really dumb way.

Each time I ask for a weapon, I can give the engine a Tier value. This is roughly equal to the strength of the item. The code rolls a d100 and uses the switch case to possibly modify the requested tier. Most of the time (50%) we set the variance to 0 – no change, 30% of the time we get a variance of 1 and a 20% get a variance of 2 tiers. Once I know how much variance I have, I can add and subtract that from my requested variance and use those numbers in the SQL.

SELECT * FROM weapon_type WHERE active = 1 AND tier >= ? AND tier <= ? ORDER BY RAND() LIMIT 1

Notice that I’m still getting a random result from the database… so even if our d100 rolled a variance, the SQL is still going to return a result within a range… which means we might get the requested tier after all. This was my super hacky way of doing a sort of “normal distribution”.

What’s Wrong With That?

There’s nothing “wrong” with it per se. It gives the engine some flexibility and makes things “more fun” for the player. The problem is, it’s not enough. I wanted there to be a small chance that you might get a weapon from any tier, regardless what was asked for. I could have modified the switch statement even more and added a bunch of breakpoints, but let’s be honest, that’s not the best way.

Additionally, I was using a similar setup for determining the “condition” of the item. This was a way to make everything have unique values even if they are the same type.

  $condition = rand(0, round($weapon['attack'] * .4));
  if((rand(0, 100) * $args['power'])  >= 98) {
    // some weapons might be amazing
    $weapon['attack'] = $weapon['attack'] * 2;
  } else if((rand(0, 100) * $args['power'])  > 38) {
    $weapon['attack'] = $weapon['attack'] + $condition;
  } else {
    $weapon['attack'] = $weapon['attack'] - $condition;
  }

It may not look like it, but these two code blocks are doing very similar things. They are both trying to randomize some aspect of the item without being too dramatic. Again, this is a perfect use case for a standard distribution model and writing a good function means I could replace both of these sections.

Standard Distribution

I spent quite some time trying to find a good example of how to do this in code. I found lots of things about Gaussian numbers and deviation models, but none of them made sense to me and seemed overly complex. The last statistics class I took was my senior year in high school. That was over 20 years ago… but that’s no problem… my friend Jeff is a math professor! Jeff’s answer was so simple that I dismissed it out right for about a week.

One way I might do it is by “coin flips”. If I wanted a ~ level 10 with high variability, I would flip 20 coins and count the number of tails. If I wanted low variability, I would flip two coins and add the number of tails to 9.

Jeff, the Math Professor

To be honest, when he told me this, I wasn’t even sure I knew how to replicate this in code. I doubled down on my switch statement, and moved on. It was only after I started combining all the item create routines into one that I came back to this. When I ended up finally “getting” it, I was able to build a nice little function that has lots of flexibility to be used all over the final game design.

/**
 * Generate a random number within a standard deviation.
 *
 * Creates a random integer, along a standard deviation curve with a
 * desired maximum value and "peak" set. Peak is where you would like the tip of
 * the curve to land.
 *
 * @param   int    $max    Minimum 1. Desired maximum value.
 * @param   int    $peak   Desired point to generate numbers around on the line.
 * @param   float  $curve  Optional. Default 2. Minimum 2.
 *                         Higher numbers decrease the deviation (tighter curve).
 *
 * @return  int    Integer between 1 and $max
 */
function standardDeviation($max, $peak, $curve = 2) {
  // max needs to be a positive integer to avoid infinite loops
  if ($max < 1) { return $max; }

  if ($curve < 2) { $curve = 2; }
  $value = 0;

  while ($value < 1 || $value > $max) {
    $heads = 0;
    for ($i=0; $i < ($max * $curve); $i++) {
      $heads += rand(0,1);
    }

    $heads = intval( round($heads / ($curve / 2)) );
    $value = $heads + ($peak - $max);
  }

  return $value;
}

Besides a bit of input sanitization in the beginning, the “real” function is pretty simple. You provide a maximum desired value $max, where you want the peak of the curve to be $peak, and how tight you want the curve to be $curve. Using your inputs, we flip a number of coins rand(0,1) equal to $max * $curve. We then take the total number of heads and divide it by $curve/ 2. If you notice, the default for $curve is 2, so unless you specify a different number, this operation doesn’t do anything… $heads = $heads. We then take how ever many heads we flipped and add the difference between $max and the desired $peak. This is ultimately the operation that lets us move the tippy-top of the curve… you know, the peak!

There’s a bit of a hack in here still. This whole thing is wrapped in a while loop to ensure that we return a value that is not less than 1 and not greater than the input $max. There might be a better way of doing this, but it works. Let me know if you have a different solution.

What Was the Point of the First Half of this Article?

At some point, while working on this function, I had a sudden realization… I had written this same function dozens of times before… in my teenage years. I had learned and forgotten all of this way back when I thought I was going to be a programmer for a living. I am a programmer now, but I took a 15 year break and tried lots of other things before coming back around. Now, I find myself trying to recreate some of the joy I felt writing simple text-based adventure games in the 90’s, but first I have to re-learn all those things I forgot.

I’ve been writing software for work for about 6 years now, but I don’t get to much math. I’m still primarily writing CRUD. It’s all just basic input/output maybe with some summing or multiplication. Wrapping my head around mathematical concepts has proven to be much more difficult in my 40’s than it was in my teens. This project has taught me many things so far, but the most surprising is just how much I’ve forgotten. The good news is, I love learning. I’m excited to see what’s next!

https://noroguesallowed.com

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.