Module:Challenge

From Fallen London Wiki

Documentation for this module may be created at Module:Challenge/doc

local p = {}

local function get_description(prob)
	if prob <= 10 then
		return 'almost impossible'
	elseif prob <= 30 then
		return 'high-risk'
	elseif prob <= 40 then
		return 'tough'
	elseif prob <= 50 then
		return 'very chancy'
	elseif prob <= 60 then
		return 'chancy'
	elseif prob <= 70 then
		return 'modest'
	elseif prob <= 80 then
		return 'very modest'
	elseif prob <= 90 then
		return 'low-risk'
	else
		return 'straightforward'
	end
end

local function sign(num)
	if num == 0 then
		return num
	end
	return math.floor(num / math.abs(num))
end

local function clamp(num, lo, hi)
    return math.min(hi, math.max(num, lo))
end

local function broad_target_to_stat(target, diff, scaler)
	return math.ceil(target / scaler * diff)
end

local function broad_percentage(stat, diff, scaler)
	return math.min(100, math.floor(scaler * stat / diff))
end

local function make_title_row(challenge_type, quality_link, diff, embedcontext)
	local title_row
	if challenge_type == 'Broad' then
		title_row = "[[Broad difficulty|Broad]], '''" .. quality_link .. "''' "
	elseif challenge_type == 'Narrow' then
		title_row = "[[Narrow difficulty|Narrow]], '''" .. quality_link .. "''' "
	end

	if diff == nil then
		if embedcontext then
    		return title_row .. 'Unknown Difficulty Level'
    	else
    		return title_row .. 'Unknown Difficulty Level\n[[Category:Diff]]'
		end
	end

	return title_row .. diff
end

local function make_row(stat, prob)
	return stat .. ' - ' .. get_description(prob) .. ' (' .. prob .. '%)'
end

--[[
The lowest example stat in our table should be the highest of:
1. The highest stat that gives the minimum possible probability
2. The set minimum value for the stat
3. diff - 5 (for positive step) or diff - 4 (for negative step), to match the
   expected behaviour for the default step of 10
]]
local function get_min(diff, step, min_value, min_challenge)
    if step > 0 then
        return math.max(diff - 5, min_value, math.floor(diff + (min_challenge - 60) / step))
    else
        return math.max(diff - 4, min_value, math.floor(diff + 40 / step))
    end
end

--[[
The highest example stat in our table should be the lowest of:
1. The lowest stat that gives a 100% probability
2. The lowest stat on our table + 9, ensuring we use up to 10 rows total.
]]
local function get_max(diff, step, min_rows)
    if step > 0 then
        return math.min(diff + 9 - min_rows, math.ceil(diff + 40 / step))
    else
        return math.min(diff + 9 - min_rows, math.ceil(diff - 60 / step))
    end
end

local function insert_row(rows, stat_text, prob, embedcontext)
    if embedcontext then
        table.insert(rows, "''" .. make_row(stat_text, prob) .. "''")
    else
        table.insert(rows, '*' .. make_row(stat_text, prob))
    end
end

local function range_text(prob, stat, step, min_value, min_challenge)
    local min_challenge = step == 10 and 10 or 0
    if (prob == min_challenge and step < 0) or (prob == 100 and step > 0) then
        return ' and above'
    elseif (prob == min_challenge or prob == 100) and stat > min_value then
        return ' and below'
    end
    return ''
end

local function join_rows(rows, embedcontext)
	local sep = '\n'
	if embedcontext then
		sep = '<span style="font-size:larger"> || </span>'
	end
	return table.concat(rows, sep) .. '<br/>'
end

