Module:Sandbox/KickahaOta/QueryDistances
Jump to navigation
Jump to search
You might want to create a documentation page for this module.
Editors can experiment in this module's sandbox and testcases pages.
Please add categories to the /doc subpage. Subpages of this module.
Editors can experiment in this module's sandbox and testcases pages.
Please add categories to the /doc subpage. Subpages of this module.
-- Should we use the sandbox version of some modules?
-- local m_baseutil = require('Module:Util')
-- local use_sandbox = m_baseutil.misc.maybe_sandbox('Sandbox/KickahaOta/QueryDistances')
local use_sandbox = true
local m_util = use_sandbox and require('Module:Util/sandbox') or m_baseutil
local m_game = use_sandbox and mw.loadData('Module:Game/sandbox') or mw.loadData('Module:Game')
local m_cargo = use_sandbox and require('Module:Cargo/sandbox') or require('Module:Cargo')
local getArgs = require('Module:Arguments').getArgs
local m_calculate = {}
function m_calculate.calculate_distances(query_results, args)
--[[
Computes and inserts a new column into a cargo query results table. This column will contain the distance between each
row in the table and a specified origin row. (The distance value will be an integer. The specified origin row
will have a distance value of 0; the rows connected to that row will have a distance value of 1; the rows
connected to those rows will have a distance value of 2; and so on.)
Takes 2 arguments:
query_results: table; required. Table returned from Module:Cargo.query
args - table; required. Association table of arguments:
id_field - string; required. The name of the field containing a row's id for connection purposes.
Example: "passive_skills.id".
connection_ids_field - string; required. The name of the field containing the ids of the rows that a given row connects to.
Example: "passive_skills.connections".
The contents of this field can either be an array of ids or a string containing a delimited list of ids.
connection_ids_delimiter - string; the delimeter separating ids in the connection_ids_field field.
Example: ",".
This parameter is required if the connection_ids_field field contains a string. It is optional and ignored
if the field contains an array.
origin_row_id - string; required. The id of a row that should be used as the starting point of the calculation.
Example: "AscendancyBerserkerStart" to start at the starting node in the Berserker's Ascendancy tree.
distance_field - string; required. the name of the field that should be set to each row's calculated distance from
the starting point.
Examples: "distance"; "passive_skills.connection_distance".
max_distance - integer; optional. The longest route we should calculate.
For instance, if max_distance = 10, then we will only calculate distances to rows that can be reached from the
origin row through 10 or fewer connections. Any nodes that can't be reached within this limit will wind up with
the distance field set to nil.
By default, max_distance is 99. This typically means that any rows that have any connection at all to the
origin row will have their distance fields set to non-nil.
Example: 10
test - boolean; optional. If true, does logging. Defaults to false.
Any additional arguments accepted by remove_unwanted_rows.
Returns: A table that is a copy of the table in query_results, except that each row will have the field identified by
distance_field set to the calculated distance. The field will be created if it does not already exist in each row.
]]
query_results = query_results or error("No results table supplied")
local param_test = args or error("No args supplied")
param_test = args.id_field or error("no id_field supplied")
param_test = args.connection_ids_field or error("no connection_ids_field supplied")
param_test = args.distance_field or error("no distance_field supplied")
param_test = args.origin_row_id or error("no originidrow supplied")
args.max_distance = args.max_distance or 99
test = args.test or false
-- Start by creating three tables:
-- calc_results: A copy of query_results, where we'll eventually insert the computed distances into each row.
-- calc_results_id_map: A table that maps each connection ID to the index of the row in calc_results with that ID.
-- calculables: A queue table containing the indexes of rows in calcResults where we now know the distance for that row, but we
-- haven't yet used that knowledge to set the distances of the other rows connected to it. At first, this should contain
-- a single index -- the index of the row identified by origin_row_id.
local calc_results = {}
local calc_results_id_map = {}
local calculables = {}
for i, row in ipairs(query_results) do
calc_results[i] = row
local rowid = calc_results[i][args.id_field]
if rowid == nil then
error("No id_field")
end
if calc_results_id_map[rowid] ~= nil then
if test then mw.log(string.format("Warning: id_field not unique: id_field='%s', rowid='%s', previous rowid='%s'", args.id_field, rowid, calc_results_id_map[rowid])) end
else
calc_results_id_map[rowid] = i
end
if rowid == args.origin_row_id then
calc_results[i][args.distance_field] = 0
table.insert(calculables, i)
else
calc_results[i][args.distance_field] = nil
end
end
if #calculables == 0 then
error("Origin row not found")
elseif #calculables > 1 then
error("Multiple origin rows found")
end
-- Now we start processing by grabbing the next row from the queue
while #calculables > 0 do
local idx = table.remove(calculables, 1)
local id = calc_results[idx][args.id_field]
local distance_to_this=calc_results[idx][args.distance_field]
if test then mw.log(string.format("Processing node %s with distance %d", id, distance_to_this)) end
-- Check each connected node to see if our current node is the most efficient route we've found to that other node
local outgoing_ids = calc_results[idx][args.connection_ids_field]
if type(outgoing_ids) == "string" then
param_test = args.connection_ids_delimiter or error("no connection_ids_delimiter supplied")
outgoing_ids = m_util.string.split(outgoing_ids, args.connection_ids_delimiter)
end
if type(outgoing_ids) ~= "table" then
error(string.format("Unexpected outgoing id data type '%s' in field '%s' for id '%s'", type(outgoing_ids), args.connection_ids_field, calc_results[idx][args.id_field]))
end
for _,outgoing_id in ipairs(outgoing_ids) do
local idx_connected_row = calc_results_id_map[outgoing_id]
if idx_connected_row == nil then
if test then mw.log(string.format("Outgoing connection %s for row %s not found, ignoring", outgoing_id, calc_results[idx][args.id_field])) end
else
connected_row_current_distance = calc_results[idx_connected_row][args.distance_field]
-- Does the connected node have no distance computed yet? Then the route through the current node is the first
-- route we've found to the connected node; so by definition it's the most efficient we've found.
-- Does the connected node have a distance computed that's higher than our current node's distance + 1? Then
-- we've found a more efficient route than the previous one(s) we found, and we should update it.
-- Does the connected node have a distance computed that's lower than that? Then we already found a more
-- efficient route to that node, and we should leave it alone.
local is_better = false
if (connected_row_current_distance == nil) then
is_better = true
if test then mw.log(string.format("Found first route to %s with distance %d", outgoing_id, distance_to_this + 1)) end
elseif (connected_row_current_distance > distance_to_this + 1) then
is_better = true
if test then mw.log(string.format("Found better route to %s, reducing distance from %d to %d", outgoing_id, connected_row_current_distance, distance_to_this + 1)) end
else
if test then mw.log(string.format("Found route to %s with distance %d, but prior route has better distance %d", outgoing_id, distance_to_this + 1, connected_row_current_distance)) end
end
if is_better then
calc_results[idx_connected_row][args.distance_field] = distance_to_this + 1
-- Add the connected node to the calculables queue, so that we (re)check the nodes connected to it
if (distance_to_this + 1) < args.max_distance then
table.insert(calculables, idx_connected_row)
else
if test then mw.log(string.format("Not checking %s because max_distance reached", outgoing_id)) end
end
end
end
end
end
return m_calculate.remove_unwanted_rows(calc_results,args)
end
function m_calculate.remove_unwanted_rows(calc_results,args)
calc_results = calc_results or error("No results table supplied")
local param_test = args or error("No args supplied")
param_test = args.distance_field or error("no distance_field supplied")
param_test = args.id_field or error("no id_field specified")
test = args.test_remove or false
local stripped_calc_results = {}
for i, row in ipairs(calc_results) do
local rowid = row[args.id_field] or tostring(i)
local keep = true
if args.remove_nil_distances and row[args.distance_field] == nil then
keep = false
if test then mw.log(string.format("Removing row %s with nil distance", rowid)) end
end
if keep and (args.remove_origin or args.remove_zero) and row[args.distance_field] == 0 then
keep = false
if test then mw.log(string.format("Removing row %s with zero distance", rowid)) end
end
if keep and args.field_required_values then
local field_required_values = {}
if type(args.field_required_values) == 'table' then
field_required_values = args.field_required_values
elseif type(args.field.required_values) == 'string' then
field_required_values = m_util.string.splitargs(args.field_required_values)
else
error(string.format("Unexpected field_required_values type '%s'", tostring(type(args.field_required_values))))
end
for key, val in pairs(field_required_values) do
if tostring(row[key]) ~= tostring(val) then
if test and keep then
mw.log(string.format("Removing row %s with %s value %s (not %s)",
rowid, key, tostring(row[key]), tostring(val)))
end
keep = false
break
end
end
end
if keep and args.target_ids then
keep = false
for _, target_id in ipairs(args.target_ids) do
if tostring(rowid) == tostring(target_id) then
keep = true
break
end
end
if test and not keep then mw.log(string.format("Removing row %s not in target ID list", tostring(rowid))) end
end
if keep then
stripped_calc_results[#stripped_calc_results+1] = row
if test then mw.log(string.format("Keeping row %s with distance %s", tostring(rowid), tostring(row[args.distance_field]))) end
end
end
return stripped_calc_results
end
function m_calculate.passive_skill_distance_to_start_nodes_table(frame)
local tpl_args = getArgs(frame, {
parentFirst = true
})
frame = m_util.misc.get_frame(frame)
tpl_args = tpl_args or error("no args supplied")
skill_id = tpl_args.skill_id or error("no skill_id supplied")
-- Grab the connected nodes in the non-Ascendancy passive skills tree
local results = m_cargo.query(
{'passive_skills'},
{'passive_skills.name', 'passive_skills.id', 'passive_skills.connections', 'passive_skills.ascendancy_class'},
{where='id NOT LIKE "%jewel_slot%" AND connections HOLDS LIKE "%" AND (ascendancy_class="" OR ascendancy_class IS NULL)', order_by='passive_skills.id'}
)
-- Parse Module:Game to find the class info we need to do the calculation and format the results
local class_start_node_ids = {}
local class_map_start_node_id_to_name = {}
for _, class_key in ipairs(m_game.constants.characters_order) do
local class_data = m_game.constants.characters[class_key] or error(string.format("No such character class %s", class_key))
local class_start_id = class_data['passive_skill_tree_start_id'] or error(string.format("No start node for character class %s", class_key))
local class_name = class_data['name'] or error(string.format("No name for character class %s", class_key))
table.insert(class_start_node_ids, class_start_id)
class_map_start_node_id_to_name[class_start_id] = class_name
end
-- Find the distances to the start nodes
local calc_args = {
id_field = "passive_skills.id",
connection_ids_field = "passive_skills.connections",
connection_ids_delimiter = ",",
origin_row_id = skill_id,
distance_field = "distance",
test = tpl_args.test_calc or false,
test_remove = tpl_args.test_remove or false,
max_distance = 50,
target_ids = class_start_node_ids
}
local calc_results = m_calculate.calculate_distances(results, calc_args)
local mapped_calc_results = m_cargo.map_results_to_id{results = calc_results, field = 'passive_skills.id'}
local tbl = mw.html.create('table')
tbl
:attr('class', 'wikitable')
:tag('tr')
:tag('th')
:wikitext('Class')
:done()
:tag('th')
:wikitext('Distance')
:done()
for _, start_node_id in ipairs(class_start_node_ids) do
class_name = class_map_start_node_id_to_name[start_node_id] or error(string.format('No class name found for id %s', start_node_id))
class_results = mapped_calc_results[start_node_id] or error(string.format('No results found for class %s', start_node_id))
class_distance = class_results[1]['distance'] or error(string.format('No distance found for class %s', start_node_id))
tbl
:tag('tr')
:tag('td')
:wikitext(class_name)
:done()
:tag('td')
:wikitext(class_distance)
:done()
end
return tostring(tbl)
end
return m_calculate