lep / jassdoc

Document the WarCraft 3 API
52 stars 20 forks source link

GetRandomReal #151

Closed shuen4 closed 2 months ago

shuen4 commented 3 months ago
scope test initializer init
private function init takes nothing returns nothing
    call SetRandomSeed(0)
    call BJDebugMsg(R2S(GetRandomReal(-42, 42)))
    call SetRandomSeed(0)
    call BJDebugMsg(R2S(GetRandomReal(42, -42)))
    call SetRandomSeed(0)
    call BJDebugMsg(R2S(GetRandomReal(42, -42) - /* 42 - (-42) */ 84))
    call SetRandomSeed(0)
    call BJDebugMsg(R2S(GetRandomReal(-99, 99)))
    call SetRandomSeed(0)
    call BJDebugMsg(R2S(GetRandomReal(99, -99)))
    call SetRandomSeed(0)
    call BJDebugMsg(R2S(GetRandomReal(99, -99) - /* 99 - (-99) */ 198))
    call SetRandomSeed(0)
    call BJDebugMsg(R2S(GetRandomReal(-12, 99)))
    call SetRandomSeed(0)
    call BJDebugMsg(R2S(GetRandomReal(99, -12)))
    call SetRandomSeed(0)
    call BJDebugMsg(R2S(GetRandomReal(99, -12) - /* 99 - (-12) */ 111))
endfunction
endscope

image

i think GetRandomReal doesn't swap value like GetRandomInt does GetRandomReal(-12, 99) - generate 1 number and add -12 GetRandomReal(99, -12) - generate 1 number and add 99, resulting 99 - (-12) offset compared to correct function usage GetRandomReal(99, -12) - /* 99 - (-12) */ 111 - same above but offset is used to produce same result as GetRandomReal(-12, 99)

WaterKnight commented 3 months ago

You mean that if the highBound (2nd argument) is smaller than the lowBound (1st argument), it should swap them?

For me on 1.36.1.21015, this is not the case for GetRandomInt, either.

The formulae for min and max output values seem to be:

min = lowBound
max = (lowBound > highBound) ? (2 * lowBound - highBound) : highBound

edit: Also seems to hold for GetRandomReal.

shuen4 commented 3 months ago

GetRandomInt does swap the value but GetRandomReal does not

lep commented 2 months ago

Nice observation but isn't it allready documented? https://lep.duckdns.org/jassbot/doc/GetRandomReal

Luashine commented 2 months ago

Shuen, thanks for the issue. These functions were among the earliest things I had described. I'll look at code logic later, here's a clean room description of the implementation via v1.29 (because I can't just paste the C-looking something... right? riiight?)


Now it prepares the random part:

The code is viewable elsewhere, but I will leave it at that ;)

So the random function really swaps the bounds if low <= high. What I hadn't understood at the time is what happens with the result, it was a quick test.

Judging by the calculated delta, it's how the interval is normalized to be reapplied to the returned randomly generated value (probably in (0.0; 1.0] as per usual), the low bound is added at the last step before return. Important to note, that we observe a different epsilon here, so random reals in Jass will be a common source of <0.001 values, which is supposed to be the epsilon.

shuen4 commented 2 months ago

@note Undefined behavior when lowBound > highBound. it might better if change to @note/@bug This does not swap value when lowBound > highBound, unlike GetRandomInt.

shuen4 commented 2 months ago

GetRandomReal(10, -10)

if result is >= 0.0000002f then return low

0 > result then return low(1.26 and 1.27), possible mean return low if low == high

if low <= high then calculate delta of float_minus(out2, high, low) ...otherwise calculate delta of float_minus(out3, low, high)

swapped here float_minus(temp_var, 10, -10) delta = 20

(Y) get 32-bits of the calculation: (unknown_constant) & subroutine_probably_rand(likely_seed_or_state) | (all bits FP32 mantissa except for MSB aka 0x3F800000)

unknown_constant = 0x7FFFFF fp32 fraction(23 bits) Y = float between 1 ~ 2, assume 1.5

float_sum(out3, (X), unknown_var)

it should be Y instead of X unknown_var = -1.0 out3 = 0.5

float_mul(outZ, (delta), out3)

outZ = 10

return float_sum(temp_var, low, outZ)

since above already swap this should swap too 2nd arg is low arg result = 20 2nd arg is high arg result = 0


if no swap values in GetRandomReal function

delta = -20 outZ = -10 result = 0


PS: hvnt tested for GetRandomReal(-10, 10)

Luashine commented 2 months ago

Heh in the end I didn't go by the "C-like pseudocode" someone provided us. I found it easier to go off generated values. 😅 Shuen, I wanted very nice values to play with so I found the following seeds (v1.36.2):

    local lowSeed = 1229611 -- low bound
    local midSeed = 7685839 -- 50% middle in between low and high
    local highSeed = 23999343 -- 99.99999% high bound, actual high bound is not in range

Using this function (just call step() repeatedly from chat):

nextSeed = 23993461
seedSteps = 10000000
--[[ Steps must not be a scientific notation or the number becomes a float
and breaks the iterator, it goes on infinitely]]
function step()
    local newMax = nextSeed + seedSteps
    for i = nextSeed, newMax, 1 do
        SetRandomSeed(i)
        local r = GetRandomReal(0, 8)
        if r == 0 or r == 4 or r >= 7.99999 then
            print(table.concat({i, r, tostring(r==0), tostring(r==4), tostring(r >= 7.99999)}, ","))
            print(string.format("Found seed: %-16d", i))
            newMax = i
            break
        end
    end
    nextSeed = newMax + 1
    print(string.format("...reached seed: %-16d", newMax))
end

Then test the values (remember due to string.format the values are truncated. The 'high' value is always wrong but it's correctly approximated for reading):

Un/comment the pairs iterations else it won't fit in Eikonium's debug multiboard.

function testFormula()
    local testRReal = function (seed, low, high)
        SetRandomSeed(seed);
        return string.format("%.16f", GetRandomReal(low,high))
    end

    local reFormula = function(low, high)
        local min = low
        local mid
        local max = (low > high) and (2 * low - high) or high
        if low == high then
            mid = low
        elseif low < high then
            mid = (low + high) / 2
        else -- low > high
            mid = (low + -high) / 2 + low
        end

        return min, mid, max
    end

    local lowSeed = 1229611
    local midSeed = 7685839
    local highSeed = 23999343

    print(string.format("%-10s | %-6s %-6s %-6s | %-6s %-6s %-6s",
        " / ",
        "wcMin", "wcMid", "wcHigh",
        "reMin", "reMid", "reMax"
    ))

    for _, iters in pairs({
        --[[{iterL = {-8, -7, 1}, iterH = {-2, 1, 1}},
        {iterL = {8, 7, -1}, iterH = {-2, 1, 1}},]]
        {iterL = {-2, 1, 1}, iterH = {8, 7, -1}},
        {iterL = {-2, 1, 1}, iterH = {-8, -7, 1}},
    }) do
        local lMin, lMax, lStep = table.unpack(iters.iterL)
        local hMin, hMax, hStep = table.unpack(iters.iterH)
        for l = lMin, lMax, lStep do
            for h = hMin, hMax, hStep do
                local wc3min = testRReal(lowSeed, l, h)
                local wc3mid = testRReal(midSeed, l, h)
                local wc3max = testRReal(highSeed, l, h)

                local reMin, reMid, reMax = reFormula(l, h)
                print(string.format("%-5.0f, %-5.0f | %-7.1f %-7.1f %-7.1f | %-7.1f %-7.1f %-7.1f",
                    l, h,
                    wc3min, wc3mid, wc3max,
                    reMin, reMid, reMax
                ))
            end
        end
    end
end
testFormula()

wc3min/mid/max are game's rng values. reMin etc is reverse engineered formula (see reFormula function)

Results:

formula1 formula2


Now that I haven't given any thought whether any values swap there - you'll answer this instead 😝 And because I hope this is correct and I don't want to come back to this later if it's really all right, I will add the formula function and seed as-is to the GetRandomReal description.

PS: I don't know what I find cooler. The three seeds or the (hopefully final) formula. Water's min/max code was correct, I used it above.

shuen4 commented 2 months ago

Now that I haven't given any thought whether any values swap there

local max = (low > high) and (2 * low - high) or high (2 * low - high) simplify from low + low - high or high simplify from low + high - low

function(low, high)
    local min = low
    local delta = (low > high) and (low - high) or (high - low)
    local mid = low + delta / 2
    local max = low + delta

    return min, mid, max
end

side note: final output of GetRandomReal was low + delta * (random - 1)[^1]

[^1]: random = random float between 1(0x3f800000) ~ 1.99999988079071044921875(0x3fffffff)[^2], also the reason for GetRandomReal does not include highBound [^2]: used https://www.h-schmidt.net/FloatConverter/IEEE754.html to convert hex to float

Luashine commented 2 months ago

Right thanks. This looks correct and simple as it was supposed to be :) I updated the PR with your code.