Dice probability simulator wackyness!

May 6, 2020 at 2:27am
I made this code to run *puts pinky to lower lip* ONE MILLION iterations of a opposed dice pool roll. I added a way to run a set then step up how many dice are rolled each iteration.

I am getting strange percentages if I step from 1 die up to 10 dice that do not jive with running it from 5 to 10.

I will supply more info if needed.

Does this have something to do with the way random numbers are rolled or is my math just wrong somewhere?

THANK YOU!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
#include <iostream>
#include <string>
#include <stdlib.h>
#include <time.h>
using namespace std;

int defDie = 20;

int defPool1 = 3;
int defPool2 = 5;

int defTar1 = 10;
int defTar2 = 10;

int pool1[50];
int pool2[50];

int mod01 = 0;
int mod02 = 0;

int numSucP1 = 0;
int numSucP2 = 0;

int player1Wins = 0;
int player2Wins = 0;

int randNumber()
{
	int randNum = (rand() % defDie) + 1;
	return randNum;
}

void rollPool(int poolNum)
{
	if (poolNum == 1)
	{
		for (int i = 0; i < defPool1; i++)
		{
			pool1[i] = randNumber();
			//cout << pool1[i] << endl;
		}
	}
	if (poolNum == 2)
	{
		for (int i = 0; i < defPool2; i++)
		{
			pool2[i] = randNumber();
			//cout << pool2[i] << endl;
		}
	}
}


void countSuccesses(int poolToCount)
{
	if (poolToCount == 1)
	{
		numSucP1 = 0;
		for (int i = 0; i < defPool1; i++)
		{
			if (pool1[i] >= (defTar1 + mod01))
			{
				numSucP1++;
				//cout << "Curr Suc P1: " << numSucP1 << endl;
			}
		}
	}
	if (poolToCount == 2)
	{
		numSucP2 = 0;
		for (int i = 0; i < defPool2; i++)
		{
			if (pool2[i] >= (defTar2 + mod02))
			{
				numSucP2++;
				//cout << "Curr Suc P2: " << numSucP2 << endl;
			}
		}
	}
}

void getOutcome()
{
	if (numSucP1 > numSucP2)
	{
		//cout << "Player 1 Wins" << endl;
		player1Wins++;
	}
	else if (numSucP1 <= numSucP2)
	{
		//cout << "Player 2 Wins" << endl;
		player2Wins++;
	}
	else
	{
		cout << "Draw!" << endl;
	}
}

void runIter()
{
	for (int i = 0; i < 1000000; i++)
	{
		rollPool(1);
		countSuccesses(1);
		rollPool(2);
		countSuccesses(2);
		getOutcome();
	}
	cout << "Player 1 Wins : " << player1Wins << endl;
	cout << "Player 1 Targ : " << defTar1 + mod01 << endl;
	cout << "Player 2 Wins : " << player2Wins << endl;
	cout << "Player 2 Targ : " << defTar2 + mod02 << endl;
	float winsTotal = player1Wins + player2Wins;
	float winPercP1 = (player1Wins / winsTotal) * 100.00;
	float winPercP2 = (player2Wins / winsTotal) * 100.00;
	cout << "P1 wins: %" << winPercP1 << endl;
	cout << "P2 wins: %" << winPercP2 << endl;
}


int main()
{
	srand((unsigned)time(0));
	for (int i = 0; i < 7; i++)
	{
		cout << "DP P1: " << defPool1 << endl;
		cout << "DP P2: " << defPool2 << endl;
		cout << "Trg P1: " << (defTar1 + mod01) << endl;
		cout << "Trg P2: " << (defTar2 + mod02) << endl;
		runIter();
		defPool1++;
		cout << endl;
	}
	return 0;
}
May 6, 2020 at 2:34am
I am getting strange percentages if I step from 1 die up to 10 dice that do not jive with running it from 5 to 10.

I will supply more info if needed.
Yes, what specifically is the issue? What is weird? Pretend we're stupid and can't see what is obvious.

