6

Working on another project and we're required to use the mt19937 for randomly generating numbers. We're supposed to have it randomly choose an x and y coordinate based on the section of a grid. For example, my function passes minX, maxX, minY, maxY to a function. My x coordinate works fine. I kept getting errors randomly upon test runs. Sometimes it'll run 10 times with no problem, then hits an error. I put in some self debug lines to display what the mt generator is actually generating. Like I said, x works fine and y does sometimes. It'll randomly give me a -3437892 or 9743903.

Here's my code:

void DungeonLevel::generateRoom(int minX,int maxX,int minY, int maxY){
    mt19937 mt;
    mt.seed( time(NULL) );


    // Calculate random width and height; these both range
    // from 4-13
    int iRandomWidth = 4 + (mt() % 10);
    int iRandomHeight = 4 + (mt() % 10);

    // Calculate the start points in both X and Y directions

    int iStartX;
    iStartX = mt() % (maxX - iRandomWidth);
    cout << "xStart: " << iStartX<<endl; //cout flag
    while ((iStartX > maxX) && (iStartX >= 0)){
            cout << "xStart: " << iStartX<<endl;//cout flag
            iStartX = mt() % (maxX - iRandomWidth);
    }
    int iStartY = 0;
    iStartY = mt() % (maxY - iRandomHeight);
    cout<<"yStart: " <<iStartY<<endl; //cout flag
    while ((iStartY > maxY)){
            cout<<"yStart: " <<iStartY<<endl;//cout flag
            iStartY = (mt() % (maxY - iRandomHeight));
    }

    // Iterate through both x and y coordinates, and
    // set the tiles to room tiles
    // SINGLE ROOM
    for( int x = iStartX; x <= iStartX + iRandomWidth; x++ ){
            for( int y = iStartY; y <= iStartY + iRandomHeight; y++ ){
                    if (y == iStartY){
                            dungeonGrid[y][x] = '-';
                    }
                    else if (iStartX == x){
                            dungeonGrid[y][x] = '|';
                    }
                    else if (y == (iStartY+iRandomHeight)){
                            dungeonGrid[y][x] = '-';
                    }
                    else if (x == (iStartX+iRandomWidth)){
                            dungeonGrid[y][x] = '|';
                    }
                    else {
                            dungeonGrid[y][x] = '.';
                    }

            }
    }

}
Rapptz
  • 20,807
  • 5
  • 72
  • 86
ModdedLife
  • 669
  • 2
  • 11
  • 24

3 Answers3

16

I think you should use a random distribution for the mt19937. So use it with

mt19937 mt;
mt.seed( time(nullptr) );
std::uniform_int_distribution<int> dist(4, 13);

int iRandomWidth = dist(mt);
int iRandomHeight = dist(mt);

This way you are guaranteed to get random numbers between 4 and 13.

Update While my answer solves the original problem and is in my opinion an improvement in code readability, it does not actually address the problem in the original code. Please also refer to jogojapan's answer for this.

