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