Well, you need a RNG (Random Number Generator) that works with the probabilities you come up with. Yeah, you are not getting rid of RNG, in fact, what you describe is not unheard of in gaming.
What you want is take a randomic: a random variable with uniform distribution in the range (0, 1]. And then mold the randomic to the probability distribution you want.
You should be able to get a randomic in virtually every platform. You will rarely be programming in a platform that does not have a library with a suitable random. And if there isn't (GLSL comes to mind), then go ahead an implement some kind of congruential random number generator (I suggest the Linear congruential generator, for its ease of implementation, unless you have more specific requirements).
Notes:
- I am assuming we do not need a Cryptographically secure random.
- Yes, this is all "pseudorandom". I just didn't want to write "pseudo" everytime. It is meaningless knowing the context.
What I will describe here is inteded for a discrete list of values with their probabilities.
Usually the first step is to normalize the distribution. The total probability of all the cases must be 100%.
However, you have an special requirement that a porbability beyond 100% will become 0%, so let us start there...
Input distribution:
Bad = 233%
Low = 93.3%
Good = 37.3%
High = 15%
Great = 6%
Excellent = 2.4%
Master = 0.5%
Perfect = 0.1%
Adjusted distribution:
Bad = 0%
Low = 93.3%
Good = 37.3%
High = 15%
Great = 6%
Excellent = 2.4%
Master = 0.5%
Perfect = 0.1%
To normalize, we will add the probabilities and divide
Adjusted distribution:
Bad = 0%
Low = 93.3%
Good = 37.3%
High = 15%
Great = 6%
Excellent = 2.4%
Master = 0.5%
Perfect = 0.1%
Total: 154.6%
Normalized distribution:
Bad = 0 / 154.6 = 0%
Low = 93.3 / 154.6 = 60.349288486%
Good = 37.3 / 154.6 = 24.126778784%
High = 15 / 154.6 = 9.702457956%
Great = 6 / 154.6 = 3.880983182%
Excellent = 2.4 / 154.6 = 1.552393273%
Master = 0.5 / 154.6 = 0.323415265%
Perfect = 0.1 / 154.6 = 0.064683053%
Total: 99.999999999% (100% within rounding error)
Notes
- This would work even if the input values were not meant to be probabilities, they are just weights.
- Scaling all input values by a given factor will not affect this result.
- We have to decide where will the rounding error be chocked to. I will suggest to leave the error on the last category (which is what happens naturally with this method).
- If you can work with arbitrary precision or with fractional number, you can do this without rounding error. It is usually not worth it.
The next step is to figure out the accumulated distribution:
Normalized distribution:
Bad = 0%
Low = 60.349288486%
Good = 24.126778784%
High = 9.702457956%
Great = 3.880983182%
Excellent = 1.552393273%
Master = 0.323415265%
Perfect = 0.064683053%
Accumulated distribution:
Bad = 0%
Low = 60.349288486% + 0% = 60.349288486%
Good = 24.126778784% + 60.349288486% = 84.47606727%
High = 9.702457956% + 84.47606727% = 94.178525226%
Great = 3.880983182% + 94.178525226% = 98.059508408%
Excellent = 1.552393273% + 98.059508408% = 99.611901681%
Master = 0.323415265% + 99.611901681% = 99.935316946%
Perfect = 0.064683053% + 99.935316946% = 99.999999999%
In case that wasn't clear, we are adding the previous total each step, so that we are accumulating the values. We should get 100% (within rounding error) at the end.
Finally, we take our randomic and choose...
pseudo code:
var randomic = rand();
if (randomic < 0%)
{
return "Bad";
}
if (randomic < 60.349288486%)
{
return "Low";
}
if (randomic < 84.47606727%)
{
return "Good";
}
//...
if (randomic < 99.935316946%)
{
return "Master";
}
return "Perfect"; // <- no check, sleight of hand on the rounding error
You can imagine how you could turn your data into a dictionary, and then the code above would become a loop. And of course the randomic is in the range [0, 1), so you need your probabilities in the range [0, 1). I leave that to you.
Addendum 1: I totally ignored the fact that we are talking C#. Random.NextDouble
is where you get your randomic. And to store the distribution, an IEnumerable
of tuples or KeyValuePair
will do.
Addendum 2: On your code:
OnSkillUse()
{
var randNum = new Random();
if((ChanceToMake / 100) > randNum)
return "Failure";
return "Success";
}
By the way, that is not valid C#
Flip the conditional:
OnSkillUse()
{
var randNum = new Random();
if(randNum < (ChanceToMake / 100))
return "Failure";
return "Success";
}
That is the same thing I have above - assuming randNum
is the randomic - except you only have two cases: "Success" and "Failure". The accumulated probability of "Failure" is only the probability of "Failure" because there are no more cases before.
By the way, note that if ChanceToMake
is 0, then randNum
will never be less than ChanceToMake
(if ChanceToMake
is 0 then ChanceToMake / 100
will never be more than randNum
)... which means that it will never enter the if, therfore it will always lead to "Success". I do not think you meant to say that a ChanceToMake
of 0 means certain "Success". I'm guessing you have "Success" and "Failure" inverted.
Addendum 3
Is there a better way of accomplishing this since I basically already know the probability of making the item every time the player uses the skill?
If you know the the probability - yet not know the result - then You need a RNG.
We could talk about entropy. You see, your RNG uses a seed, which should be a source of entropy. When you say new Random()
you are leaving that to the system, however you could take control of the seed. Knowing the seed, your random number generator will be completely predictable (hence, "pseudorandom")... so, one option is to seed it with something with less entropy ("less random").
For what you say, you do not want to depend on player skill, so that beyond the question.
You could even have your own source of entropy and feed it from a server to the client.
See Using seeds for rng. A reliable way of saving bandwidth?.