Haatschii
  • 9,021
  • 10
  • 58
  • 95
  • Exactly this. I can't see where the error is in OP's code, but there are many problems with `%n` for generating random numbers up to `n`, and furthermore using distributions makes the code a **lot** cleaner. – us2012 Feb 26 '13 at 03:12
  • I agree with the recommendation, but why use a `uint32_t` distribution to assign values to `(signed) int` variables? – jogojapan Feb 26 '13 at 03:37
  • @jogojapan: I choosed `uint32_t` because all values created by the distribution are `>=0`. However `int32_t` or `int` should be just as good. You'd even save an implicit cast to signed int, but I don't think this matters, will it? – Haatschii Feb 26 '13 at 03:42
  • 1
    @Haatschii It doesn't matter for the results of this specific calculation, but as soon as `intDist(mt)` becomes part of more complex calculations, it introduces an unsigned data type where you wouldn't expect it; or vice versa, the `int` type on the receiving end introduces signedness (and potential overflows) where you wouldn't expect it. This can cause precisely the kind of problem that the OP seems to be experiencing. – jogojapan Feb 26 '13 at 03:47
  • @jogojapan: Ok, I changed it accordingly. However can you give an example where it would break? I would think that as long as the boundarys of the distribution fit into `int32_t` there should be no extra overflows when using `uint32_t`. But I am not that experienced in this topic. – Haatschii Feb 26 '13 at 03:57
  • @Haatschii What I mean is something like this: Suppose instead of `4` you'd have a variable that could sometimes be negative. Adding it to the unsigned data type returned by `intDist()` would cause unexpected results in places you wouldn't expect: [example](http://liveworkspace.org/code/D0dU7$3). (Assigning it back to a signed int would remedy this, but what if that int had been declared using `auto`? Or is itself part of a function call into more complex code?) – jogojapan Feb 26 '13 at 04:06
  • @us2012: Given a decent RNG (such as mt19937), `%10` is good enough for `DungeonLevel::generateRoom` – MSalters Feb 26 '13 at 18:53
  • @Haatschii: I implemented that and it definitely made it better. Instead of iStartY being some huge negative number, its saying "floating exception (core dumped)." But it happens a lot less often. – ModdedLife Feb 26 '13 at 18:53
  • @ModdedLife Well, that's quite bad still, but probably not a result of the random number generator. Run your code through a debugger like `gdb` to find out where the FP exceptions occur. – us2012 Feb 26 '13 at 18:57
  • 2
    @ModdedLife From the code you posted, I would guess that you have a division by zero, for example in `iStartX = mt() % (maxX - iRandomWidth);` (and the same line with Y). What is `maxX`? Because if `maxX - iRandomWidth` might be zero it would cause this error. – Haatschii Feb 26 '13 at 21:53
  • @Haatschii that could actually be it. Wow I feel dumb lol. Let me check it. – ModdedLife Feb 27 '13 at 01:40
  • @Haatschii: That was it! Thanks! I can't believe I missed that. I changed it but setting the starting value of maxY to "99999999" and added a loop to make sure that (maxY - iRandomHeight) > 0. Ill update the code. Thanks again. – ModdedLife Feb 27 '13 at 02:39
  • @Haatschii: The results of `std::mt19937` are guaranteed to nonnegative and uniformly distributed within the range 0 to 2^32 -1. The result type of `std::mt19937` is `uint_fast32_t`, which is either unsigned 4 byte integer or unsigned 8 byte integer on 64 bit machines. In your answer "I don't think it's guaranteed to be positive and uniformly distributed" IMO should be refined because something very close to this is true. The results aren't IID of course, but if you seed with at least 32 bits of entropy, then for any n the `n`'th draw from the generator is uniformly distributed. – Chris Beck Mar 13 '16 at 14:26
  • @ChrisBeck: Do you have a reference for that? I was looking for clarification on the guarantees for the () operator, but couldn't find anything. – Haatschii Mar 13 '16 at 16:24
  • So there's two places to look. For the stuff about `uint_fast32_t` that is in the standard. For the part about entropy -- you can look in the paper that invented mt19937 for the precise statistical properties. As a pen and paper thing -- most RNGs, mt19937 included, are based on having some internal state, and then implying an invertible transformation to that state repeatedly, and leaking part of the state each round as an ouptut. For instance `rand` works this way. For any such generator, if the state is totally initialized with pure enntropy, the first draw, and any particular draw, is also – Chris Beck Mar 13 '16 at 16:33
  • I realized that my claim isn't quite right because, while `rand` doesn't actually have much more state than a single draw, mt19937 has a pretty large internal state, like 2k. So if you seed it with 2k of pure entropy, you'll know trivially that the first draw is pure entropy also. But that's not what happens if you seed with only 32 bits of pure entropy. – Chris Beck Mar 13 '16 at 16:34
  • However, the original claim of the paper is that mersenne twister is "623-dimensional equidistributed". If you read the paper, this means that if you seed the generator totally with pure entropy, and then draw an entire period of the generator (2^19937 draws), any particular 623 bits of the output are uniform iid. It's implied, but not formally proven, that even for much larger sizes there are at most very small correlations among the bits. (However, the generator is not cryptographically secure. If you get enough draws you can determine the whole state efficiently.) – Chris Beck Mar 13 '16 at 16:39
  • Usually the way these things work is, if you don't have a seed of length 2k, e.g. you have a seed seq, or just `time(null)`, what will happen is, there is a bootstrap random generator that is used to stretch that seed to size 2k, and then that is used to initialize mt19937. It would be extremely difficult afaik, to answer a question like "what if I only seed it with 32 bits of pure entropy, how much entropy am I guaranteed to get back in the first draw?", the answer is, probably not all of it, and it depends on the bootstrap generator and internal nature of mt19937 – Chris Beck Mar 13 '16 at 16:41
  • @ChrisBeck Thanks for the clarification. So for common use it can be assumed that the () operator returns uniformly distributed integers, right? I'll remove my guess from the answer, however feel free to edit it further if like. – Haatschii Mar 13 '16 at 18:27
  • Yes, you are "supposed" to think of it as though it is giving you a 32 bit integer uniformly at random each time you draw. – Chris Beck Mar 13 '16 at 18:32
5

The ultimate cause of the problem is that you mix signed and unsigned integers in the code without taking the necessary precautions (and without need).

Specifically, if minY is sometimes less than 13, it will happen every now and then that iRandomHeight gets negative. What you then get is similar to the effect demonstrated below:

#include <limits>
#include <iostream>

using namespace std;

int main()
{
  /* Unsigned integer larger than would fit into a signed one.
     This is the kind of thing mt199737 returns sometimes. */
  unsigned int i = ((unsigned int)std::numeric_limits<int>::max()) + 1000;
  cout << (i % 3) << endl;
  cout << (i % -3) << endl;
  cout << (signed)(i % -3) << endl;
  return 0;
}

This first generates an unsigned integer slightly larger than would fit into a signed one. mt19937 returns unsigned and will sometimes give you values like i in the code above.

The output of the code above (see on liveworkspace) is as follows:

2
2147484647
-2147482649

The second line shows the result of modulo with a negative number (such as iRandomHeight will sometime be), applied to an unsigned integer larger than fits into a corresponding signed one. The third line shows what happens when you convert that back to signed integer (which you do implicitly when you assign it to your one of your signed integer variables).

Although I agree with Haatschii that you should use std::uniform_int_distribution to make your life easier, it's also important to use signed and unsigned appropriately even then.

jogojapan
  • 68,383
  • 11
  • 101
  • 131
  • This is the real answer IMO. `std::uniform_int_distribution` isn't always the right choice, it has some undesirable properties. – Chris Beck Mar 13 '16 at 14:27
0

Figured out the amateur mistake I made with the help of @haatschii.

It makes so much sense now. iStartY and iStartX had no limitation for being set to a number below or equal to zero.. I feel so dumb for not catching that lol. I added another loop to make sure that the value was greater than 0. I also made the iStartX and iStartY values start out as maxX+1 and maxY+1 so that they would automatically enter the loop to generate a solution greater than 0.

Heres the solution code:

void DungeonLevel::generateRoom(int minX,int maxX,int minY, int maxY){
    mt19937 mt;
    mt.seed( time(NULL) );

    // Calculate random width and height; these both range
    // from 4-13
    int iRandomWidth = 4 + (mt() % 10);
    int iRandomHeight = 4 + (mt() % 10);

    int iStartX = maxX+1; //automatically has to enter the second while loop        
    while ((iStartX > maxX) && (iStartX >= 0)){
            while ((maxX - iRandomWidth) <= 0){
                    iRandomHeight = 4 + (mt() % 10); //makes value > 0
            }
            iStartX = mt() % (maxX - iRandomWidth);
    }

    int iStartY = maxY+1; //automatically has to enter the second loop
    while ((iStartY > maxY)){
            while ((maxY - iRandomHeight) <= 0){
                    iRandomHeight = 4 + (mt() % 10); //sets to valid value
            }
            iStartY = mt() % (maxY - iRandomHeight);
    }
    // Iterate through both x and y coordinates, and
    // set the tiles to room tiles
    // SINGLE ROOM
    for( int x = iStartX; x <= iStartX + iRandomWidth; x++ ){
            for( int y = iStartY; y <= iStartY + iRandomHeight; y++ ){
                    if (y == iStartY){
                            dungeonGrid[y][x] = '-';
                    }
                    else if (iStartX == x){
                            dungeonGrid[y][x] = '|';
                    }
                    else if (y == (iStartY+iRandomHeight)){
                            dungeonGrid[y][x] = '-';
                    }
                    else if (x == (iStartX+iRandomWidth)){
                            dungeonGrid[y][x] = '|';
                    }
                    else {
                            dungeonGrid[y][x] = '.';
                    }

            }
    }

}

Thanks for the tips guys!

ModdedLife
  • 669
  • 2
  • 11
  • 24