Archive for October 2014

 
 

What I am working on…

I’m working on a puzzle game for iPads that takes the mechanics of a somewhat obscure game from the 1990s(*) and puts them on a double logarithmic spiral.

The final app will be styled similar to one of M. C. Escher’s tessellations of the plane with the tessellation changing at level breaks. I don’t have a name yet…

Gameplay will be like the following, which is video of a prototype I wrote in C# (just WinForms to the GDI, nothing fancy). I’m going to write the real thing in Swift using Sprite Kit, unless Swift turns out to be too immature in which case I’ll use C++ and Cocos2d-x again.

*: The identity of which I leave as an exercise for the reader.

Discretely Distributed Random Numbers in C#

Random numbers generated from a discrete distribution are a commonly needed thing in game development.

By “discrete distribution” we just mean the roll you get from something like an unfair die, e.g. you want a random number from 0 to 5 but you want 4 and 5 to be twice as likely as 0, 1, 2, or 3. If we think of each possible random value as having a weight, in this case 0, 1, 2, and 3 would have a weight of 1 and 4 and 5 would have a weight of 2.

A simple way to generate these kinds of random values is the following. Given some n such that we want random values ranging from 0 to n-1 where for each 0 ≤ i < n we have a weight w(i):

  1. Build a data structure mapping cumulative weight to each value i. By cumulative weight we mean for each i the sum of w(0), … , w(i-1).
  2. Generate a random number r from 0 to W-1 inclusive where W is the total weight i.e. the sum of w(i) for all i.
  3. If r is a cumulative weight in our data structure return the value associated with it; otherwise, find the value v that is the first item in the data structure that has a cumulative weight greater than r and return v-1.

Obviously the data structure in the above could just be an unordered array but a better way to do it is to use a binary search tree because 3. will then be O(log n) rather than linear. In Java you can do this with a TreeMap. In C++ you can do this with an std::map (or in C++ you can do the whole thing with boost::random::discrete_distribution).

However, in C# you can’t just use a SortedDictionary, which is a binary search tree under the hood. You can’t use a SortedDictionary because it does not expose the equivalent of C++’s std::lower_bound and std::upper_bound or the equivalient of Java’s TreeMap.floorEntry(…) and TreeMap.ceilingEntry(…). In order to perform the “otherwise” part of 3. above you need to efficiently be able to find the spot in the data structure where a key would go if it was in the data structure when it is in fact not in the data structure. There is no efficient way to do this with a SortedDictionary.

However, C#’s List does support a BinarySearch method that will return the bitwise complement of the index of the next element that is larger than the item you searched for so you can use that. The downside of the above is that there will be no way to efficiently add or remove items to the discrete distribution, but often you don’t need this functionality anyway and the code to do the whole algorithm is very concise:

class DiscreteDistributionRnd
    {
        private List<int> m_accumulatedWeights;
        private int m_totalWeight;
        private Random m_rnd;

        public DiscreteDistributionRnd(IEnumerable<int> weights, Random rnd = null)
        {
            int accumulator = 0;
            m_accumulatedWeights = weights.Select(
                (int prob) => {
                    int output = accumulator;
                    accumulator += prob;
                    return output;
                }
            ).ToList();

            m_totalWeight = accumulator;
            m_rnd = (rnd != null) ? rnd : new Random();
        }

        public DiscreteDistributionRnd(Random rnd, params int[] weights) : 
            this(weights, rnd) { }

        public DiscreteDistributionRnd(params int[] weights) : 
            this(weights, null) { }

        public int Next()
        {
            int index = m_accumulatedWeights.BinarySearch(m_rnd.Next(m_totalWeight));
            return (index >= 0) ? index : ~index - 1;
        }
    }

where usage would be like the following:

            DiscreteDistributionRnd rnd = new DiscreteDistributionRnd(3,1,2,6);
            int[] ary = new int[4] {0,0,0,0};
            for (int i = 0; i < 100000; i++)
                ary[rnd.Next()]++;
            System.Diagnostics.Debug.WriteLine(
                "0 => {0}, 1 => {1}, 2 => {2}, 3 => {3}",
                (float)(ary[0] / 100000.0),
                (float)(ary[1] / 100000.0),
                (float)(ary[2] / 100000.0),
                (float)(ary[3] / 100000.0)
            );