-- -- {{{ Digit Functions --- Get sorted base-10 digits of a number ---@param n integer ---@return integer[] local function get_sorted_digits(n) local digits = {} for d in tostring(math.abs(n)):gmatch("%d") do table.insert(digits, tonumber(d)) end table.sort(digits) return digits end -- }}} --{{{ print_r --- @diagnostic disable-next-line unused-function local function print_r(t) local print_r_cache = {} --- @diagnostic disable-next-line unused-function 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 --- @diagnostic disable-next-line unused-function local 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 totalCount = 0 local tmp = {} -- create a temporary table that is easier to sort for damage, occurrences in pairs(m) do local entry = { damage = damage, count = occurrences, } totalCount = totalCount + occurrences table.insert(tmp, entry) end table.sort(tmp, function(a, b) return a.count > b.count end) return tmp -- -- local result = {} -- for _, v in pairs(tmp) do -- result[v.damage] = v.count -- end -- -- -- if table_count(result) ~= table_count(m) then -- error("I couldn't sort!!") -- end -- -- return result end --}}} --{{{table_sum(t) ---numeric sum of table values ---@param t integer[] ---@return integer local function table_sum(t) local result = 0 for _, v in pairs(t) do result = result + v end return result 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.5 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 --}}} --{{{calculateDamageD100(skill, base_damage) ---Calculate the average of the d100 attack method ---@param skill integer The skill level of the attacker (range of 1 to 100, but will be capped to 20) ---@param base_damage integer The base damage of the attacker's weapon. ---@return number ---@return table mostRolled the damage values that are most likely to be rolled local function calculateDamageD100(skill, base_damage) local sum = 0 local n = 0 local tn = math.min(skill, 80) local damages = {} for rolled = 1, 100, 1 do local digits = get_sorted_digits(rolled) local damage = 0 if rolled > tn then -- glancing blow: damage equal to weapon's base damage. damage = base_damage end if rolled < tn then -- normal hit: damage equal digit sum of what is rolled plus base damage. damage = table_sum(digits) + base_damage end if rolled == tn then -- Critical Hit: damage equal to what you rolled. damage = rolled end local key = tostring(damage) n = n + 1 sum = sum + damage if not damages[key] then damages[key] = 1 else damages[key] = damages[key] + 1 end end return sum / n, mostCommonDamageValues(damages, 0.5) end --}}} local skill_min = 30 local skill_max = 80 local skill_inc = 5 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 = calculateDamageD100(skill, damage) tmp[key] = string.format("%.2f;%s", avg, table.concat(dmg, ", ")) end print(table.concat(tmp, ";")) end