Calculating the constant C in Dota 2 pseudo-random distribution
The probability of a modifier occurring on the Nth attack since the last successful modifier is given as P(N) = C * N. C is a constant derived from the expected probability of the modifier occurring. C serves as both the initial probability of the modifier and the increment by which it increases every time the modifier fails. When P(N) becomes greater than 1.00, the modifier will always succeed.
Source: http://dota2.gamepedia.com/Pseudo-random_distribution
My question is how can I calculate the C
constant? I am ready to count/parse events in game.
Solution 1:
If you know about Markov chains you can basically solve the P(E) for any C value. You need to think about a bunch of states {1,2,3...}, which correspond to how many "stacks" of C you've accumulated.
So you have a matrix P where the nth row and kth column holds the probability that you go from n stacks to k stacks. However, the only valid transitions are:
- From n stacks to 1 stack when you proc and it resets.
- From n stacks to n+1 stacks when you don't proc.
The matrix is of finite size because once you reach n = ⌈1/C⌉
(rounded up) stacks, the probability of going back to 1 stack is 100%.
Now with your A matrix constructed, you can compute what is known as a stationary distribution, where aX = a, where a is a vector and X is a matrix. If you have Matlab you can solve for a by taking the eigenvector of the eigenvalue 1. This eigenvector represents the proportion of time spent in each state, so by dividing it by the sum of all the values, you obtain a long term proportion of time spent in each state.
Once you know the probability of being in each state you can multiply it by the probability of a proc in that state and sum it across states to get the average proc rate. Then you just try different C values until you achieve desired P(E).
Below is some Matlab code I put together, the values are not exactly the same as the wiki page by they are sufficiently similar that I have not put more thought into it.
function [C, prob] = pseudorand(target) % Takes in a target p and outputs the C value
C = 0.30210; % Initial points for iterating
prob = 0.5;
while abs(target-prob)>10^-8 % Iterate and adjust C until desire p is
achieved.
if C<0.1
C = C + (target-prob)/8; % Be more careful around small C values
else
C = C + (target-prob)/2;
end
P = zeros(ceil(1/C)); % Determine the size of the matrix.
for n = 1:size(P,1) % Set values of first column to n*C, rounding
if n*C < 1 down to 1 on the final row.
P(n,1) = n*C;
else
P(n,1) = 1;
end
end
for n = 1:size(P,1)-1
P(n,n+1) = 1-P(n,1); % Populating entries of nth row and (n+1)th
end column with value 1-n*C (once again with
rounding on the last row).
[v, d] = eig(P'); % Obtain eigenvectors and eigenvalues.
stationary = v(:,1)/sum(v(:,1));% Take first eigenvector and normalise.
prob = stationary'*P(:,1); % Find P(E) for corresponding C value.
end
EDIT (response to comment below): I'm sorry but I don't quite understand what you mean by "testing values". This process rests on the assumption that the pseudo-random probability satisfies p = n × C. The assumption is that the Warcraft 3 developers wrongly calculated the C values which lead to a inconsistency between expected (in the tooltip) and observed. So the true values can either be found by taking the observed (through parsing) probability to calculate the corresponding C value using my algorithm or you could somehow acquire the table used for C values in the source code, which is from where I assume the wiki got them from.
The other solution would to be parse in a way that discriminates between the number of misses experienced, for example you can calculate the frequency of procs after 2,3,4 non-procs then fit a straight line across the points to determine the linear growth of the C value. This method is probably the most cumbersome but provides some good granularity and also gives some indictation as to whether or not the probability is indeed p = n × C (ignoring the point where n × C goes above 1, the values should pretty much sit in a straight line).
Solution 2:
http://www.playdota.com/forums/showthread.php?t=7993
Maybe this thread will help you. A "numerical" approach is mentioned but unfortunately I cannot find the source of it. If you want to get the actual values of C you have to test them for various %-based skills. Maybe all the percentages are not represented so you might have to infer the other values for C, but it's actually pointless if there are no skills/items corresponding.
For example, if you want to have the actual C for P(E)=15%, you take Coup de Grâce, PA's ultimate and you measure the number of time you crit right after a crit divided by the number of attacks you made after a crit. You should find a value close to 3.22% like stated in the wiki. In the playdota thread it is mentioned they have done a million attacks.
I'm not sure whether the "theoretical C" is relevant here if you are able to do some testing you should find the actual C and that is the only one that matters.
Solution 3:
user72955's iterative approach is fundamentally along the right lines, but you could run into problems if you ported that Matlab code to other systems with different floating point precision, or if you wanted the numbers calculated to the limits of precision of your programming environment.
In general, when you are testing for convergence on a system where floating point numbers are inexactly represent by binary formats, you will not want to make the comparison between expected and actual with an arbitrary error value as in that code where it tests abs(target-prob)>10^-8. Testing this way is prone to getting caught in an infinite loop if the iteration converges to a value that is not as accurate as your are guessing it will be. In the code I give below, I test the outputs of consecutive iterations for convergence to 0 as my terminating condition, rather than latest output vs expect value, to avoid this issue. Because the calculation of P from C is deterministic, and precision the number format is finite, it will always converge to a point where consecutive outputs are identical and as close as possible to the expected value, without the possibility of getting caught in a cycle or failing to converge due to binary representation inaccuracies.
The second issue I have with that solution is the incrementing of C by adding an arbitrary percentage of the output error. This is not a linear gain system where additive negative feedback will quickly and accurately converge, as the poster realized when they had to put in the divide-by-8 fudge factor for smaller increments for low values of C. If more precision were asked for, more fudge factors would be needed to keep the system convergent. It's better not to make assumptions about the relation of the output to the input, and adjust your input by an interval halving approach.
Here is a solution in C# code that will converge to the limit of the precision of the decimal format type. The 'm' after numbers just tells the compiler they are decimal format literals, as opposed to floats or doubles.
public decimal CfromP( decimal p )
{
decimal Cupper = p;
decimal Clower = 0m;
decimal Cmid;
decimal p1;
decimal p2 = 1m;
while(true)
{
Cmid = ( Cupper + Clower ) / 2m;
p1 = PfromC( Cmid );
if ( Math.Abs( p1 - p2 ) <= 0m ) break;
if ( p1 > p )
{
Cupper = Cmid;
}
else
{
Clower = Cmid;
}
p2 = p1;
}
return Cmid;
}
private decimal PfromC( decimal C )
{
decimal pProcOnN = 0m;
decimal pProcByN = 0m;
decimal sumNpProcOnN = 0m;
int maxFails = (int)Math.Ceiling( 1m / C );
for (int N = 1; N <= maxFails; ++N)
{
pProcOnN = Math.Min( 1m, N * C ) * (1m - pProcByN);
pProcByN += pProcOnN;
sumNpProcOnN += N * pProcOnN;
}
return ( 1m / sumNpProcOnN );
}
CfromP() is the main function you call, and PfromC is a helper function that forward computes P (the easy direction to go).
It outputs this:
C(0.05) = 0.003801658303553139101756466
C(0.10) = 0.014745844781072675877050816
C(0.15) = 0.032220914373087674975117359
C(0.20) = 0.055704042949781851858398652
C(0.25) = 0.084744091852316990275274806
C(0.30) = 0.118949192725403987583755553
C(0.35) = 0.157983098125747077557540462
C(0.40) = 0.201547413607754017070679639
C(0.45) = 0.249306998440163189714677100
C(0.50) = 0.302103025348741965169160432
C(0.55) = 0.360397850933168697104686803
C(0.60) = 0.422649730810374235490851220
C(0.65) = 0.481125478337229174401911323
C(0.70) = 0.571428571428571428571428572
C(0.75) = 0.666666666666666666666666667
C(0.80) = 0.750000000000000000000000000
C(0.85) = 0.823529411764705882352941177
C(0.90) = 0.888888888888888888888888889
C(0.95) = 0.947368421052631578947368421
Solution 4:
This isn't adding any new thought to this discussion, but here's the above C# code translated to Python. I also added a function to test a value of C against many iterations with random rolls, and the variance between runs easily supports the theoretical value for C.
#!/usr/bin/env python
import math
import random
def PfromC(C):
if not isinstance(C, float):
C = float(C)
pProcOnN = 0.0
pProcByN = 0.0
sumNpProcOnN = 0.0
maxFails = int(math.ceil(1.0 / C))
for N in range(1, maxFails + 1):
pProcOnN = min(1.0, N * C) * (1.0 - pProcByN)
pProcByN += pProcOnN
sumNpProcOnN += N * pProcOnN
return (1.0 / sumNpProcOnN)
def CfromP(p):
if not isinstance(p, float):
p = float(p) # double precision
Cupper = p
Clower = 0.0
Cmid = 0.0
p2 = 1.0
while(True):
Cmid = (Cupper + Clower) / 2.0
p1 = PfromC(Cmid)
if math.fabs(p1 - p2) <= 0:
break
if p1 > p:
Cupper = Cmid
else:
Clower = Cmid
p2 = p1
return Cmid
def testPfromC(C, iterations):
if not isinstance(C, float):
C = float(C)
nsum = 0
for i in range(iterations):
n = 1
while (C * n) < 1.0 and random.random() > (C * n):
n += 1
nsum += n
av_iters = float(nsum) / iterations
return 100.0 / av_iters
if __name__ == "__main__":
iterations = 100000
for i in range(1, 100):
p = i / 100.0
C = CfromP(p)
print "%.2f\t%f\t%f" % (p, C, testPfromC(C, iterations))
The above code gives the following values for p, theoretical C, and experimental p:
0.01 0.000156 1.002238
0.02 0.000620 2.002579
0.03 0.001386 3.001511
0.04 0.002449 4.002288
0.05 0.003802 4.994758
0.06 0.005440 6.005638
0.07 0.007359 7.003934
0.08 0.009552 8.010426
0.09 0.012016 8.987512
0.10 0.014746 10.014611
0.11 0.017736 11.004199
0.12 0.020983 12.005177
0.13 0.024482 13.024971
0.14 0.028230 13.975791
0.15 0.032221 14.959288
0.16 0.036452 15.999053
0.17 0.040920 17.009059
0.18 0.045620 17.972132
0.19 0.050549 18.976664
0.20 0.055704 19.954822
0.21 0.061081 20.943636
0.22 0.066676 22.012710
0.23 0.072488 22.979420
0.24 0.078511 24.014735
0.25 0.084744 25.031101
0.26 0.091183 25.959664
0.27 0.097826 27.102915
0.28 0.104670 28.023057
0.29 0.111712 29.000554
0.30 0.118949 29.959944
0.31 0.126379 31.002753
0.32 0.134001 31.939392
0.33 0.141805 32.999597
0.34 0.149810 34.025757
0.35 0.157983 34.908278
0.36 0.166329 35.980153
0.37 0.174909 37.053368
0.38 0.183625 38.058557
0.39 0.192486 38.999431
0.40 0.201547 40.008482
0.41 0.210920 41.034227
0.42 0.220365 42.099236
0.43 0.229899 42.969354
0.44 0.239540 44.056551
0.45 0.249307 45.176913
0.46 0.259872 45.906516
0.47 0.270453 46.863211
0.48 0.281008 48.012061
0.49 0.291552 48.983830
0.50 0.302103 49.938326
0.51 0.312677 51.091310
0.52 0.323291 51.903294
0.53 0.334120 53.118028
0.54 0.347370 54.012304
0.55 0.360398 54.951094
0.56 0.373217 56.077072
0.57 0.385840 56.917788
0.58 0.398278 57.943470
0.59 0.410545 58.905304
0.60 0.422650 59.988002
0.61 0.434604 61.122079
0.62 0.446419 62.023197
0.63 0.458104 63.044547
0.64 0.469670 63.999181
0.65 0.481125 65.143609
0.66 0.492481 66.020110
0.67 0.507463 66.997186
0.68 0.529412 68.085570
0.69 0.550725 68.980741
0.70 0.571429 70.037330
0.71 0.591549 71.011632
0.72 0.611111 72.145387
0.73 0.630137 73.091401
0.74 0.648649 73.969421
0.75 0.666667 74.791519
0.76 0.684211 75.818460
0.77 0.701299 77.138471
0.78 0.717949 77.965415
0.79 0.734177 79.041386
0.80 0.750000 80.042903
0.81 0.765432 80.993302
0.82 0.780488 81.967885
0.83 0.795181 82.964831
0.84 0.809524 84.091559
0.85 0.823529 85.107832
0.86 0.837209 86.080003
0.87 0.850575 87.164960
0.88 0.863636 88.069258
0.89 0.876404 88.907065
0.90 0.888889 90.034934
0.91 0.901099 91.098742
0.92 0.913043 91.925283
0.93 0.924731 93.009413
0.94 0.936170 94.065413
0.95 0.947368 95.073302
0.96 0.958333 95.967448
0.97 0.969072 96.931159
0.98 0.979592 97.973900
0.99 0.989899 98.999119