Module:Collection log calculator

Revision as of 20:07, 23 October 2021 by en>CephHunter
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Documentation for this module may be created at Module:Collection log calculator/doc

-- <nowiki>
local arr = require 'Module:Array'
local chart = require 'Module:Chart data'
local yesno = require 'Module:Yesno'
local prob = require 'Module:Probability'
local iter = require 'Module:Iterator'
local spairs = iter.spairs
local opairs = iter.opairs
local formCalc = require 'Module:Form calculator'._main
local split = mw.text.split

local floor = math.floor
local ceil = math.ceil
local min = math.min
local max = math.max
local log10 = math.log10

local p = {}

local encodeTable = {
    [' '] = '_',
    ["'"] = '~a~',
    [':'] = '~b~',
    ['('] = '~c~',
    [')'] = '~d~',
    ['?'] = '~e~'
}
local function encodeName( name )
    return (name:gsub( "[ ':&%(%)%?]", encodeTable ))
end

local decodeTable = {
    ['_'] = ' ',
    ['~a~'] = "'",
    ['~b~'] = ':',
    ['~c~'] = '(',
    ['~d~'] = ')',
    ['~e~'] = '?'
}
local function decodeName( name )
    return (name:gsub( "_", decodeTable ):gsub( "~%w~", decodeTable ))
end

local function findStartParamIndex( config )
    local i = 1
    while config['param' .. i] ~= nil do
        i = i + 1
    end
    return i
end

local function sortedParamNames( t )
    local sorted = {}

    for k, v in pairs(t) do
        local id = type(v) == 'string' and v or k
        local group
        if type(v) == 'table' then group = v.group or v.order end
        table.insert( sorted, { key=k, id=id, group=group } )
    end

    local function sortFunc( lhs, rhs )
        if lhs.group == rhs.group then
            return lhs.id < rhs.id
        elseif lhs.group and rhs.group then
            return lhs.group < rhs.group
        else
            return lhs.group ~= nil
        end
    end

    table.sort( sorted, sortFunc )

    local i = 0
    return function()
        i = i + 1
        if sorted[i] then
            return sorted[i].id, t[sorted[i].key]
        end
    end
end