--[[
Return the challenge information for a Narrow Difficulty challenge.

Input parameters:
    @quality_link: the text to show for the quality name (typically an expanded {{IL}})
    @diff: the quality level that gives a 60% success probability
    @min_value: the minimum achievable quality level when attempting this challenge
    @step: the flat amount by which the success probability changes for each
        quality level. Negative step is allowed, but 0 is not.
    @embedcontext: true if being called from an embedded context where the compact
        and category-free table should be returned

The function returns a wiki code string describing the challenge. If diff is not
nil, this includes a list of example values.
]]
function narrow_challenge(quality_link, diff, min_value, step, embedcontext)
	local title_row = make_title_row('Narrow', quality_link, diff, embedcontext)
	if step < 0 then
		title_row = 'Inverted ' .. title_row
	end
	if diff == nil then
    	return title_row
	end

    -- normal narrow diffs bottom out at 10% difficulty
    -- but ones with different step go to 0%
    local min_challenge = step == 10 and 10 or 0
	
    local start, stop
    if step > 0 then
        start = get_min(diff, step, min_value, min_challenge)
        stop = get_max(diff, step, diff - start)
    else
        stop = get_min(diff, step, min_value, min_challenge)
        start = get_max(diff, step, diff - stop)
    end

	local step_sign = sign(step)
	local rows = {}
	table.insert(rows, title_row)
	for stat = start, stop, step_sign do
		if stat >= min_value then
			local prob = clamp(60 + step * (stat - diff), 0, 100)
			local stat_text = stat .. range_text(prob, stat, step, min_value, min_challenge)
            insert_row(rows, stat_text, prob, embedcontext)
		end
	end
    if start > stop and step > 1 then
      insert_row(rows, min_value .. ' and above', 100, embedcontext)
    elseif stop > start and step < 1 then
      insert_row(rows, min_value .. ' and above', 0, embedcontext)
    end
    return join_rows(rows, embedcontext)
end

--[[
Return the challenge information for a Broad Difficulty challenge.

Input parameters:
    @quality_link: the text to show for the quality name (typically an expanded {{IL}})
    @diff: the quality level that gives a scalar% success probability
    @scaler: the probability scaler. currently always 60
    @embedcontext: true if being called from an embedded context where the compact
        and category-free table should be returned

The function returns a wiki code string describing the challenge. If diff is not
nil, this includes a list of example values.
]]
function broad_challenge(quality_link, diff, scaler, embedcontext)
	local title_row = make_title_row('Broad', quality_link, diff, embedcontext)
	if diff == nil then
    	return title_row
	end

    local rows = {}
    table.insert(rows, title_row)
    local guaranteed = broad_target_to_stat(100, diff, scaler)
    -- sixty_p only needs to be calculated if scaler is not 60, but it's easy
    -- enough so why not
    local sixty_p = broad_target_to_stat(60, diff, scaler)
    if guaranteed < sixty_p + 5 then
    	-- More useful tables for low diff values
    	local start = math.max(1, guaranteed - 6)
    	for i=start,guaranteed do
    		insert_row(rows, i, broad_percentage(i, diff, scaler), embedcontext)
    	end
    else
        for i, target_prob in ipairs({41, 51, 61, 71, 81, 91, 100}) do
    	    local stat = broad_target_to_stat(target_prob, diff, scaler)
        	-- For sufficiently high diff, actual_prob will equal target_prob
        	-- but for lower diffs, actual_prob is less confusing
        	local actual_prob = broad_percentage(stat, diff, scaler)
            insert_row(rows, stat, actual_prob, embedcontext)
        end
    end
    return join_rows(rows, embedcontext)
end

function p.narrow_challenge(frame)
	local args = frame.args
	local parent_args = frame:getParent().args
	local quality_link = args.qualityLink or parent_args.qualityLink
	local diff = tonumber(args.diff or parent_args.diff)
	local min_value = tonumber(args.minValue or parent_args.minValue) or 0
	local step = tonumber(args.step or parent_args.step) or 10

    local embedcontext = frame:callParserFunction('#var', 'embedcontext') ~= ''
    return narrow_challenge(quality_link, diff, min_value, step, embedcontext)
end

function p.broad_challenge(frame)
	local args = frame.args
	local parent_args = frame:getParent().args
	local quality_link = args.qualityLink or parent_args.qualityLink
	local diff = tonumber(args.diff or parent_args.diff)
	local scaler = tonumber(args.scaler or parent_args.scaler) or 60

    local embedcontext = frame:callParserFunction('#var', 'embedcontext') ~= ''
    return broad_challenge(quality_link, diff, scaler, embedcontext)
end

return p