Adapted from Steve Rabin article on filtered randomness from AI Programming Wisdom 2.
As you identified in the question, a truly random sequence allows for rare anomalies that are perceived as random. Instead, people expect random results to have an average distribution & avoid certain characteristics. In particular, research by Falk has identified certain things that cause people to perceive things as less random:
- long runs of outcomes
- lack of alternations
- repeated patterns
- symmetric patterns
To counter these problems we can keep a short history of results and apply a series of rules:
- force alternation when needed
- restrict long runs
- eliminate unwanted patterns
My personal take is that tuning unwanted runs is more important that filtering out patterns. But since it's relatively easy to comment out that section of code if desired, so I'm presenting the material in its original form.
Here's a rough C# adaptation from the original C++ companion code along with a basic tester / demo:
class Program { const int FR_CHANCE_HISTORY_LENGTH = 20; int[] m_hist = new int[FR_CHANCE_HISTORY_LENGTH]; const float FLOATING_POINT_ROUNDOFF_ERROR = 0.000001f; static void Main(string[] args) { int count = 0; float odds = 0.25f; for(int a=0; a<1000; a++) { if (RandChance(odds)) { count++; } } Console.WriteLine("count for " + (int)(odds * 100) + "% out of 1000 trials = " + count + " or " + count/10.0 + "%"); Console.WriteLine("example output: "); for (int a = 0; a < 50; a++) { int i = RandChance(odds) ? 1 : 0; Console.Write(i); } } public static bool RandChance(float a) { Random rng = new Random(); return ((((float)rng.Next()) + 0.5) * (1.0 / (float)Int32.MaxValue) <= a); } // These tables were carefully hand-tuned to ensure that random chances were within 1% of requested chance. int[] MaxAlternations = { 2, 4, 4, 4, 4, 4, 4, 4, 4, 6, //0.01 through 0.10 6, 6, 6, 6, 6, 8, 8, 8, 8, 8, //0.11 through 0.20 10,10,10,10,10,12,12,12,12,12, //0.21 through 0.30 12,12,12,12,12,12,12,12,12,12, //0.31 through 0.40 12,12,12,12,12,14,14,14,14,14 };//0.41 through 0.50 int[] MinAlternations = { 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, //0.01 through 0.10 1, 1, 1, 4, 4, 4, 4, 4, 4, 4, //0.11 through 0.20 4, 4, 4, 6, 6, 6, 6, 6, 6, 6, //0.21 through 0.30 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, //0.31 through 0.40 8, 8,10,10,10,12,12,12,12,12 };//0.41 through 0.50 int[] MaxTrueRun = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, //0.01 through 0.10 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, //0.11 through 0.20 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, //0.21 through 0.30 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, //0.31 through 0.40 3, 3, 3, 3, 3, 3, 3, 3, 3, 3 };//0.41 through 0.50 int[] MaxFalseRun = {25,25,25,25,25,25,25,15,15,15, //0.01 through 0.10 14,14,14,12,12,12,12,10,10,10, //0.11 through 0.20 10,10,10,10,10,10,10, 7, 7, 7, //0.21 through 0.30 7, 7, 7, 7, 7, 6, 6, 5, 5, 5, //0.31 through 0.40 5, 5, 4, 4, 4, 4, 4, 3, 3, 3 };//0.41 through 0.50 int[] InitialHistoryChance = { 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0 }; bool GenRand(float chance) { int i, alternations = 0; float adjchance = chance; int adjchance_int = (int)((chance * 100.0f) + 0.5f) - 1; bool flip = false; for (i = 0; i < FR_CHANCE_HISTORY_LENGTH - 1; i++) { // move history down m_hist[i] = m_hist[i + 1]; } if (chance > 0.5f) { adjchance = 1.0f - chance - FLOATING_POINT_ROUNDOFF_ERROR; adjchance_int = (int)(((1.0f - chance) * 100.0f) + 0.5f) - 1; flip = true; } if (adjchance_int < 0) { adjchance_int = 0; } // Get random value based on chance m_hist[FR_CHANCE_HISTORY_LENGTH - 1] = RandChance(adjchance) ? 1 : 0; // force desired alterations int max, min; for (i = 0; i < FR_CHANCE_HISTORY_LENGTH - 1; i++) { // count alternations if (m_hist[i] != m_hist[i + 1]) { alternations++; } } max = MaxAlternations[adjchance_int]; min = MinAlternations[adjchance_int]; if (alternations > max) { // if too many alternations, make the new value the same as the last m_hist[FR_CHANCE_HISTORY_LENGTH - 1] = m_hist[FR_CHANCE_HISTORY_LENGTH - 2]; } else if (alternations < min) { // if not enough alternations, make the new value the opposite fo the last m_hist[FR_CHANCE_HISTORY_LENGTH - 1] = (m_hist[FR_CHANCE_HISTORY_LENGTH - 2]+1)%2; } // eliminate implausible runs int run = 1; //the first element starts as a run of 1 for (i = FR_CHANCE_HISTORY_LENGTH - 2; i >= 0; i--) { // count size of the most recent run if (m_hist[i] == m_hist[i + 1]) { run++; } else { break; } } if (m_hist[FR_CHANCE_HISTORY_LENGTH - 1]==1 && run > MaxTrueRun[adjchance_int]) { m_hist[FR_CHANCE_HISTORY_LENGTH - 1] = 0; } else if (!(m_hist[FR_CHANCE_HISTORY_LENGTH - 1]==1) && run > MaxFalseRun[adjchance_int]) { m_hist[FR_CHANCE_HISTORY_LENGTH - 1] = 1; } // eliminate repeating motifs of size 4, like 01110111 where 0111 is the motif if (chance >= 0.4f && chance <= 0.6f) { // enforce for around the 50% chance case if (m_hist[FR_CHANCE_HISTORY_LENGTH - 1] == m_hist[FR_CHANCE_HISTORY_LENGTH - 5] && m_hist[FR_CHANCE_HISTORY_LENGTH - 2] == m_hist[FR_CHANCE_HISTORY_LENGTH - 6] && m_hist[FR_CHANCE_HISTORY_LENGTH - 3] == m_hist[FR_CHANCE_HISTORY_LENGTH - 7] && m_hist[FR_CHANCE_HISTORY_LENGTH - 4] == m_hist[FR_CHANCE_HISTORY_LENGTH - 8]) { if (m_hist[FR_CHANCE_HISTORY_LENGTH - 1] == 0) { m_hist[FR_CHANCE_HISTORY_LENGTH - 1] = 1; } else { m_hist[FR_CHANCE_HISTORY_LENGTH - 1] = 0; } } } // eliminate 111000 and 000111 pattern if (chance >= 0.4f && chance <= 0.6f) { // enforce for around the 50% chance case if ((m_hist[FR_CHANCE_HISTORY_LENGTH - 1] == 0 && m_hist[FR_CHANCE_HISTORY_LENGTH - 4] == 1 && m_hist[FR_CHANCE_HISTORY_LENGTH - 2] == 0 && m_hist[FR_CHANCE_HISTORY_LENGTH - 5] == 1 && m_hist[FR_CHANCE_HISTORY_LENGTH - 3] == 0 && m_hist[FR_CHANCE_HISTORY_LENGTH - 6] == 1) || (m_hist[FR_CHANCE_HISTORY_LENGTH - 1] == 1 && m_hist[FR_CHANCE_HISTORY_LENGTH - 4] == 0 && m_hist[FR_CHANCE_HISTORY_LENGTH - 2] == 1 && m_hist[FR_CHANCE_HISTORY_LENGTH - 5] == 0 && m_hist[FR_CHANCE_HISTORY_LENGTH - 3] == 1 && m_hist[FR_CHANCE_HISTORY_LENGTH - 6] == 0)) { if (m_hist[FR_CHANCE_HISTORY_LENGTH - 1] == 0) { m_hist[FR_CHANCE_HISTORY_LENGTH - 1] = 1; } else { m_hist[FR_CHANCE_HISTORY_LENGTH - 1] = 0; } } } if (flip) { // requested percentage (chance) is above 0.50, so flip result return (!(m_hist[FR_CHANCE_HISTORY_LENGTH - 1]==1)); } else { return (m_hist[FR_CHANCE_HISTORY_LENGTH - 1]==1); } } }
Result:
count for 25% out of 1000 trials = 253 or 25.3% example output: 10000001000000100000001000010011000000001001000010
As indicated in the comments, the tables were hand tuned for the corresponding percentages. As such if experiment with other values you should make sure to thoroughly test the result to avoid unpleasant surprises.
1/4to2/8. \$\endgroup\$