function module.logUniform(min, max, skewPower)
min = min or 1/1000
max = max or 1000
skewPower = skewPower or 12
local logMin = math.log(min)
local logMax = math.log(max)
-- Skew the distribution: higher exponent = stronger skew toward low values
local rand = math.random() ^ skewPower
local value = math.exp(logMin + (logMax - logMin) * rand)
-- Scale down to make expected value close to 1
local scale = 1 / (2 * (logMax - logMin) / (skewPower + 1)) -- adjust scale for skew
return value * scale
end
I want to make a RNG system for my game that generates a random number between 2 numbers that will always average out to 1. This is what I have right now, but I’m not sure it average out to 1 and the results are too scattered. I want the results to be way closer to 1.
@UW4Z I have an idea. Generate 5 numbers with math.random inside a function maybee (-20,20) and then check if the sum divided by 5 is one using an if statment.
If it is, it prints. If it isn’t, it rerolls until it gets those numbers that average
JUST BY THE WAY, what do you wanna use this for, it sounds interesting and cool
If I understand what you want correctly, then this should help:
local function BiasedRNG()
local RNG = Random.new(Seed)
return math.floor(RNG:NextInteger(0, 1000000)^0.5) --will give 0-1000 range, but values will be closer to 1.
end
In case of you wanting to make it to be avarage of all selections it will be 1, like this: 20+0+0+...+0 (19 times +0) = 20/20 = 1
You need to build much more complex system.
Doesn’t necessarily need to be divided. You can just check whether the sum IS 5 or not as the only number divided by 5 to equal 1 is 5.
Although from my understanding of OP’s post, this isn’t the intended behaviour. They are attempting to generate random numbers that are generally closer to 1, i.e in a test of 100 random numbers, 75 of them will be around the range of 1.
To test it, take the average of 100 random numbers multiple times. If the averages are close to 1, then your bias function is likely working as intended.
From my tests, your averages are more biased towards 3 rather than 1.
Ok we finna use a clamped f distribution and then scale it to get our desired range and mean (average)
local rng = Random.new()
local function generateF(min, max, mean)
-- mean gotta be greater than 1
if mean <= 1 then
return warn('the mean gotta be >1')
end
-- Calculate d2 from the mean: mean = d2 / (d2 - 2)
local d2 = (2 * mean) / (mean - 1)
local d1 = 5 -- Fixed d1
-- Generate 2 chi-squared random variables
local function generate_chi_squared(df)
local sum = 0
for _ = 1, df do
local u = rng:NextNumber(0, 1)
local v = rng:NextNumber(0, 1)
local normal = math.sqrt(-2 * math.log(u)) * math.cos(2 * math.pi * v)
sum = sum + normal * normal
end
return sum
end
-- Generate F-distributed random variable: F = (X1/d1) / (X2/d2)
local X1 = generate_chi_squared(d1)
local X2 = generate_chi_squared(d2)
local F_val = (X1 / d1) / (X2 / d2)
-- Scale F_val to achieve the desired mean
local theoretical_mean = d2 / (d2 - 2)
local scaled_F = F_val * (mean / theoretical_mean)
-- Truncate to [min, max]
if scaled_F < min then
return min
elseif scaled_F > max then
return max
else
return scaled_F
end
end
Change the average to something close to and greater than 1 because the function cannot take it
Also, your distribution is way too extreme. There is almost no chance of getting anywhere near 50 let alone 1000 with an average of 1. Here are the results for 100000 generations with degree of freedom 1 set to 1 (when d1 approaches 1, results become more extreme):
function module.logUniformNormalized(min, max, skewPower)
min = min or 1/1000
max = max or 1000
skewPower = skewPower or 2 -- much lower skew power
local logMin = math.log(min)
local logMax = math.log(max)
local rand = math.random() ^ skewPower
local value = math.exp(logMin + (logMax - logMin) * rand)
-- Compute expected value of this distribution for scaling
-- Approximate: E[X] = (max^(1 - a) - min^(1 - a)) / ((1 - a) * (log(max) - log(min)))
-- where a = skewPower
local a = skewPower
local expected
if a == 1 then
-- Avoid divide by zero: E[X] = (log(max) - log(min)) / (log(max/min))
expected = (logMax - logMin) / math.log(max / min)
else
expected = (max^(1 - a) - min^(1 - a)) / ((1 - a) * (logMax - logMin))
end
return value / expected
end
Instead of creating an entirely new function, I did some more search and discovered instead of modifying the formula, I can use specific number pairs to generate responses that average out to a certain number.
Some of these pairs are:
.5 and .2
1 and 10,000
.1 and 1,000
.01 and 100
& etcetera...
function randomPair(min, max, target)
local rand = math.random() * (max - min) + min
local anchor = 2 * target - rand
return rand, anchor
end
print("target 1 (min)", randomPair(1, 1000, 1))
print("target 500 (avg)", randomPair(1, 1000, 500))
print("target 1000 (max)", randomPair(1, 1000, 1000))
It seems the further the target is from the mean (1/2 range since math.random is uniform) the more likely the pair is to be out of bounds. You can see this by running the code for yourself, typically one of the numbers is outside the range.
(Theoretically if we increase the number of terms in the mean above from a pair to a large tuple, we could take advantage of the growing denominator and allow the quotient to approach the target to some degree)
I’m confused when looking at your list of pairs because to me none of these average out to 1 (in the sense of (a+b)/2=1) unless you’re talking about a different kind of average?
With all this in mind I think modifying the math.random function into a skewed distribution is your best bet.