function p.createConfig( frame )
    local settings = require( 'Module:Collection log calculator/' .. frame.args.type .. '/Data' )

    local conf = settings['config template']

    local pi = findStartParamIndex( conf )
    local function addParam( id, args )
        conf['param'..pi] = id
        conf['label'..pi] = args.label
        conf['type'..pi] = args.type
        conf['default'..pi] = args.default
        conf['range'..pi] = args.range
        conf['toggles'..pi] = args.toggles
        conf['help'..pi] = args.help
        pi = pi + 1
    end

    for name, data in opairs( settings.collectionData ) do
        local id = encodeName( name )
        table.insert( conf.range2, name )
        table.insert( conf.toggles2, name .. '=' .. id )
        local group = {}

        addParam( id, { label=name, type='group', range=group } )

        for param, paramData in sortedParamNames( data.params or {} ) do
            local paramId = encodeName( id..'#'..param )
            table.insert( group, paramId )
            paramData.label = paramData.label or param

            if paramData.toggles ~= nil then
                local sections = split( paramData.toggles, ';', true )
                for i, section in ipairs( sections ) do
                    local list = split( section, '=', true )
                    local tog = split( list[#list], ',', true )

                    list[#list] = arr.map( tog, function(x) return id..'#'..encodeName(x) end )
                    list[#list] = table.concat( list[#list], ',' )
                    sections[i] = table.concat( list, '=' )
                end
                paramData.toggles = table.concat( sections, ';' )
            end

            if paramData.type == 'group' then
                local range = split( paramData.range, ',', true )
                range = arr.map( range, function(x) return id..'#'..encodeName(x) end )
                paramData.range = table.concat( range, ',' )
            end

            addParam( paramId, paramData )
        end

        for dropName, v in sortedParamNames( data.drops ) do
            local paramName = encodeName( id..'#'..dropName )
            local image = type( v ) == 'table' and v.image or dropName
            local label = string.format( '[[File:%s.png]] %s', image, dropName )
            table.insert( group, paramName )
            addParam( paramName, { label=label, type='check', default='false' } )
        end
    end

    conf.toggles2 = table.concat( conf.toggles2, ';' )
    conf.default2 = conf.range2[1]
    conf.range2 = table.concat( conf.range2, ',' )

    for k, v in pairs( conf ) do
        if type( v ) == 'table' then
            conf[k] = table.concat( v, ',' )
        end
    end

    return formCalc( conf )
end

-- print( p.createConfig{type='Boss'} )

function p.calc( frame )
    local args = frame.args
    return p._calc( args )
end

function p._calc( args )
    local settings = require( 'Module:Collection log calculator/' .. args.type .. '/Data' )
    local handlers = require( 'Module:Collection log calculator/' .. args.type .. '/Handlers' )
    handlers.default = p.defaultHandler

    local filteredArgs = {
        currentKc = tonumber( args.currentKc or 0 ),
        colName = args.colName,
        colData = settings.collectionData[args.colName],
        type = args.type
    }

    for k, v in pairs( args ) do
        if string.find( k, '^'..encodeName( args.colName )..'#' ) then
            local argName = decodeName( k:match( '#(.+)$' ) )
            if v == 'true' or v == 'false' then
                v = yesno( v )
            end
            filteredArgs[argName] = v
        end
    end

    return handlers[filteredArgs.colData.handler]( filteredArgs )
end

local function generateOutput( chanceArrays, note, type )
    if #chanceArrays == 0 then
        return 'Your collection log is already complete.'
    end

    local combinedChance = chanceArrays:reduce( function(x, acc) return x * acc end )
    local diff = (combinedChance .. {combinedChance[#combinedChance]}) - ({0} .. combinedChance)
    local average = diff:reduce( function(x, acc, i) return acc + x * i end )
    average = floor( average + 0.5 )

    local killcountFor50 = 1
    local killcountFor90 = 1
    local killcountFor99 = 1
    local killcountFor9999 = 1
    for i = 1, #combinedChance do
        if combinedChance[i] < 0.5 then
            killcountFor50 = i + 1
        end
        if combinedChance[i] < 0.9 then
            killcountFor90 = i + 1
        end
        if combinedChance[i] < 0.99 then
            killcountFor99 = i + 1
        end
        if combinedChance[i] < 0.9999 then
            killcountFor9999 = i + 1
        else
            break
        end
    end

    killcountFor9999 = max( killcountFor9999, 10 )

    local stepsize = ceil( killcountFor9999 / 1000 )
    local xAxis = arr.range( 1, killcountFor9999, stepsize )
    local yAxis = combinedChance:slice( 1, killcountFor9999 ):take_every( stepsize ) * 100
    table.insert( xAxis, 1, 0 )
    table.insert( yAxis, 1, 0 )

    local action = type == 'Clue' and 'clue' or 'kill'
    local plot = chart.newChart{ type='scatter' }
        :setTitle( 'Chance vs ' .. action .. ' count' )
        :setXLabel( (type ~= 'Clue' and 'Additional ' or '') .. action .. ' count' )
        :setYLabel( 'Chance [%]' )
        :showLegend( false )
        :setDimensions('80vw', '70vh')

    plot:newDataSet{
        data = chart.convertToXYFormat( yAxis, xAxis ),
        pointRadius = 0,
    }

    return string.format( "%sAverage %s count: %d\n* 50%% at %d %ss\n* 90%% at %d %ss\n* 99%% at %d %ss\n%s",
        (note ~= nil) and ('Note: ' .. note .. '<br>') or '',
        action,
        average,
        killcountFor50,
        action,
        killcountFor90,
        action,
        killcountFor99,
        action,
        tostring( plot )
    )
end

function p.defaultHandler( args, chanceArrays )
    chanceArrays = chanceArrays or arr{}
    local colData = args.colData
    local droprateCounts = {}

    for item, data in spairs( colData.drops ) do
        if data.droprate and args[item] == false then
            local droprate = data.droprate

            if args.Hardmode == true then
                droprate = data.HMDroprate or droprate
            end

            if data.threshold then
                table.insert( chanceArrays, prob.withThresholdArr( droprate, data.threshold, args.currentKc, colData.genRange ) )
            else
                droprateCounts[droprate] = (droprateCounts[droprate] or 0) + 1
            end
        end
    end

    for droprate, count in pairs( droprateCounts  ) do
        table.insert( chanceArrays, prob.noThresholdArr( droprate, colData.genRange )^count )
    end

    return generateOutput( chanceArrays, colData.note, args.type )
end

return p
-- </nowiki>