简体   繁体   中英

How to produce a random number sequence that doesn't produce more than X consecutive elements

Ok, I really don't know how to frame the question properly because I barely have any idea how to describe what I want in one sentence and I apologize.

Let me get straight to the point and you can just skip the rest cause I just want to show that I've tried something and not coming here to ask a question on a whim.

I need an algorithm that produces 6 random numbers where it may not produce more than 2 consecutive numbers in that sequence.

example: 3 3 4 4 2 1

^FINE.

example: 3 3 3 4 4 2

^ NO! NO! WRONG!

Obviously, I have no idea how to do this without tripping over myself constantly.

Is there a STL or Boost feature that can do this? Or maybe someone here knows how to concoct an algorithm for it. That would be awesome.

What I'm trying to do and what I've tried. (the part you can skip)

This is in C++. I'm trying to make a Panel de Pon/Tetris Attack/Puzzle League whatever clone for practice. The game has a 6 block row and 3 or more matching blocks will destroy the blocks. Here's a video in case you're not familiar.

When a new row comes from the bottom it must not come out with 3 horizontal matching blocks or else it will automatically disappear. Something I do not want for horizontal. Vertical is fine though.

I've tried to accomplish just that and it appears I can't get it right. When I start the game chunks of blocks are missing because it detects a match when it shouldn't. My method is more than likely heavy handed and too convoluted as you'll see.

enum BlockType {EMPTY, STAR, UP_TRIANGLE, DOWN_TRIANGLE, CIRCLE, HEART, DIAMOND};
vector<Block> BlockField::ConstructRow()
{
    vector<Block> row;

    int type = (rand() % 6)+1;

    for (int i=0;i<6;i++)
    {
        row.push_back(Block(type));
        type = (rand() % 6) +1;
    }

    // must be in order from last to first of the enumeration
    RowCheck(row, diamond_match);
    RowCheck(row, heart_match);
    RowCheck(row, circle_match);
    RowCheck(row, downtriangle_match);
    RowCheck(row, uptriangle_match);
    RowCheck(row, star_match);

    return row;
}

void BlockField::RowCheck(vector<Block> &row, Block blockCheckArray[3])
{
    vector<Block>::iterator block1 = row.begin();
    vector<Block>::iterator block2 = row.begin()+1;
    vector<Block>::iterator block3 = row.begin()+2;
    vector<Block>::iterator block4 = row.begin()+3;
    vector<Block>::iterator block5 = row.begin()+4;
    vector<Block>::iterator block6 = row.begin()+5;

    int bt1 = (*block1).BlockType();
    int bt2 = (*block2).BlockType();
    int bt3 = (*block3).BlockType();
    int bt4 = (*block4).BlockType();
    int type = 0;

    if (equal(block1, block4, blockCheckArray)) 
    {
        type = bt1 - 1;
        if (type <= 0) type = 6;
        (*block1).AssignBlockType(type);
    }
    else if (equal(block2, block5, blockCheckArray)) 
    {
        type = bt2 - 1;
        if (type <= 0) type = 6;
        (*block2).AssignBlockType(type);
    }
    else if (equal(block3, block6, blockCheckArray)) 
    {
        type = bt3 - 1;
        if (type == bt3) type--;
        if (type <= 0) type = 6;
        (*block3).AssignBlockType(type);
    }
    else if (equal(block4, row.end(), blockCheckArray)) 
    {
        type = bt4 - 1;
        if (type == bt3) type--;
        if (type <= 0) type = 6;

        (*block4).AssignBlockType(type);
    }
}

Sigh , I'm not sure if it helps to show this...At least it shows that I've tried something.

Basically, I construct the row by assigning random block types, described by the BlockType enum, to a Block object's constructor(a Block object has blockType and a position).

Then I use a RowCheck function to see if there's 3 consecutive blockTypes in one row and I have do this for all block types. The *_match variables are arrays of 3 Block objects with the same block type. If I do find that there are 3 consecutive block types then, I just simply subtract the first value by one. However if I do that I might end up inadvertently producing another 3 match so I just make sure the block types are going in order from greatest to least.

Ok, it's crappy, it's convoluted and it doesn't work. That's why I need your help.

Idea no 1.

