First things first, I would get a handle on the probabilities of any event occurring. Get out a pen and paper, we'll do this by hand.
Make everything independent when you can. For example, when you see the series of if's at the start:
if rand(20) >= 2 then
//Make Object A
if rand(20) >= 5 then
//Make another Object A
if rand(20) >= 14 then
//Make a third Object A
if rand(20) >= 18 then
//Make one last Object A
We should recognize that these are 4 independent draws. Under each one, we have two possibilities: created an object A, or "did nothing." Assign each a probability using ratios, so the first if statement's probability of making an object is 2/20.
Other sections will not allow you to treat them as independent draws because of the 'elseif' patterns.
if rand(100) >= 25 then
if rand(10) < 8 then
//Make Object B
elseif rand(10) < 8 then
//Make Object C
else
//Make Object D
will require a tree to describe the probabilities
X
/ \
p=25/100 / \ p=75/100
Do nothing X
/ \
p=8/10 / \ p=2/10
Make B X
/ \
p=8/10 / \ p=2/10
Make C Make D
Now so far this is just trying to get a handle on what has been written. The next step is to try to simplify it. The idea is that you can shuffle the rolls around, so long as the probabilities are the same. For example, just with some multiplication I can see the probabilities of creating a B C or D.
X
/ \
p=25/100 / \ p=75/100
Do nothing X
(p=.25) / \
p=8/10 / \ p=2/10
Make B X
(p=.75*.8=.6) / \
p=8/10 / \ p=2/10
Make C Make D
(p=.75*.2*.8=.12) (p=.75*.2*.2=.03)
Now, from looking at all of these probabilities, they can all be expressed as a rational ratio with 100 on the bottom (.12 = 12/100; .03 = 3/100). Accordingly, we can handle all of this with a single roll, which makes the logic a whole ton simpler.
roll = rand(100) -- a random roll from [0,100) is sufficient for the whole block
if roll < 60 then -- we'll hit this with a probability of .6
// make object B
elseif roll < 72 then -- 72-60 = 12, so we'll hit this with a probability of .12
// make object C
elseif roll < 75 -- 75-72 = 3, so we'll hit this with a probability of .03
// make object D
else -- 100-75=25, so we'll hit this with a probability of .25
-- do nothing. This is the equivalent of not entering the
-- outermost if statement, with a probability of .25.
end
The other pattern I see here is that there are many independent draws which all create the same object. You're right: it would be nice to simplify these. You can use a joint probability table to join them. For example, we can take the first two "make object A" cases.
Nothing(p=.3) Make A (p=.7) <-- second if
v--- first 'if' +-------------------------+--------------------------+
Nothing(p=.15) | Nothing (p=.3*.15=.045) | Make A (p=.7*.15=.105) |
+-------------------------+--------------------------+
Make A (p=.85) | Make A (p=.3*.85=.255) | Make 2x A (p=.7*.85=.595 |
+-------------------------+--------------------------+
You can apply this process as many times as you like, until you get a table containing the probability of getting 1A, 2A, 3A, or 4A.
When you are done with all of this, what you should see is a small number of draws, followed by a lookup from a few tables to determine the outcome based on those draws. That should be enough to get a firm grasp on the probability spaghetti code.
From there, you could use some functional programming to make this cleaner. You could build a table object which looks up a draw, and returns a function which will do the correct task for that slot in the table. This has the advantage of being a very standard way to do probabilistic outcomes, so any future developer who looks at it will immediately recognize the random draw table and be comfortable with it.