Module:Grind/proc
From Fallen London Wiki
Documentation for this module may be created at Module:Grind/proc/doc
local p = {}
local util = require('Module:Grind/util')
local GRON = require('Module:Grind/GRON')
local fml = require('Module:Grind/fml')
-- == GRON processing ==
-- Checks if the GRON might have `resource` as an effect output.
-- Even if the function returns true, it may happen to be otherwise on evaluation.
-- The primary goal is to find whether it cannot be in the output under any circumstances.
local function is_source_of(gron, resource)
local gtype = gron[1]
if gtype == 'error' then
-- returning false may cause the error to be deleted
return true
end
if gtype == 'input' then
return true
end
if gtype == 'action' then
local success = gron['success'] or {}
local failure = gron['failure'] or {}
local alt_success = gron['alt_success'] or {}
local alt_failure = gron['alt_failure'] or {}
return (success[resource] or failure[resource] or alt_success[resource] or alt_failure[resource]) ~= nil
end
if gtype == 'seq' or gtype == 'airs' or gtype == 'req' or gtype == 'pswitch' or gtype == 'best' then
for i = 2, #gron do
if is_source_of(gron[i], resource) then
return true
end
end
return false
end
if gtype == 'gate' then
-- could it be in the action?
local target = gron['target']
local g_resource = nil
if type(target) == 'table' then
g_resource = target[1]
end
local preserve_progress = util.s2b(gron['preserve_progress'], false)
local action = gron['action']
local check_action = false
if resource and resource == g_resource then
if preserve_progress then
check_action = true
end
else
check_action = true
end
if check_action and action then
local action_source = is_source_of(action, resource)
if action_source then
return true
end
end
-- could it be in the resolution?
local resolution = gron['resolution']
if resolution == nil then
return false
end
return is_source_of(resolution, resource)
end
if gtype == 'filter' then
local target = gron['target'] or {}
if type(target) == 'string' then
target = {target}
end
if type(target) ~= 'table' then
return false
end
for _, r in ipairs(target) do
if r == resource then
local action = gron[2]
if type(action) ~= 'table' then
return is_source_of(action, resource)
end
end
end
return false
end
if gtype == 'sell' then
local action = gron['action']
if gron[resource] == nil then
-- the resource is not sold, could it be in the action?
if type(action) == 'table' then
if is_source_of(action, resource) then
return true
end
end
end
-- could it be a result of selling?
for k, eff in pairs(gron) do
if type(k) == 'string' and k ~= 'action' and k ~= 'market' and k ~= 'optimise' then
if eff[resource] ~= nil then
-- is it possible?
if type(action) == 'table' then
if is_source_of(action, k) then
return true
end
end
end
end
end
return false
end
return false
end
-- Collects all the tables with reference ids into a cache.
local function scan_refs(gron, ref_cache)
ref_cache = ref_cache or {}
if type(gron) ~= 'table' then
return ref_cache
end
if gron['id'] ~= nil then
ref_cache[gron['id']] = gron
end
for key, val in pairs(gron) do
ref_cache = scan_refs(val, ref_cache)
end
return ref_cache
end
-- Resolves (ref)erences.
-- Handles recursion automatically.
function p.resolve_refs(gron, ref_cache, inside_of)
ref_cache = ref_cache or scan_refs(gron)
inside_of = inside_of or {}
if type(gron) ~= 'table' then
return gron
end
if gron[1] == 'ref' then
local id = gron[2]
if inside_of[id] then
return {'error', '(ref) recursion: ' .. id}
end
local _inside_of = mw.clone(inside_of)
_inside_of[id] = true
local ref = ref_cache[id]
-- additional (ref)s might be inside of the resolved reference
local ref = p.resolve_refs(ref, ref_cache, _inside_of)
return ref
end
local new_gron = {}
for key, val in pairs(gron) do
if key ~= 'id' then
new_gron[key] = p.resolve_refs(val, ref_cache, inside_of)
end
end
return new_gron
end
-- Resolves (req)uirements.
-- `partial`: if true, only resolve those for which data is provided.
function p.resolve_reqs(gron, data, partial)
partial = partial or false
if type(gron) ~= 'table' then
return gron
end
local gtype = gron[1]
if gtype == 'req' then
local target = gron['target']
local is_input = util.s2b(gron['input'], false)
local is_value = util.s2b(gron['value'], false)
if is_input and is_value then
return {'error', '(req).input and (req).value are mutually exclusive'}
end
local is_data = (not is_input) and (not is_value)
if is_data or is_input then
if type(target) ~= 'string' then
return {'error', '(req).target must be a string'}
end
elseif is_value then
-- distributions are not supported
if tonumber(target) == nil then
return {'error', '(req).target must be a number'}
end
end
local ranges = gron['ranges']
if type(ranges) ~= 'table' then
return {'error', '(req).ranges invalid or missing'}
end
if #ranges ~= #gron - 1 then
return {'error', '(req): number of ranges not equal to the number of branches'}
end
if is_data and data[target] == nil and partial then
-- do not evaluate this (req)
for i = 2, #gron do
gron[i] = p.resolve_reqs(gron[i], data, partial)
end
return gron
elseif is_input and data['Input:' .. target] == nil and partial then
-- do not evaluate this (req)
for i = 2, #gron do
gron[i] = p.resolve_reqs(gron[i], data, partial)
end
return gron
else
local val
if is_value then
val = tonumber(target)
else
local key = target
-- TODO: type suffix?
if is_input then
key = 'Input:' .. key
end
val = data[key] or 0
end
for i = 2, #gron do
local range_i = ranges[i - 1]
local rmin_i, rmax_i = util.parse_range(range_i)
if rmin_i == nil or rmax_i == nil then
return {'error', '(req): invalid ranges[' .. (i - 1) .. ']: ' .. tostring(range_i)}
end
if rmin_i <= val and val <= rmax_i then
return p.resolve_reqs(gron[i], data, partial)
end
end
-- All (req) branches failed to match the target value.
return {'nop', avoid='yes'}
end
else
local new_gron = {}
for k, v in pairs(gron) do
new_gron[k] = p.resolve_reqs(v, data, partial)
end
return new_gron
end
end
-- Multiplies what is supposed to be a number by `m`.
-- TODO: handle (req)!
local function multiply_number(gron, m)
if tonumber(gron) ~= nil then
return tonumber(gron) * m
end
if type(gron) ~= 'table' then
return {'err', 'unknown number type: ' .. type(gron)}
end
local gtype = gron[1]
if gtype == 'formula' then
local s = gron[2]
if type(s) ~= 'string' then
return {'err', 'empty formula'}
end
local tree, err = fml.parse(s)
if err then
return {'err', 'formula error: ' .. err}
end
tree = {{'*', tree, {m, 'num'}}, 'binary'}
return {'formula', fml.encode(tree)}
elseif gtype == 'input' then
local i, itype = gron[2], gron['type']
if itype == nil then
i, itype = util.normalise_input(i)
end
return {'input', i, ['type']=itype, x=m}
elseif gtype == 'err' then
return gron
else
return {'err', 'unknown number type: ' .. tostring(gtype)}
end
end
-- Resolves (formula)e before evaluation.
-- Formatting functions cannot display raw distributions.
-- `partial`: if true, only substitute known data.
function p.resolve_formulae(gron, data, partial)
partial = partial or false
if type(gron) ~= 'table' then
return gron
end
local gtype = gron[1]
if gtype == 'formula' then
local s = gron[2]
if type(s) ~= 'string' then
return {'err', 'empty formula'}
end
local tree, err = fml.parse(s)
if err then
return {'err', 'formula error: ' .. err}
end
tree = fml.substitute(tree, data, not partial)
err = fml.find_error(tree)
if err then
return {'err', 'formula evaluation error: ' .. tostring(err)}
elseif tree[2] == 'num' then
return tostring(tree[1])
else
return {'formula', fml.encode(tree)}
end
end
local new_gron = {}
for k, v in pairs(gron) do
new_gron[k] = p.resolve_formulae(v, data, partial)
end
return new_gron
end
-- Or, at least, what was supposed to be a number.
-- It can turn out to be:
-- * (formula)
-- * (req)
-- * (input)
local function preprocess_number(gron)
if type(gron) ~= 'table' then
return gron
end
local gtype = gron[1]
if gtype == 'formula' then
return gron
elseif gtype == 'req' then
for i = 2, #gron do
local gron_i, err_i = preprocess_number(gron[i])
if err_i then
return nil, err_i
end
gron[i] = gron_i
end
return gron
elseif gtype == 'input' then
local name = gron[2]
if type(name) ~= 'string' then
return nil, 'Empty (input)!'
end
local itype = gron['type']
if itype == nil then
name, itype = util.normalise_input(name)
end
return {'input', name, ['type']=itype, ['x']=gron['x']}
end
return nil, 'unknown number type: ' .. tostring(gtype)
end
-- Preprocesses an effect for further usage:
-- * Does Echo->Penny conversion.
function p.preprocess_effect(effect)
if effect == nil then
return nil
end
local new_effect = {}
for resource, delta in pairs(effect) do
-- TODO: use preprocess_number() first
if resource == 'Echo' then
if effect['Penny'] then
new_effect['Penny'] = mw.clone(effect['Penny'])
else
new_effect['Penny'] = multiply_number(delta, 100)
end
else
new_effect[resource] = mw.clone(delta)
end
end
return new_effect
end
-- Prepares GRON for further usage:
-- * Removes branches that make no sense;
-- * Resolves (import)s (with `import_all`, late (import)s as well);
-- * Looks for some of possible errors and indicates them;
-- * Does Echo->Penny conversion.
function p.preprocess_gron(frame, gron, resource, cache, import_all)
import_all = import_all or false
local gtype = gron[1]
if gtype == 'error' then
return gron
end
if gtype == 'nop' then
return gron
end
if gtype == 'input' then
local name = gron[2]
if type(name) ~= 'string' then
return {'error', 'Empty (input)!'}
end
local itype = gron['type']
if itype == nil then
name, itype = util.normalise_input(name)
end
return {'input', name, ['type']=itype, ['x']=gron['x']}
end
if gtype == 'import' then
local title = gron[2]
if util.s2b(gron['late']) and not import_all then
return gron
end
if cache[title] == nil then
local gron_text = frame:callParserFunction{
name = '#show',
args = {title, '?Has grind definition'}
}
if gron_text ~= nil and gron_text ~= '' then
cache[title] = GRON.parse(gron_text)
else
return {'error', '(import) cannot import: ' .. title}
end
end
local imported_gron = mw.clone(cache[title])
imported_gron = p.preprocess_gron(frame, imported_gron, resource, cache, import_all)
local assume = gron['assume'] or {}
if type(assume) ~= 'table' then
return {'error', '(import).assume is invalid'}
end
for k, v in pairs(assume) do
assume[k] = tonumber(v)
end
imported_gron = p.resolve_reqs(imported_gron, assume, true)
imported_gron = p.resolve_formulae(imported_gron, assume, true)
return imported_gron
end
if gtype == 'action' then
local new_gron = mw.clone(gron)
if type(new_gron['a']) == 'table' then
new_gron['a'] = preprocess_number(new_gron['a'])
end
new_gron['success'] = p.preprocess_effect(new_gron['success'])
new_gron['alt_success'] = p.preprocess_effect(new_gron['alt_success'])
new_gron['failure'] = p.preprocess_effect(new_gron['failure'])
new_gron['alt_failure'] = p.preprocess_effect(new_gron['alt_failure'])
local challenge = new_gron['challenge']
if challenge ~= nil and challenge['Echo'] ~= nil then
-- I doubt that will ever happen outside of weird places
-- like the Iron Republic but let's handle this case anyway.
local data = challenge['Echo']
if type(data) == 'table' and (data[2] == 'broad' or data[2] == 'narrow') then
data[1] = multiply_number(data[1], 100)
else
data = multiply_number(data, 100)
end
challenge['Echo'] = nil
challenge['Penny'] = data
end
return new_gron
end
if gtype == 'seq' then
local new_gron = {'seq'}
for i = 2, #gron do
local gron_i = p.preprocess_gron(frame, gron[i], resource, cache, import_all)
if gron_i == nil or gron_i[1] ~= 'seq' then
table.insert(new_gron, gron_i)
else
for j = 2, #gron_i do
table.insert(new_gron, gron_i[j])
end
end
end
return new_gron
end
if gtype == 'airs' then
local new_gron = {'airs'}
new_gron['range'] = gron['range']
new_gron['target'] = gron['target']
if type(gron['ranges']) == 'string' then
gron['ranges'] = {gron['ranges']}
end
new_gron['ranges'] = gron['ranges']
new_gron['ranges'] = {}
local last_item = nil
local last_range = nil
local all_equal = true
for i = 2, #gron do
local item = p.preprocess_gron(frame, gron[i], resource, cache, import_all)
local range = gron['ranges'][i - 1]
if util.tables_equal(last_item, item) then
-- merge ranges instead of adding a new branch, if possible
local last_min, last_max = util.parse_range(last_range)
local min, max = util.parse_range(range)
if last_max ~= nil and min ~= nil and last_max + 1 == min then
if max ~= max + 1 then
range = last_min .. '-' .. max
else
range = last_min .. '-'
end
new_gron['ranges'][#(new_gron['ranges'])] = range
else
table.insert(new_gron, item)
table.insert(new_gron['ranges'], range)
end
else
table.insert(new_gron, item)
table.insert(new_gron['ranges'], range)
end
if all_equal and last_item ~= nil then
all_equal = util.tables_equal(last_item, item)
end
last_item = item
last_range = range
end
if last_item ~= nil and all_equal then
return last_item
else
return new_gron
end
end
if gtype == 'req' then
local new_gron = {'req'}
new_gron['target'] = gron['target']
new_gron['input'] = gron['input']
new_gron['value'] = gron['value']
if type(gron['ranges']) == 'string' then
gron['ranges'] = {gron['ranges']}
end
new_gron['ranges'] = gron['ranges']
if gron['target'] == 'Echo' then
new_gron['target'] = 'Penny'
for i, range in ipairs(new_gron['ranges']) do
new_gron[i] = util.multiply_range(range, 100)
end
end
for i = 2, #gron do
local gron_i = p.preprocess_gron(frame, gron[i], resource, cache, import_all)
table.insert(new_gron, gron_i)
end
return new_gron
end
if gtype == 'pswitch' then
local new_gron = {'pswitch'}
new_gron['target'] = gron['target']
new_gron['action'] = p.preprocess_gron(frame, gron['action'], gron['target'], cache, import_all)
if type(gron['ranges']) == 'string' then
gron['ranges'] = {gron['ranges']}
end
new_gron['ranges'] = gron['ranges']
if gron['target'] == 'Echo' then
new_gron['target'] = 'Penny'
for i, range in ipairs(new_gron['ranges']) do
new_gron[i] = util.multiply_range(range, 100)
end
end
for i = 2, #gron do
local gron_i = p.preprocess_gron(frame, gron[i], resource, cache, import_all)
table.insert(new_gron, gron_i)
end
return new_gron
end
if gtype == 'best' then
local new_gron = {'best'}
for i = 2, #gron do
local gron_i = p.preprocess_gron(frame, gron[i], resource, cache, import_all)
if resource == nil or is_source_of(gron_i, resource) then
if gron_i[1] ~= 'nop' or not util.s2b(gron_i['avoid'], false) then
table.insert(new_gron, gron_i)
end
end
end
if #new_gron == 2 then
return new_gron[2]
elseif #new_gron == 1 then
return {'nop', avoid=yes}
end
return new_gron
end
if gtype == 'gate' then
local new_gron = {'gate'}
new_gron['target'] = gron['target']
new_gron['reset'] = gron['reset']
new_gron['must_succeed'] = gron['must_succeed']
new_gron['failure_cost'] = preprocess_number(gron['failure_cost'])
new_gron['preserve_progress'] = gron['preserve_progress']
new_gron['optimise'] = gron['optimise']
if gron['resolution'] ~= nil then
new_gron['resolution'] = p.preprocess_gron(
frame, gron['resolution'], resource, cache, import_all
)
end
if type(gron['target']) ~= 'table' then
if gron['target'] == nil then
return {'error', '(gate): target undefined'}
else
return {'error', '(gate): target incorrect'}
end
end
local g_resource = gron['target'][1]
if g_resource == nil then
return {'error', '(gate): target[1] undefined'}
end
if tonumber(new_gron['target'][2]) == nil then
return {'error', '(gate): target[2] is not a number: ' .. tostring(gron['target'][2])}
end
if g_resource == 'Echo' then
new_gron['target'][1] = 'Penny'
new_gron['target'][2] = multiply_number(new_gron['target'][2], 100)
if new_gron['failure_cost'] ~= nil then
new_gron['failure_cost'] = multiply_number(new_gron['failure_cost'], 100)
end
g_resource = 'Penny'
end
if not util.s2b(gron['optimise'], true) then
-- this (gate) only tracks its resource
g_resource = resource
end
new_gron['action'] = p.preprocess_gron(frame, gron['action'], g_resource, cache, import_all)
return new_gron
end
if gtype == 'filter' then
local action = gron[2]
if type(action) ~= 'table' then
return {'error', '(filter)[2] incorrect or undefined'}
end
action = p.preprocess_gron(frame, action, resource, cache, import_all)
local target = gron['target'] or {}
if type(target) == 'string' then
target = {target}
end
if type(target) ~= 'table' then
return {'error', '(filter).target incorrect or undefined'}
end
local new_gron = {
'filter',
action,
target=target
}
return new_gron
end
if gtype == 'sell' then
local action = gron['action']
if type(action) ~= 'table' then
return {'error', '(sell).action incorrect or undefined'}
end
if gron['optimise'] == nil then
action = p.preprocess_gron(frame, action, resource, cache, import_all)
else
-- how exactly will optimisation behave on evaluation?
-- we cannot know at this stage, therefore assume nothing specific
action = p.preprocess_gron(frame, action, nil, cache, import_all)
end
if gron['market'] ~= nil and type(gron['market']) ~= 'string' then
return {'error', '(sell).market must be a string'}
end
if gron['optimise'] ~= nil and type(gron['optimise']) ~= 'string' then
return {'error', '(sell).optimise must be undefined or a string'}
end
local new_gron = {
'sell',
action=action,
market=gron['market'],
optimise=gron['optimise']
}
for k, eff in pairs(gron) do
if type(k) == 'string' and k ~= 'action' and k ~= 'market' and k ~= 'optimise' then
new_gron[k] = p.preprocess_effect(eff)
end
end
return new_gron
end
return {'error', 'unknown object of type ' .. tostring(gtype)}
end
-- Substitutes inputs.
function p.compose(gron, args)
if type(gron) ~= 'table' then
return gron
end
local gtype = gron[1]
if gtype == 'input' then
local key = gron[2]
local x = gron['x']
local val = args[key]
if val == nil then
return {'error', '(input): input not provided for ' .. key}
end
if x ~= nil then
return val * x
else
return mw.clone(val)
end
end
for k, v in pairs(gron) do
gron[k] = p.compose(v, args)
end
return gron
end
return p