Module:Sandbox/Yestarhumeler/Grind
From Fallen London Wiki
Documentation for this module may be created at Module:Sandbox/Yestarhumeler/Grind/doc
local p = {}
local function table_to_str(t)
if type(t) ~= 'table' then
return tostring(t)
end
local s = '{'
for k, v in pairs(t) do
if type(k) == 'string' then
s = s .. '"' .. k .. '"'
else
s = s .. k
end
s = s .. ': ' .. table_to_str(v) .. ','
end
return s .. '}'
end
local function find_match(s, initial_pos)
local pos = initial_pos
local context = {s:sub(pos, pos)}
while #context > 0 do
pos = pos + 1
if pos > #s then
return nil
end
local last = context[#context]
local c = s:sub(pos, pos)
if c == '"' then
if last == '"' then
table.remove(context)
else
table.insert(context, '"')
end
end
if last == '(' and c == '(' then
table.insert(context, '(')
end
if last == '(' and c == ')' then
table.remove(context)
end
end
return pos
end
local function strip_whitespaces(str)
local s = '' .. str
while s:sub(1, 1):match('%s') do
s = s:sub(2)
end
while s:sub(-1, -1):match('%s') do
s = s:sub(1, -2)
end
return s
end
local function skip_whitespaces(s, initial_pos)
local pos = initial_pos
while s:sub(pos, pos):match('%s') do
pos = pos + 1
end
return pos
end
-- Grind Object Notation.
local function parse_gron(s)
local gron = {}
local pos = 1 -- string position
local valc = 0 -- numbered values
pos = skip_whitespaces(s, pos)
-- expect (
if s:sub(pos, pos) ~= '(' then
return nil
end
local gron_end = find_match(s, pos)
if gron_end == nil then
return nil
end
pos = pos + 1
pos = skip_whitespaces(s, pos)
-- parse entries
while pos < gron_end do
pos = skip_whitespaces(s, pos)
local key = ''
local val = nil
local val_tuple = nil
-- parse the key
local c = s:sub(pos, pos)
if c == '"' then
local key_end = find_match(s, pos)
key = s:sub(pos + 1, key_end - 1)
pos = key_end + 1
pos = skip_whitespaces(s, pos)
elseif c == '(' then
local key_end = find_match(s, pos)
key = parse_gron(s:sub(pos, key_end))
pos = key_end + 1
pos = skip_whitespaces(s, pos)
else
key = ''
while c ~= ':' and c ~= ';' and c ~= ')' do
key = key .. c
pos = pos + 1
c = s:sub(pos, pos)
end
key = strip_whitespaces(key)
end
local correct = false
c = s:sub(pos, pos)
if c == ')' then
-- the last entry is a numbered value
valc = valc + 1
gron[valc] = key
break
end
if c == ';' then
-- the entry is a numbered value
correct = true
valc = valc + 1
gron[valc] = key
pos = pos + 1
end
if c == ':' then
-- the entry is a key-values pair
correct = true
pos = pos + 1
pos = skip_whitespaces(s, pos)
val = ''
c = s:sub(pos, pos)
while c ~= ';' and c ~= ')' do
local skip = false
if c == ',' then
if val_tuple == nil then
val_tuple = {}
end
table.insert(val_tuple, val)
val = ''
pos = pos + 1
pos = skip_whitespaces(s, pos)
c = s:sub(pos, pos)
skip = true
end
if not skip and c == '(' then
local val_gron_end = find_match(s, pos)
local val_gron = s:sub(pos, val_gron_end)
val = parse_gron(val_gron)
pos = val_gron_end + 1
pos = skip_whitespaces(s, pos)
c = s:sub(pos, pos)
if c ~= ',' and c ~= ';' and c ~= ')' then
return nil
end
skip = true
end
if not skip and c == '"' then
local val_end = find_match(s, pos)
val = s:sub(pos + 1, val_end - 1)
pos = val_end + 1
pos = skip_whitespaces(s, pos)
c = s:sub(pos, pos)
if c ~= ',' and c ~= ';' and c ~= ')' then
return nil
end
skip = true
end
if not skip then
while c ~= ',' and c ~= ';' and c ~= ')' do
val = val .. c
pos = pos + 1
c = s:sub(pos, pos)
end
val = strip_whitespaces(val)
end
end
-- the remaining val
if val ~= nil and val_tuple ~= nil then
table.insert(val_tuple, val)
val = nil
end
if val_tuple ~= nil then
gron[key] = val_tuple
else
gron[key] = val
end
pos = pos + 1
end
if not correct then
return nil
end
end
return gron
end
local function eval_check(quality, level, broad, data)
if quality == 'Luck' then
return level / 100
end
local actual_level = data[quality] or 0
if broad then
if level == 0 then
return 1
end
return math.min(1, 0.6 * actual_level / level)
else
local step = 0.1
return math.max(step, math.min(1, 0.6 + (actual_level - level) * step))
end
end
local function effect_mul(gron, c)
local effect = {}
for resource, delta in pairs(gron) do
effect[resource] = delta * c
end
return effect
end
local function effect_sum(gron1, gron2)
local effect = {}
for resource, delta in pairs(gron1) do
effect[resource] = delta
end
for resource, delta in pairs(gron2) do
if effect[resource] == nil then
effect[resource] = 0
end
effect[resource] = effect[resource] + delta
end
return effect
end
local function expected_effect(a, effect)
return {
a=a,
effect=effect
}
end
local function resource_per_action(expected_effect, resource)
local a = expected_effect.a
local effect = expected_effect.effect
local r = effect[resource] or 0
if r == 0 then
return 0
end
-- 1/0 is inf, I'm fine with that
return r / a
end
local function parse_range(range)
local min, max = range:gmatch('(%d+)%-%(%d+)')()
return tonumber(min), tonumber(max)
end
-- Returns: expected effect, gron
local function optimise_gron(gron, resource, data)
local gtype = gron[1]
if gtype == 'action' then
local challenge = gron['challenge']
local a = gron['a'] or 1
local success = gron['success'] or {}
local failure = gron['failure'] or {}
if challenge == nil then
return expected_effect(a, success), gron
end
local quality, level, ctype = challenge[1], challenge[2], challenge[3]
ctype = ctype or 'broad'
local p = eval_check(quality, level, ctype == 'broad', data)
local effect = effect_sum(
effect_mul(success, p),
effect_mul(failure, 1 - p)
)
return expected_effect(a, effect), gron
end
if gtype == 'seq' then
local optimal_gron = {'seq'}
local effect = {}
local a = 0
for i = 2, #gron do
local effect_i, gron_i = optimise_gron(gron[i], resource, data)
effect = effect_sum(effect, effect_i.effect)
a = a + effect_i.a
table.insert(optimal_gron, gron_i)
end
return expected_effect(a, effect), optimal_gron
end
if gtype == 'airs' then
local range = gron['range'] or '0-100'
local ranges = gron['ranges']
local rmin, rmax = parse_range(range)
local optimal_gron = {'airs', range=range, ranges=ranges}
local effect = {}
local a = 0
for i = 2, #gron do
local effect_i, gron_i = optimise_gron(gron[i], resource, data)
local rmin_i, rmax_i = parse_range(ranges[i - 1])
local p = (rmax_i - rmin_i + 1) / (rmax - rmin + 1)
effect = effect_sum(effect, effect_mul(effect_i.effect, p))
a = a + effect_i.a
table.insert(optimal_gron, gron_i)
end
return expected_effect(a, effect), optimal_gron
end
if gtype == 'best' then
local optimal_gron = nil
local expected_effect = {}
local best_rpa = -1 / 0
for i = 2, #gron do
local effect_i, gron_i = optimise_gron(gron[i], resource, data)
local rpa = resource_per_action(effect_i, resource)
if rpa > best_rpa then
expected_effect = effect_i
optimal_gron = gron_i
best_rpa = rpa
end
end
return expected_effect, optimal_gron
end
if gtype == 'gate' then
local target = gron['target']
local action = gron['action']
local resolution = gron['resolution']
local g_a = 0
local effect_r = {}
local gron_r = nil
if resolution ~= nil then
effect_r, gron_r = optimise_gron(resolution, resource, data)
g_a = effect_r.a
end
local g_resource, g_level = target[1], target[2]
local effect_a, gron_a = optimise_gron(action, g_resource, data)
local rpa = resource_per_action(effect_a, g_resource)
local a = g_level / rpa + g_a
local gron = {
'gate',
title=gron['title'],
target=target,
action=gron_a,
resolution=gron_r
}
return expected_effect(a, effect_r.effect), gron
end
-- error
return nil
end
local function page_exists(frame, name)
local game_type = frame:callParserFunction{
name = '#show',
args = {name, '?Has Game Type'}
}
return game_type ~= nil and game_type ~= ''
end
local function is_pyramidal(frame, name)
local increase_type = frame:callParserFunction{
name = '#show',
args = {name, '?Increase Type'}
}
return increase_type == 'Pyramidal'
end
local function format_gron(frame, gron)
local gtype = gron[1]
local s = ''
if gtype == 'action' then
local title = gron.title
local challenge = gron['challenge']
local a = gron['a'] or 1
local success = gron['success'] or {}
local failure = gron['failure'] or {}
if title then
if page_exists(frame, title) then
s = s .. '[[' .. title .. ']]'
else
s = s .. title
end
end
if challenge then
local quality, level, ctype = challenge[1], challenge[2], challenge[3]
ctype = ctype or 'broad'
if ctype ~= 'broad' then
ctype = 'narrow'
end
local quality_il = quality
if page_exists(frame, quality) then
quality_il = frame:expandTemplate{
title = 'IL',
args = {quality}
}
end
s = s .. ' (' .. quality_il .. ' ' .. level .. ' (' .. ctype .. '))'
end
if s ~= '' then
s = s .. '. '
end
if a ~= 1 then
s = s .. 'Action cost: ' .. a .. '. '
end
if next(success) ~= nil then
s = s .. 'Success: '
local first = true
for resource, delta in pairs(success) do
if not first then
s = s .. ', '
else
first = false
end
local resource, delta = resource, delta
if resource == 'Echo' then
resource = 'Penny'
delta = delta * 100
end
local delta_formatted
if is_pyramidal(frame, resource) then
delta_formatted = delta .. ' CP'
else
delta_formatted = delta .. ' x'
end
if delta_formatted:sub(1, 1) ~= '-' then
delta_formatted = '+' .. delta_formatted
end
local resource_il = delta_formatted .. ' ' .. resource
if page_exists(frame, resource) then
resource_il = frame:expandTemplate{
title = 'IL',
args = {resource, delta_formatted}
}
end
s = s .. resource_il
end
s = s .. '. '
end
if next(failure) ~= nil then
s = s .. 'Failure: '
local first = true
for resource, delta in pairs(failure) do
if not first then
s = s .. ', '
else
first = false
end
local resource, delta = resource, delta
if resource == 'Echo' then
resource = 'Penny'
delta = delta * 100
end
local delta_formatted
if is_pyramidal(frame, resource) then
delta_formatted = delta .. ' CP'
else
delta_formatted = delta .. ' x'
end
if delta_formatted:sub(1, 1) ~= '-' then
delta_formatted = '+' .. delta_formatted
end
local resource_il = delta_formatted .. ' ' .. resource
if page_exists(frame, resource) then
resource_il = frame:expandTemplate{
title = 'IL',
args = {resource, delta_formatted}
}
end
s = s .. resource_il
end
s = s .. '. '
end
if s == '' then
s = 'Action cost: 1.'
end
end
if gtype == 'seq' then
s = s .. '<ul>'
for i = 2, #gron do
local item = gron[i]
s = s .. '<li>' .. format_gron(frame, item) .. '</li>'
end
s = s .. '</ul>'
end
if gtype == 'airs' then
local range = gron['range'] or '0-100'
local ranges = gron['ranges']
s = s .. 'Airs (' .. range .. '):<ul>'
for i = 2, #gron do
local item = gron[i]
local range_i = ranges[i - 1]
s = s .. '<li>' .. range_i .. ': ' .. format_gron(frame, 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>' .. format_gron(frame, item) .. '</li>'
end
s = s .. '</ul>'
end
if gtype == 'gate' then
local target = gron['target']
local action = gron['action']
local resolution = gron['resolution'] or {}
local resource, level = target[1], target[2]
if resource == 'Echo' then
resource = 'Penny'
level = level * 100
end
local resource_il = resource
if page_exists(frame, resource) then
resource_il = frame:expandTemplate{
title = 'IL',
args = {resource}
}
end
s = s .. 'Collect ' .. resource_il .. ' ' .. level .. '. '
s = s .. 'For that, do the following:<ul><li>' .. format_gron(frame, action) .. '</li></ul>'
if resolution ~= nil then
s = s .. 'Then, do the following:<ul><li>' .. format_gron(frame, resolution) .. '</li></ul>'
end
end
return s
end
local function format_expected_effect(frame, expected_effect)
local s = expected_effect.a .. ' actions'
for resource, delta in pairs(expected_effect.effect) do
s = s .. ', '
local resource, delta = resource, delta
if resource == 'Echo' then
resource = 'Penny'
delta = delta * 100
end
local delta_formatted
if is_pyramidal(frame, resource) then
delta_formatted = delta .. ' CP'
else
delta_formatted = delta .. ' x'
end
if delta_formatted:sub(1, 1) ~= '-' then
delta_formatted = '+' .. delta_formatted
end
local resource_il = delta_formatted .. ' ' .. resource
if page_exists(frame, resource) then
resource_il = frame:expandTemplate{
title = 'IL',
args = {resource, delta_formatted}
}
end
s = s .. resource_il
end
return s .. '.'
end
local function collect_args(frame, max_depth, depth)
depth = depth or 0
if max_depth ~= nil and depth >= max_depth then
return {}
end
if frame == nil then
return {}
end
local args = collect_args(frame:getParent(), max_depth, depth + 1)
for k, v in pairs(frame.args) do
if type(k) == 'string' then
args[k] = tonumber(v)
end
end
return args
end
function p.rpa(frame)
local resource = frame.args[1]
local grind = frame.args[2]
local gron = parse_gron(grind)
if gron == nil then
return 'Error parsing GRON'
end
local data = {}
for k, v in pairs(frame.args) do
if type(k) == 'string' then
data[k] = tonumber(v)
end
end
-- TODO: parent args
local expected_effect, optimal_gron = optimise_gron(gron, resource, data)
return '' .. resource_per_action(expected_effect, resource)
end
function p.analyse(frame)
local text = frame.args[1]
local resource = frame.args[2]
local grind = frame.args[3]
local gron = parse_gron(grind)
if gron == nil then
return 'Error parsing GRON'
end
local data = collect_args(frame, 4)
local expected_effect, optimal_gron = optimise_gron(gron, resource, data)
local rpa = resource_per_action(expected_effect, resource)
local formatted_gron = format_gron(frame, optimal_gron)
local formatted_effect = format_expected_effect(frame, expected_effect)
text = text:gsub('#RPA#', rpa)
text = text:gsub('#OPTIMAL#', formatted_gron)
text = text:gsub('#EFFECT#', formatted_effect)
return text
end
function p.format_gron(frame)
local grind = frame.args[1]
local gron = parse_gron(grind)
if gron == nil then
return 'Error parsing GRON'
end
local formatted_effect = format_gron(frame, gron)
return formatted_effect
end
return p