while(sequence doesn't satisfy you)
      generate a new sequence 

Idea no 2.

Precalculate all allowable sequences (there are about ~250K of them) 
randomly choose an index and take that element.

The second idea requires much memory, but is fast. The first one isn't slow either because there is a veeery little probability that your while loop will iterate more than once or twice. HTH

It should suffice to keep record of the previous two values, and loop when the newly generated one matches both of the previous values.

For an arbitrary run length, it would make sense to size a history buffer on the fly and do the comparisons in a loop as well. But this should be close to matching your requirements.

int type, type_old, type_older;

type_older = (rand() % 6)+1;
row.push_back(Block(type_older));

type_old = (rand() % 6)+1;
row.push_back(Block(type_old));

for (int i=2; i<6; i++)
{
    type = (rand() % 6) +1;
    while ((type == type_old) && (type == type_older)) {
        type = (rand() % 6) +1;
    }

    row.push_back(Block(type));
    type_older = type_old;
    type_old = type;
}

Most solutions seen so far involve a potentially infinite loop. May I suggest a different approch?

// generates a random number between 1 and 6
// but never the same number three times in a row
int dice()
{
    static int a = -2;
    static int b = -1;
    int c;
    if (a != b)
    {
        // last two were different, pick any of the 6 numbers
        c = rand() % 6 + 1;
    }
    else
    {
        // last two were equal, so we need to choose from 5 numbers only
        c = rand() % 5;
        // prevent the same number from being generated again
        if (c == b) c = 6;
    }
    a = b;
    b = c;
    return c;
}

The interesting part is the else block. If the last two numbers were equal, there is only 5 different numbers to choose from, so I use rand() % 5 instead of rand() % 6 . This call could still produce the same number, and it also cannot produce the 6, so I simply map that number to 6.

Solution with simple do-while loop (good enough for most cases):

vector<Block> row;

int type = (rand() % 6) + 1, new_type;
int repetition = 0;

for (int i = 0; i < 6; i++)
{
    row.push_back(Block(type));
    do {
        new_type = (rand() % 6) + 1;
    } while (repetition == MAX_REPETITION && new_type == type);

    repetition = new_type == type ? repetition + 1 : 0;
    type = new_type;
}

Solution without loop (for those who dislike non-deterministic nature of previous solution):

vector<Block> row;

int type = (rand() % 6) + 1, new_type;
int repetition = 0;

for (int i = 0; i < 6; i++)
{
    row.push_back(Block(type));

    if (repetition != MAX_REPETITION)
        new_type = (rand() % 6) + 1;
    else
    {
        new_type = (rand() % 5) + 1;
        if (new_type >= type)
            new_type++;
    }

    repetition = new_type == type ? repetition + 1 : 0;
    type = new_type;
}

In both solutions MAX_REPETITION is equal to 1 for your case.

How about initializing a six element array to [1, 2, 3, 4, 5, 6] and randomly interchanging them for awhile? That is guaranteed to have no duplicates.

Lots of answers say "once you detect Xs in a row, recalculate the last one until you don't get an X".... In practice for a game like this, that approach is millions of times faster than you need for "real-time" human interaction, so just do it!

But, you're obviously uncomfortable with it and looking for something more inherently "bounded" and elegant. So, given you're generating numbers from 1..6, when you detect 2 Xs you already know the next one could be a duplicate, so there are only 5 valid values: generate a random number from 1 to 5, and if it's >= X, increment it by one more.

That works a bit like this:

1..6 -> 3
1..6 -> 3
"oh no, we've got two 3s in a row"
1..5 -> ?
        < "X"/3   i.e. 1, 2       use as is
        >= "X"         3, 4, 5,   add 1 to produce 4, 5 or 6.

Then you know the last two elements differ... the latter would take up the first spot when you resume checking for 2 elements in a row....

vector<BlockType> constructRow()
{
    vector<BlockType> row;

    row.push_back(STAR); row.push_back(STAR);
    row.push_back(UP_TRIANGLE); row.push_back(UP_TRIANGLE);
    row.push_back(DOWN_TRIANGLE); row.push_back(DOWN_TRIANGLE);
    row.push_back(CIRCLE); row.push_back(CIRCLE);
    row.push_back(HEART); row.push_back(HEART);
    row.push_back(DIAMOND); row.push_back(DIAMOND);

    do
    {
        random_shuffle(row.begin(), row.end());
    }while(rowCheckFails(row));

    return row;
}

The idea is to use random_shuffle() here. You need to implement rowCheckFails() that satisfies the requirement.

EDIT

I may not understand your requirement properly. That's why I've put 2 of each block type in the row. You may need to put more.

I think you would be better served to hide your random number generation behind a method or function. It could be a method or function that returns three random numbers at once, making sure that there are at least two distinct numbers in your output. It could also be a stream generator that makes sure that it never outputs three identical numbers in a row.

int[] get_random() {
    int[] ret;
    ret[0] = rand() % 6 + 1;
    ret[1] = rand() % 6 + 1;
    ret[2] = rand() % 6 + 1;

    if (ret[0] == ret[1] && ret[1] == ret[2]) {
        int replacement;
        do {
            replacement = rand() % 6 + 1;
        } while (replacement == ret[0]);
        ret[rand() % 3] = replacement;
    }
    return ret;
}

If you wanted six random numbers (it's a little difficult for me to tell, and the video was just baffling:) then it'll be a little more effort to generate the if condition:

for (int i=0; i<4; i++) {
    if (ret[i] == ret[i+1] && ret[i+1] == ret[i+2])
        /* three in a row */

If you always change ret[1] (the middle of the three) you'll never have three-in-a-row as a result of the change, but the output won't be random either: XYX will happen more often than XXY because it can happen by random chance and by being forced in the event of XXX .

First some comments on the above solutions.

  1. There is nothing wrong with the techniques that involve rejecting a random value if it isn't satisfactory. This is an example of rejection sampling, a widely used technique. For example, several algorithms for generating a random gaussian involve rejection sampling. One, the polar rejection method, involves repeatedly drawing a pair of numbers from U(-1,1) until both are non-zero and do not lie outside the unit circle. This throws out over 21% of the pairs. After finding a satisfactory pair, a simple transformation yields a pair of gaussian deviates. (The polar rejection method is now falling out of favor, being replaced by the ziggurat algorithm. That too uses a rejection sampling.)

  2. There is something very much wrong with rand() % 6 . Don't do this. Ever. The low order bits from a random number generator, even a good random number generator, are not quite as "random" as are the high order bits.

  3. There is something very much wrong with rand() , period. Most compiler writers apparently don't know beans about producing random numbers. Don't use rand() .

Now a solution that uses the Boost random number library:

vector<Block> BlockField::ConstructRow(
  unsigned int max_run) // Maximum number of consecutive duplicates allowed
{
  // The Mersenne Twister produces high quality random numbers ...
  boost::mt19937 rng;

  // ... but we want numbers between 1 and 6 ...
  boost::uniform_int<> six(1,6);

  // ... so we need to glue the rng to our desired output.
  boost::variate_generator<boost::mt19937&, boost::uniform_int<> >
    roll_die(rng, six);

  vector<Block> row;

  int prev = 0;
  int run_length = 0;

  for (int ii=0; ii<6; ++ii) {
    int next;
    do {
      next = roll_die();
      run_length = (next == prev) ? run_length+1 : 0;
    } while (run_length > max_run);
    row.push_back(Block(next));
    prev = next;
  }

  return row;
}

I know that this already has many answers, but a thought just occurred to me. You could have 7 arrays, one with all 6 digits, and one for each missing a given digit. Like this:

int v[7][6] = {
    {1, 2, 3, 4, 5, 6 },
    {2, 3, 4, 5, 6, 0 }, // zeros in here to make the code simpler, 
    {1, 3, 4, 5, 6, 0 }, // they are never used
    {1, 2, 4, 5, 6, 0 },
    {1, 2, 3, 5, 6, 0 },
    {1, 2, 3, 4, 6, 0 },
    {1, 2, 3, 4, 5, 0 }
};

Then you can have a 2 level history. Finally to generate a number, if your match history is less than the max, shuffle v[0] and take v[0][0]. Otherwise, shuffle the first 5 values from v[n] and take v[n][0]. Something like this:

#include <algorithm>

int generate() {
    static int prev         = -1;
    static int repeat_count = 1;

    static int v[7][6] = {
        {1, 2, 3, 4, 5, 6 },
        {2, 3, 4, 5, 6, 0 }, // zeros in here to make the code simpler, 
        {1, 3, 4, 5, 6, 0 }, // they are never used
        {1, 2, 4, 5, 6, 0 },
        {1, 2, 3, 5, 6, 0 },
        {1, 2, 3, 4, 6, 0 },
        {1, 2, 3, 4, 5, 0 }
    };

    int r;

    if(repeat_count < 2) {
        std::random_shuffle(v[0], v[0] + 6);
        r = v[0][0];
    } else {
        std::random_shuffle(v[prev], v[prev] + 5);
        r = v[prev][0];
    }

    if(r == prev) {
        ++repeat_count;
    } else {
        repeat_count = 1;
    }

    prev = r;   
    return r;
}

This should result in good randomness (not reliant of rand() % N ), no infinite loops, and should be fairly efficient given the small amount of numbers that we are shuffling each time.

Note, due to the use of statics, this is not thread safe , that may be fine for your usages, if it is not, then you probably want to wrap this up in an object, each with its own state.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM