This commit is contained in:
Kim ravn Hansen
2025-08-31 10:08:06 +02:00
commit 81e1196669
12 changed files with 4014 additions and 0 deletions

217
tooling/mathD20.lua Executable file
View File

@@ -0,0 +1,217 @@
--
--{{{ print_r(t)
local function print_r(t)
local print_r_cache = {}
local function sub_print_r(t, indent)
if (print_r_cache[tostring(t)]) then
print(indent .. "*" .. tostring(t))
else
print_r_cache[tostring(t)] = true
if (type(t) == "table") then
local tLen = #t
for i = 1, tLen do
local val = t[i]
if (type(val) == "table") then
print(indent .. "#[" .. i .. "] => " .. tostring(t) .. " {")
sub_print_r(val, indent .. string.rep(" ", string.len(i) + 8))
print(indent .. string.rep(" ", string.len(i) + 6) .. "}")
elseif (type(val) == "string") then
print(indent .. "#[" .. i .. '] => "' .. val .. '"')
else
print(indent .. "#[" .. i .. "] => " .. tostring(val))
end
end
for pos, val in pairs(t) do
if type(pos) ~= "number" or math.floor(pos) ~= pos or (pos < 1 or pos > tLen) then
if (type(val) == "table") then
print(indent .. "[" .. pos .. "] => " .. tostring(t) .. " {")
sub_print_r(val, indent .. string.rep(" ", string.len(pos) + 8))
print(indent .. string.rep(" ", string.len(pos) + 6) .. "}")
elseif (type(val) == "string") then
print(indent .. "[" .. pos .. '] => "' .. val .. '"')
else
print(indent .. "[" .. pos .. "] => " .. tostring(val))
end
end
end
else
print(indent .. tostring(t))
end
end
end
if (type(t) == "table") then
print(tostring(t) .. " {")
sub_print_r(t, " ")
print("}")
else
sub_print_r(t, " ")
end
print()
end
--}}}
--{{{ table_count(t)
function table_count(t)
local count = 0
for _ in pairs(t) do count = count + 1 end
return count
end
--}}}
--{{{ range(low, high, n)
---Create a table with integers in a given range
---@param low integer Lowest number
---@param high integer Highest number
---@param n integer Increment - the array will contain every nth number between [low..high]
---@return table
local function range(low, high, n)
local t = {}
for i = low, high, n do
table.insert(t, i)
end
return t
end
---}}}
-- {{{ sortMapByValueDesc(m)
---Sort the table m by value.
---@param m table
---@return table
local function sortMapByValueDesc(m)
local tmp = {}
-- create a temporary table that is easier to sort
for damage, occurrences in pairs(m) do
local entry = {
damage = damage,
count = occurrences,
}
table.insert(tmp, entry)
end
table.sort(tmp, function(a, b)
return a.count > b.count
end)
return tmp
end
--}}}
-- {{{ arraySum(t)
local function table_sum(t)
local accumulator = 0
for _, v in pairs(t) do
accumulator = accumulator + v
end
return accumulator
end
---}}}
--{{{ mostCommonDamageValues(instances, ratio)
--- Calculate the most likely values
---@param instances table A map of [damage score] => [occurrences]
---@param ratio number Include most common damage until the sum of the probabilities of the damage scores are at least this number.
---@return table
local function mostCommonDamageValues(instances, ratio)
if ratio == nil then
ratio = 0.25
end
local sortedInstances = sortMapByValueDesc(instances)
local totalOccurrences = table_sum(instances)
local result = {}
local occurrenceAccumulator = 0
for _, instance in pairs(sortedInstances) do
occurrenceAccumulator = occurrenceAccumulator + instance.count
table.insert(result, instance.damage)
if occurrenceAccumulator / totalOccurrences > ratio then
return result
end
end
return result
end
--}}}
-- {{{ calculateDamageD20(skill, base_damage)
---@param skill integer
---@param base_damage integer
---@return number average The average damage
---@return table typical The damage numbers that, together, are likely to occur more than 50% of the time.
local function calculateDamageD20(skill, base_damage)
local sum = 0
local n = 0
local tn = math.min(skill, 15)
local outcomes = {}
for rolled = 1, 20, 1 do
local damage = 0
if rolled > tn then
-- glancing blow
damage = base_damage
end
if rolled == tn then
-- crit
damage = 2 * (skill + base_damage)
end
if rolled < tn then
-- hit
damage = rolled + base_damage
end
local key = tostring(damage)
if outcomes[key] == nil then
outcomes[key] = 1
else
outcomes[key] = outcomes[key] + 1
end
sum = sum + damage
n = n + 1
end
return sum / n, mostCommonDamageValues(outcomes, 0.5)
end
-- }}}
local skill_min = 5
local skill_max = 15
local skill_inc = 1
local skills_table = range(skill_min, skill_max, skill_inc)
local damage_min = 0
local damage_max = 10
local damage_inc = 3
local damages_table = range(damage_min, damage_max, damage_inc)
io.write("skill\\damage", ";")
for i, dmg in pairs(damages_table) do
io.write(string.format("%d (avg);%d (typical)", dmg, dmg))
if i < #damages_table then
io.write(";")
end
end
print("")
for _, skill in pairs(skills_table) do
io.write(skill, ";")
local tmp = {}
for key, damage in ipairs(damages_table) do
local avg, dmg = calculateDamageD20(skill, damage)
tmp[key] = string.format("%.2f;%s", avg, table.concat(dmg, ", "))
end
print(table.concat(tmp, ";"))
end