The first weird thing I see is your use of icky global variables.

In your getOutcome function, your first two if-statements already cover 100% of possibilities.
cout << "Draw!" << endl; is not reachable code.

1
2
	float winPercP1 = (player1Wins / winsTotal) * 100.00;
	float winPercP2 = (player2Wins / winsTotal) * 100.00;
Both player1Wins and winsTotal are ints, so this is doing integer division, most likely rounding to 0.
You want to cast one of the operands to a float.

1
2
	float winPercP1 = (static_cast<float>(player1Wins) / winsTotal) * 100.00;
	float winPercP2 = (static_cast<float>(player2Wins) / winsTotal) * 100.00;
Last edited on May 11, 2020 at 7:43pm
May 6, 2020 at 4:21am
i'd assume bc you're starting off lopsided in one direction..
1
2
3
int defPool1 = 3;
int defPool2 = 5;


then only incrementing defpool1.

1
2
3
   runIter();
        defPool1++;
        cout << endl;


the initial advantage of player 2 isn't really balanced out until "round" 5 but from then on p1 has an advantage.

in summary defpool is representative of the number of rolls so more rolls equals more chances to win?

idk why i feel like this is one of those "find the programmers mistakes" homework assignments.
Last edited on May 6, 2020 at 4:25am
May 6, 2020 at 4:23am
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <iostream>
#include <iomanip>
#include <string>
#include <cstdlib>
#include <ctime>
using namespace std;

const int DieSize = 20;
const int Repetitions = 1000000;

struct Player {
    int defPool;
    int defTar   = 10;
    int pool[50]   {};
    int mod      =  0;
    int numSuc   =  0;
    int wins     =  0;
    Player(int dp) : defPool(dp) { }
};

int randNumber() { return rand() % DieSize + 1; }

void rollPool(Player *players, int n) {
    for (int i = 0; i < players[n].defPool; i++)
        players[n].pool[i] = randNumber();
}

void countSuccesses(Player *players, int n) {
    players[n].numSuc = 0;
    for (int i = 0; i < players[n].defPool; i++)
        if (players[n].pool[i] >= players[n].defTar + players[n].mod)
            players[n].numSuc++;
}

void getOutcome(Player *players, int& draws) {
    if (players[0].numSuc > players[1].numSuc)
        players[0].wins++;
    else if (players[0].numSuc < players[1].numSuc)
        players[1].wins++;
    else
        ++draws;
}

void runIter(Player *players) {
    int draws = 0;
    for (int i = 0; i < Repetitions; i++) {
        for (int j = 0; j < 2; ++j) {
            rollPool(players, j);
            countSuccesses(players, j);
        }
        getOutcome(players, draws);
    }
    
    float winsTotal = players[0].wins + players[1].wins;
    cout << fixed << setprecision(1);
    for (int i = 0; i < 2; ++i) {
        float winPerc = (players[i].wins / winsTotal) * 100.00;
        cout << setw(3) <<  players[i].defPool << ' '
             << setw(3) << (players[i].defTar + players[i].mod) << ' '
             << setw(8) <<  players[i].wins << ' '
             << setw(5) <<  winPerc << '\n';
    }
    cout << setw(16) << draws << '\n';
}

int main() {
    srand(time(nullptr));
    Player players[2] = { Player(3), Player(5) };
    cout << " DP TRG     WINS   %\n";
    for (int i = 0; i < 7; i++) {
        runIter(players);
        players[0].defPool++;
    }
}

May 6, 2020 at 3:32pm
Well I jest and call me Sherly! Well hell man let me see if I get this.

Why did you:
Set time(nullptr)


You created a data structure for all the variables and loaded it into a object array?

In the struct you have this "Player(int dp) : defPool(dp) { }" it is shorthand for a function that sets the dice pool? Is there a way to have one for the pool and one for the modifier?

This "int pool[50] {};" curly brackets initialize the array?

What is setw()? Edit: Figured this one out. Space holder.

