Module:Grind/fmt
From Fallen London Wiki
Documentation for this module may be created at Module:Grind/fmt/doc
local p = {}
local util = require('Module:Grind/util')
local html = require('Module:Grind/html')
local GRON = require('Module:Grind/GRON')
local fml = require('Module:Grind/fml')
local OL, _OL = html.OL, html._OL
local UL, _UL = html.UL, html._UL
local LI, _LI = html.LI, html._LI
-- == GRON human-readable formatting ==
-- Checks whether the page exists.
-- Assumes the page is for a quality or an item.
-- Results are cached and reused.
function p.page_exists(frame, cache, name)
if frame.callParserFunction == nil then
return false
end
if cache.exists == nil then
cache.exists = {}
end
if cache.exists[name] == nil then
-- much faster than querying whether the page exists via traditional MediaWiki methods
local game_type = frame:callParserFunction{
name = '#show',
args = {name, '?Has Game Type'}
}
cache.exists[name] = (game_type ~= nil) and (game_type ~= '')
end
return cache.exists[name]
end
-- Checks whether `name` is a pyramidal quality.
-- Results are cached and reused.
local function is_pyramidal(frame, cache, name)
if frame.callParserFunction == nil then
return false
end
if cache.pyramidal == nil then
cache.pyramidal = {}
end
if cache.pyramidal[name] == nil then
local increase_type = frame:callParserFunction{
name = '#show',
args = {name, '?Increase Type'}
}
cache.pyramidal[name] = increase_type == 'Pyramidal'
end
return cache.pyramidal[name]
end
-- Evaluates either:
-- * {{IL|name}} or
-- * {{IL|name|amount}}.
-- Results are cached and reused.
function p.get_il(frame, cache, name, amount)
if not p.page_exists(frame, cache, name) then
if amount ~= nil then
return amount .. ' ' .. name
else
return name
end
end
if amount ~= nil then
if cache.il_amount == nil then
cache.il_amount = {}
end
if cache.il_amount[name] == nil then
cache.il_amount[name] = {}
end
if cache.il_amount[name][amount] == nil then
cache.il_amount[name][amount] = frame:expandTemplate{
title = 'IL',
args = {name, amount}
}
end
return cache.il_amount[name][amount]
else
if cache.il == nil then
cache.il = {}
end
if cache.il[name] == nil then
cache.il[name] = frame:expandTemplate{
title = 'IL',
args = {name}
}
end
return cache.il[name]
end
end
-- Evaluates {{IL|name|Appearance=Airs}}
local function get_airs_il(frame, cache, name)
if cache.il_airs == nil then
cache.il_airs = {}
end
if cache.il_airs[name] == nil then
cache.il_airs[name] = frame:expandTemplate{
title = 'IL',
args = {name, Appearance='Airs'}
}
end
return cache.il_airs[name]
end
-- Returns: the string; whether the last layer has binary +/-.
local function format_formula_tree(frame, cache, tree)
local tok_data, tok_type = unpack(tree)
if tok_type == 'num' then
return tostring(tok_data), false
elseif tok_type == 'var' then
local name = tok_data
if name:sub(1, 6) == 'Input:' then
name = name:sub(7)
local _
name, _ = util.normalise_input(name)
return '{' .. name .. '}', false
else
return p.get_il(frame, cache, tok_data), false
end
elseif tok_type == 'unary' then
local op, inner = unpack(tok_data)
local inner_str, inner_esc = format_formula_tree(frame, cache, inner)
if inner_esc then
return op .. '(' .. inner_str .. ')', false
else
return op .. inner_str, false
end
elseif tok_type == 'binary' then
local op, a, b = unpack(tok_data)
local a_str, a_esc = format_formula_tree(frame, cache, a)
local b_str, b_esc = format_formula_tree(frame, cache, b)
local esc = op:match('[+-]') == op
local s = ''
if op:match('[+-]') == op then
a_esc = false
b_esc = false
end
if a_esc then
s = s .. '(' .. a_str .. ')'
else
s = s .. a_str
end
s = s .. ' ' .. op .. ' '
if b_esc then
s = s .. '(' .. b_str .. ')'
else
s = s .. b_str
end
return s, esc
elseif tok_type == 'func_call' then
local func = tok_data[1]
if func == 'random.range' and #tok_data == 3 then
local a = tok_data[2]
local b = tok_data[3]
local a_str, a_esc = format_formula_tree(frame, cache, a)
local b_str, b_esc = format_formula_tree(frame, cache, b)
local s = '['
if a_esc then
s = s .. '(' .. a_str .. ')'
else
s = s .. a_str
end
s = s .. ' to '
if b_esc then
s = s .. '(' .. b_str .. ')'
else
s = s .. b_str
end
s = s .. ']'
return s, false
else
local s = func .. '('
for i = 2, #tok_data do
if i > 2 then
s = s .. ', '
end
local arg = tok_data[i]
local arg_str, arg_esc = format_formula_tree(frame, cache, arg)
s = s .. arg_str
end
s = s .. ')'
return s, false
end
else
return '(unknown token of type ' .. tok_type .. ')', false
end
end
-- Formats a formula.
local function format_formula(frame, cache, s)
local tree, err = fml.parse(s)
if err then
return html.span('(formula error: ' .. err .. ')', html.red, true)
end
local s = format_formula_tree(frame, cache, tree)
return html.span(s, html.orange)
end
-- Formats something that is supposed to be a string.
local function format_string(frame, cache, gron)
if type(gron) == 'string' then
return gron
end
if type(gron) ~= 'table' then
return html.span('(invalid string)', html.red, true)
end
local gtype = gron[1]
if type(gtype) ~= 'string' then
return html.span('(invalid string: ' .. GRON.encode(gron) .. ')', html.red, true)
end
if gtype == 'input' then
if type(gron[2]) == 'string' then
return html.span('(input required: ' .. gron[2] .. ')', html.orange)
else
return html.span('(undefined input)', html.red, true)
end
elseif gtype == 'req' then
local s = '(depending on '
local target = gron['target']
local ranges = gron['ranges']
if type(target) ~= 'string' then
return html.span('(invalid target in (req))', html.red, true)
end
s = s .. p.get_il(frame, cache, target) .. ': '
if type(ranges) ~= 'table' then
return html.span('(invalid ranges in (req))', html.red, true)
end
for i = 2, #gron do
if i > 2 then
s = s .. '; '
end
local item = gron[i]
local range_i = ranges[i - 1]
s = s .. 'with ' .. tostring(range_i) .. ': ' .. format_string(frame, cache, item)
end
s = s .. ')'
return html.span(s, html.orange)
else
return html.span('(invalid value of type ' .. gtype .. ')', html.red, true)
end
end
-- Formats something that is supposed to be a number.
local function format_number(frame, cache, gron)
if type(tonumber(gron)) == 'number' then
return util.f2s(tonumber(gron))
end
if type(gron) ~= 'table' then
return html.span('(invalid number)', html.red, true)
end
local gtype = gron[1]
if type(gtype) ~= 'string' then
return html.span('(invalid number: ' .. GRON.encode(gron) .. ')', html.red, true)
end
if gtype == 'input' then
if type(gron[2]) == 'string' then
local x_string = ''
if type(tonumber(gron['x'])) == 'number' and tonumber(gron['x']) ~= 1 then
x_string = '*' .. tostring(gron['x'])
end
return html.span('(input required: ' .. gron[2] .. ')' .. x_string, html.orange)
else
return html.span('(undefined input)', html.red, true)
end
elseif gtype == 'req' then
local s = '(depending on '
local target = gron['target']
local ranges = gron['ranges']
if type(target) ~= 'string' then
return html.span('(invalid target in (req))', html.red, true)
end
s = s .. p.get_il(frame, cache, target) .. ': '
if type(ranges) ~= 'table' then
return html.span('(invalid ranges in (req))', html.red, true)
end
for i = 2, #gron do
if i > 2 then
s = s .. '; '
end
local item = gron[i]
local range_i = ranges[i - 1]
s = s .. 'with ' .. tostring(range_i) .. ': ' .. format_number(frame, cache, item)
end
s = s .. ')'
return html.span(s, html.orange)
elseif gtype == 'formula' then
local s = gron[2]
if type(s) ~= 'string' then
return html.span('(empty formula)', html.red, true)
end
return format_formula(frame, cache, s)
elseif gtype == 'err' then
return html.span('(error: ' .. tostring(gron[2]) .. ')', html.red, true)
else
return html.span('(invalid value of type ' .. gtype .. ')', html.red, true)
end
end
-- Formats action effect in a human-readable format.
local function format_action_effect(frame, cache, effect, a)
if type(effect) ~= 'table' then
return html.span('not a table:' .. tostring(effect), html.red, true)
end
local s = ''
local first = true
for resource, delta in pairs(effect) do
if not first then
s = s .. ', '
else
first = false
end
local resource, delta = resource, delta
local delta_formatted = format_number(frame, cache, delta)
if is_pyramidal(frame, cache, resource) then
delta_formatted = delta_formatted .. ' CP'
else
delta_formatted = delta_formatted .. ' x'
end
if delta_formatted:sub(1, 1) ~= '-' and type(delta) ~= 'table' then
delta_formatted = '+' .. delta_formatted
end
s = s .. p.get_il(frame, cache, resource, delta_formatted)
end
if tonumber(a) ~= nil then
if not first then
s = s .. ', '
end
s = s .. util.f2s(tonumber(a)) .. ' action'
if tonumber(a) ~= 1 then
s = s .. 's'
end
end
s = s .. '.'
return s
end
-- Formats GRON in a human-readable format.
function p.format_gron(frame, cache, gron)
if type(gron) ~= 'table' then
return 'Not a table: ' .. tostring(gron)
end
local gtype = gron[1]
local s = ''
if gtype == 'error' then
if type(gron[2]) == 'string' then
s = html.span('Error! ' .. gron[2] .. '.', html.red, true)
else
s = html.span('Error! No error information available.', html.red)
end
end
if gtype == 'nop' then
s = 'Do nothing.'
end
if gtype == 'input' then
s = html.span('Input required: ' .. gron[2], html.orange, true)
end
if gtype == 'action' then
local title = gron['title']
local link = gron['link']
local challenge = gron['challenge']
local a = gron['a'] or 1
local comment = gron['comment']
local success = gron['success'] or {}
local failure = gron['failure'] or {}
local alt_success = gron['alt_success'] or {}
local alt_failure = gron['alt_failure'] or {}
local alt_success_p = gron['alt_success_p'] or '0'
local alt_failure_p = gron['alt_failure_p'] or '0'
local expected = gron['expected'] or {}
local a_expected = gron['a_expected'] or a
local prob = gron['p']
if title then
link = link or (p.page_exists(frame, cache, title) and title)
if link then
s = s .. '[[' .. link .. '|' .. title .. ']]'
else
s = s .. title
end
end
if challenge then
s = s .. ' ('
local first = true
for quality, info in pairs(challenge) do
if not first then
s = s .. ', '
end
first = false
local level, ctype
if type(info) == 'string' or type(info) == 'number' then
level, ctype, step = info, 'broad', nil
elseif type(info) == 'table' then
if type(info[1]) == 'string' and tonumber(info[1]) == nil then
level = format_number(frame, cache, info)
ctype, step = 'broad', nil
else
level = format_number(frame, cache, info[1])
ctype = info[2]
step = info[3] or '10'
end
else
level = html.span('(invalid challenge: ' .. tostring(info) .. ')', html.red)
ctype = 'broad'
step = '10'
end
local quality_il = p.get_il(frame, cache, quality)
s = s .. quality_il .. ' ' .. level
if quality == 'Luck' then
s = s .. '%'
elseif ctype ~= 'broad' then
s = s .. ' (narrow'
if step ~= '10' then
s = s .. ', ' .. step .. '% step'
end
s = s .. ')'
end
end
s = s .. ' challenge'
if prob ~= nil then
s = s .. string.format(': %.2f', prob * 100) .. '%'
end
s = s .. ')'
end
if s ~= '' and (s:sub(-1, -1) == ')' or (title or ''):sub(-1, -1) ~= '.') then
s = s .. '. '
end
if a ~= 1 and a ~= '1' then
s = s .. 'Action cost: ' .. format_number(frame, cache, a) .. '. '
end
if comment ~= nil then
s = s .. format_string(frame, cache, comment)
end
local p_alt_s = tonumber(alt_success_p)
if p_alt_s ~= nil then
p_alt_s = p_alt_s / 100
end
local p_alt_f = tonumber(alt_failure_p)
if p_alt_f ~= nil then
p_alt_f = p_alt_f / 100
end
local need_success = (prob == nil) or (prob > 0)
local need_failure = (prob == nil) or (prob < 1)
local need_alt_success = p_alt_s > 0
local need_alt_failure = p_alt_f > 0
local need_expected = util.b2n(need_success) + util.b2n(need_failure) + util.b2n(need_alt_success) + util.b2n(need_alt_failure) > 1
local has_effect = (next(success) or next(alt_success) or next(failure) or next(alt_failure)) ~= nil
if has_effect then
s = s .. UL
end
if need_success and next(success) ~= nil then
s = s .. LI .. 'Success: '
s = s .. format_action_effect(frame, cache, success)
s = s .. _LI
end
if need_success and need_alt_success and next(alt_success) ~= nil then
s = s .. LI .. 'Alternative success (' .. string.format('%.2f', p_alt_s * 100) .. '%): '
s = s .. format_action_effect(frame, cache, alt_success)
s = s .. _LI
end
if need_failure and next(failure) ~= nil then
s = s .. LI .. 'Failure: '
s = s .. format_action_effect(frame, cache, failure)
s = s .. _LI
end
if need_failure and need_alt_failure and next(alt_failure) ~= nil then
s = s .. LI .. 'Alternative failure (' .. string.format('%.2f', p_alt_f * 100) .. '%): '
s = s .. format_action_effect(frame, cache, alt_failure)
s = s .. _LI
end
if need_expected and next(expected) ~= nil then
s = s .. LI .. 'Expected action effect: '
if tonumber(a_expected) ~= tonumber(a) then
s = s .. format_action_effect(frame, cache, expected, a_expected)
else
s = s .. format_action_effect(frame, cache, expected)
end
s = s .. _LI
end
if has_effect then
s = s .. _UL
end
end
if gtype == 'seq' then
s = s .. OL
for i = 2, #gron do
local item = gron[i]
s = s .. LI .. p.format_gron(frame, cache, item) .. _LI
end
s = s .. _OL
end
if gtype == 'airs' then
local range = gron['range'] or '1-100'
local ranges = gron['ranges']
local target = gron['target']
local target_il
if target then
target_il = get_airs_il(frame, cache, target)
s = s .. 'Depending on '
else
target_il = 'Airs'
end
s = s .. target_il .. ' (' .. range .. '):' .. UL
for i = 2, #gron do
local item = gron[i]
local range_i = ranges[i - 1]
s = s .. LI
if target then
s = s .. target_il .. ' '
end
s = s .. tostring(range_i) .. ': ' .. p.format_gron(frame, cache, item)
s = s .. _LI
end
s = s .. _UL
end
if gtype == 'req' then
local req = gron['target']
local ranges = gron['ranges']
s = s .. 'Depending on '
s = s .. p.get_il(frame, cache, req) .. ':' .. UL
for i = 2, #gron do
local item = gron[i]
local range_i = ranges[i - 1]
s = s .. LI .. range_i .. ': ' .. p.format_gron(frame, cache, item) .. _LI
end
s = s .. _UL
end
if gtype == 'pswitch' then
local target = gron['target']
local ranges = gron['ranges']
local action = gron['action']
local prob = gron['p']
s = s .. 'Do the following to collect '
s = s .. p.get_il(frame, cache, target)
s = s .. ':' .. UL .. LI .. p.format_gron(frame, cache, action) .. _LI .. _UL
s = s .. 'Then, depending on '
s = s .. p.get_il(frame, cache, target)
s = s .. ' progress, do the following:' .. UL
for i = 2, #gron do
local item = gron[i]
local range_i = ranges[i - 1]
s = s .. LI .. tostring(range_i)
if prob ~= nil then
local p_i = prob[i - 1]
s = s .. ' (' .. string.format('%.2f', p_i * 100) .. '%)'
end
s = s .. ': ' .. p.format_gron(frame, cache, item) .. _LI
end
s = s .. _UL
end
if gtype == 'best' then
s = s .. 'Choose the best option:' .. UL
for i = 2, #gron do
local item = gron[i]
s = s .. LI .. p.format_gron(frame, cache, item) .. _LI
end
s = s .. _UL
end
if gtype == 'gate' then
local target = gron['target']
local action = gron['action']
local resolution = gron['resolution']
local times = gron['times']
local reset = gron['reset']
local must_succeed = gron['must_succeed']
local failure_cost = tonumber(gron['failure_cost']) or 0
local attempts = tonumber(gron['attempts']) or 1
local resource, level, resource_il
if type(target) ~= 'table' then
resource_il = html.span('(invalid target)', html.red, true)
level = nil
else
resource, level = target[1], target[2]
resource_il = p.get_il(frame, cache, resource)
end
s = s .. 'Collect ' .. resource_il .. ' ' .. format_number(frame, cache, level)
if is_pyramidal(frame, cache, resource) then
s = s .. ' CP'
end
if util.s2b(must_succeed) and failure_cost ~= 0 and failure_cost ~= '0' then
s = s .. ' (failure later will remove '
s = s .. resource_il .. ' ' .. format_number(frame, cache, failure_cost) .. ')'
end
if util.s2b(reset) then
s = s .. ' (will be reset)'
end
s = s .. '. '
s = s .. 'For that, do the following'
if times == 1 then
s = s .. ' once'
elseif times == 1/0 then
s = s .. ' again and again and again'
elseif times ~= nil then
s = s .. ' ' .. util.f2s(times) .. ' times'
end
s = s .. ':' .. UL
if type(action) == 'table' and action[1] == 'seq' then
s = s .. p.format_gron(frame, cache, action)
else
s = s .. LI .. p.format_gron(frame, cache, action) .. _LI
end
s = s .. _UL
if resolution ~= nil then
s = s .. 'Then, do the following'
if attempts > 1 then
s = s .. ' (expect ' .. util.f2s(attempts) .. ' attempts)'
end
s = s .. ':' .. UL
if type(resolution) == 'table' and resolution[1] == 'seq' then
s = s .. p.format_gron(frame, cache, resolution)
else
s = s .. LI .. p.format_gron(frame, cache, resolution) .. _LI
end
s = s .. _UL
end
end
if gtype == 'filter' then
local action = gron[2]
s = s .. p.format_gron(frame, cache, action)
end
if gtype == 'sell' then
local action = gron['action']
local market = gron['market']
-- TODO: check for more possible unexpected non-effect entries
s = s .. 'Sell resources'
if type(market) == 'string' then
s = s .. ' at ' .. market
end
s = s .. ':' .. UL
for sell_resource, sell_effect in pairs(gron) do
if type(sell_resource) == 'string'
and sell_resource ~= 'action'
and sell_resource ~= 'market'
and sell_resource ~= 'optimise' then
s = s .. LI .. p.get_il(frame, cache, sell_resource)
s = s .. ': ' .. format_action_effect(frame, cache, sell_effect)
s = s .. _LI
end
end
s = s .. _UL
s = s .. 'For that, do the following:' .. UL
if type(action) == 'table' and action[1] == 'seq' then
s = s .. p.format_gron(frame, cache, action)
else
s = s .. LI .. p.format_gron(frame, cache, action) .. _LI
end
s = s .. _UL
end
return s
end
-- Formats the expected effect in a human-readable format.
function p.format_expected_effect(frame, expected_effect, cache)
local s = util.f2s(expected_effect.a) .. ' action'
if tonumber(expected_effect.a) ~= 1 then
s = s .. 's'
end
for resource, delta in pairs(expected_effect.effect) do
s = s .. ', '
local resource, delta = resource, delta
local delta_formatted = util.f2s(delta)
if is_pyramidal(frame, cache, resource) then
delta_formatted = delta_formatted .. ' CP'
else
delta_formatted = delta_formatted .. ' x'
end
if delta_formatted:sub(1, 1) ~= '-' then
delta_formatted = '+' .. delta_formatted
end
s = s .. p.get_il(frame, cache, resource, delta_formatted)
end
return s
end
return p