Edit:
New question. When passing the struct its "Player* players":
Are we specifying that it is the struct "Player" then points to the players object array?
Last edited on May 6, 2020 at 4:07pm
May 11, 2020 at 3:27pm
Thank you for the rewrite. Learned some stuff. Did some new things. All in all, stuff and things!
May 11, 2020 at 4:08pm
If you want random numbers that are not biased stop using the C library. For C++ code you should be using <random>.

https://web.archive.org/web/20180123103235/http://cpp.indi.frih.net/blog/2014/12/the-bell-has-tolled-for-rand/
May 11, 2020 at 6:28pm
I admit I am not paying too much attention, but the first thing that struck me is that you are using a biased method to get numbers. Furry Guy is correct in pointing this out.

rand() will always give you a number in [0, 2**n-1], which is usually 0..32767 — not friendly for using remainder. To understand this, think about getting a number in a much more restricted range: 0..7.

If you were to want only three values ([0,7] % 3), you would have:

  • 3 chances to get a 0: {0, 3, 6} % 3
  • 3 chances to get a 1: {1, 4, 7} % 3
  • 2 chances to get a 2: {2, 5} % 3 ← ONLY TWO CHANCES!!??

Yes. This is called the “Pigeonhole Principle”, and it is the reason that using remainders on rand() are a “don’t do that” kind of thing. (Even though people have been doing it since rand() was created.)

The correct way to fix this is to simply ignore values that are too big for a proper remainder. This requires only a very little math:

  x = rand();
  while (x >= (RAND_MAX / range * range))
    x = rand();

(Compiler issues and edge cases omitted.)

The easiest way is to just use C++’s modern RNG facilities, even if it was designed with really convoluted, hard-to-remember syntax. I find a little function helps.

1
2
3
4
5
long random( long min, long max )
{
  static std::ranlux24 rng( std::chrono::system_clock::now().time_since_epoch().count() );
  return std::uniform_int_distribution<long>( min, max )( rng );
}

Now you can get your perfectly distributed RGNs without any fuss:

 
  int die_roll = random( 1, 6 );

Hope this helps.

(Disclaimer: code typed off the top of my head, meaning I may have typoed or otherwise erred.)
May 11, 2020 at 7:13pm
And in case anyone questions the "loop infinitely until you get the required number" logic, I also had this concern, but it appears that the Boost random libraries (which eventually become C++11 libraries) also do a slightly more sophisticated method of infinitely looping until it fits as well, assuming I'm understanding the source correctly:
https://www.boost.org/doc/libs/1_67_0/boost/random/uniform_int_distribution.hpp
(generate_uniform_int contains the logic)

I wonder if there's any way to put an upper bound on the number of times you need to loop without it being egregiously slow.
May 11, 2020 at 7:36pm
1
2
	float winPercP1 = (player1Wins / winsTotal) * 100.00;
	float winPercP2 = (player2Wins / winsTotal) * 100.00;



Both player1Wins and winsTotal are ints, so this is doing integer division, most likely rounding to 0. You want to cast one of the operands to a float.
WinsTotal is a float, so player1Wins and player2Wins will be cast to floats automatically.
May 11, 2020 at 7:43pm
Yep, my mistake. I clearly am delusional, because I thought I remembered doubled checking that before posting, and saw it as an int.
May 12, 2020 at 3:14am
Ganado wrote:
I wonder if there's any way to put an upper bound on the number of times you need to loop without it being egregiously slow.

There is no point in adding extra logic. The RNG is (supposedly) producing normalized samples, and even if it isn’t, the chances of getting stuck past the top end of a properly-constructed ‘accept’ range is zero, barring a broken RNG (of which every implementation of rand() is not).

Of course, you can break it yourself by constructing a poor ‘accept’ range. By “properly-constructed” I do mean as I listed in the math above, which will cover at minimum half the possible sample values. The infinite loop shouldn’t actually loop more than one or two times for the vast majority of all pulls in that edge case — zero times for normal cases.

Hope this helps.
Topic archived. No new replies allowed.