Module:Grind
Module:Grind was created to solve grind optimisation and estimation problems. It uses a custom object encoding: GRON (grind object notation).
GRON format[edit]
Strings[edit]
- Simple form: "abc" ->
abc
.- Note that the following characters are forbidden in simple form:
,:;()"
.
- Note that the following characters are forbidden in simple form:
- Full form: "abc" ->
"abc"
.,:;()
are allowed in full form.- To use
"
, escape it with \:"\"I myself am my only true friend!\""
- To use
\
, escape it with another \.
GRON objects[edit]
GRON objects consist of:
- An opening bracket
(
; - A
;
-separated list of entries;- Each entry is one of the following:
- A numbered value: a string in any form or another GRON object.
- Internally, these values are numbered. You do not need to provide any number with the value, and there is no way to do it explicitly.
- If a GRON object ends with
;)
, the last numbered value will be an empty string.
- A named value, consisting of a key (a string in any form),
:
and a value (a string in any form or another GRON object).- You may provide key-value pairs in any order and place them between numbered values (only the relative order of numbered values determines how the GRON object is interpreted).
- A special tuple syntax is supported for values. A tuple is a
,
-separated list of (non-tuple) values. It is equivalent to a GRON with numbered values:(a:b,c)
<->(a:(b;c))
.
- A numbered value: a string in any form or another GRON object.
- Each entry is one of the following:
- A closing bracket
)
.
Formulae[edit]
Some of GRON objects will accept formulae instead of numeric constants.
A formula is a GRON object of the form: (formula; <FORMULA_STRING>)
, where the formula string is a string as defined above.
Formula strings have the structure usual for expressions in many programming languages and support:
- unary operators
+-
; - binary operators
+-*/
; - brackets
()
; - numeric constants, both integers and floats;
- variables
$(VAR)
;- Variable names shall not contain symbols
()"
. - Whitespace symbols are not removed from variable names:
$( Watchful )
is valid but incorrect. - Variables with non-provided values are considered to be 0.
- The usual naming notation rules apply to variable names. In particular, the prefix
Input:
makes the variable value to be filled from the corresponding input value.
- Variable names shall not contain symbols
- function calls
f(A,B,C)
;err($(MESSAGE))
is a special function-like pattern which parses directly into an error with messageMESSAGE
.- The following mathematical functions are supported:
min(a,b)
: minimum of two values.max(a,b)
: maximum of two values.exp(a)
: exponent.ln(a)
: natural logarithm.pow(a,b)
: a^b.sqrt(a)
: square root.sign(a)
: signum (negative: -1, zero: 0, positive: 1).abs(a)
: absolute value.round(a)
: rounding to the nearest integer.floor(a)
: rounding to the nearest integer <=a.ceil(a)
: rounding to the nearest integer >=a.sin(a)
: sine.cos(a)
: cosine.tan(a)
: tangent.pi()
: pi (constant).
random.range(A, B)
is the uniform distribution of integers A, A+1, A+2, ..., B-1, B.- All distributions described in a formula are considered to be independent.
- You may pass the distribution to other functions (including
random.range()
itself) to create more complex distributions. - To approximate the continuous uniform distribution, use
random.range(0,C)/C
with a sufficiently large constantC
.
Examples:
random.range(1,$(Watchful))
.floor($(Persuasive)/3)
.
Supported GRON objects[edit]
Conventions
- The first numbered argument of a basic block is its name in lower case.
- If a block
action
accepts a named argumentchallenge
, the former will sometimes be referred to as(action)
and the latter as(action).challenge
.
- If a block
- Wherever a range is required, you may provide one in the following forms:
1
: exact value.1-3
: exact range.-4--3
: exact range with negative numbers.
1-
: from 1 to infinity.-3
: from 0 to 3.-
: from 0 to infinity.
- Wherever a boolean value is required, it must be one of the following strings:
true
/false
yes
/no
on
/off
1
/0
- Wherever a pyramidal quality is specified, you should provide CP amounts and values instead of levels.
- Wherever a number is required, you may provide a formula (without
random.range
calls). Wherever a distribution is accepted, you may userandom.range
as well.- All variables used must be static: progress qualities for other blocks cannot be used.
- Variable prefix and suffix notation:
- (input) names require suffixes specifying the type of the input (which affects both calculator inputs and input interpretation during substitution):
:GRON
: GRON input.:String
: string input.:Number
: number input.:Boolean
: boolean input (will be interpreted as 0 or 1).
- Formula variables corresponding to inputs must have an
Input:
prefix and might have a numeric type suffix (:Number
,:Boolean
). Is no type suffix is encountered, number is assumed.(input; Test:Number)
is the same as(formula; "$(Input:Test:Number)")
.
- You may use numeric type suffixes for (action).challenge quality, (req).target, (pswitch).target, (gate).target and formula variables corresponding to items and qualities. If none is encountered, number is assumed, unless there is special handling for this particular item or quality.
- (input) names require suffixes specifying the type of the input (which affects both calculator inputs and input interpretation during substitution):
(import)[edit]
Signature: (import; <TARGET>[; late:<LATE>; assume:<ASSUME>]).
Imports and substitutes the GRON object from the specified page's Has grind definition
SMW property.
Required arguments:
- The second named argument: string. The title of the target page.
Optional arguments:
late
: boolean. Default: false. If true, the actual import will happen during evaluation. Might be used to reduce SMW properties length.assume
: a GRON object. Each key is player stat or a quality or an item, each value is a number. If set, these will be substituted into the imported object immediately.
(input)[edit]
Signature: (input; <NAME>).
A placeholder indicating that an input is expected.
Required arguments:
- The second named argument: string. ID of the input placeholder.
- You should specify the type of input with the relevant suffix.
- The string shall not contain anything except letters, numbers and the type suffix.
(ref)[edit]
Signature: (ref; <ID>).
A local reference. Expands into the element with the corresponding ID. References are resolved before imports, so you cannot reference imported GRON objects.
Required arguments:
- The second named argument: string. ID of the target element.
- The element must be located somewhere in the GRON object and have the
id
argument set.- The
id
argument is not mentioned elsewhere since it is only used for references; it is supported for all GRON objects.
- The
- The element must be located somewhere in the GRON object and have the
(action)[edit]
Signature: (action[; title:<TITLE>; link:<LINK>; a:<A>; comment:<COMMENT>; challenge:<CHALLENGE>; success:<SUCCESS>; failure:<FAILURE>; alt_success:<ALT_SUCCESS>; alt_success_p:<ALT_SUCCESS_P>; alt_failure:<ALT_FAILURE>; alt_failure_p:<ALT_FAILURE_P>]).
Optional arguments:
title
: a string. The title to be displayed. If a page with this title exists (or thelink
parameter is specified), the title will be formatted as a link during formatting.link
: a page. The link to the action. Default:title
, if such page exists.a
: a string. Action cost. Default: 1.comment
: a string. A comment for this action.challenge
: a GRON object consisting of key-value pairs. The challenge associated with this action.- The key is the tested quality.
- The value is either:
- A string: challenge level. If the quality is
Luck
, it is the percentage of success. For other qualities, it is assumed it is a broad challenge. - A tuple of two strings. The first value is challenge level, the second one is
broad
/narrow
. Note that narrow difficulty is interpreted as the level one needs to get 50% probability of success; broad difficulty is interpreted as the level one needs for 60%. - A tuple of three strings. The third is the narrow difficulty step.
- A string: challenge level. If the quality is
success
: GRON. The provided object is a set of named values, where keys are quality/item names and values correspond to how many is added/removed. For qualities, always use CP. More complex assignments are currently not supported in effects.- You may put an input placeholder instead of a value.
- You may put a value distribution instead of a value.
failure
: GRON. Same as above.alt_success
: GRON. Same as above. Used as an alternative/rare success effect, which, should the challenge be passed or absent, is applied with probabilityalt_success_p
.alt_success_p
: number string, from 0 to 100. Default: 0. The probability of the corresponding alternative/rare success in %.alt_failure
: GRON. Similar.alt_failure_p
: number string, from 0 to 100. Default: 0. Similar.
(nop)[edit]
Signature: (nop[; avoid:<AVOID>]).
Equivalent to (action;a:0;title:"Do nothing")
if used without arguments.
Optional arguments:
avoid
: boolean string. Default: false. If set, (best) blocks will avoid choosing this option whenever possible.
(seq)[edit]
Signature: (seq; <ITEM_1>; <ITEM_2>; <...>).
A sequence of actions or other GRON objects; individual items are not repeated.
Numbered arguments, starting with the second one, are GRON objects.
(airs)[edit]
Signature: (airs; <ITEM_1>; <ITEM_2>; <...>; ranges:<RANGES>[; range:<RANGE>; target:<TARGET>]).
A randomiser.
Required arguments:
ranges
: a tuple of airs ranges. The number of ranges shall be equal to the number of remaining numbered arguments; ranges shall not intersect; ranges shall cover the entirerange
.- Numbered arguments, starting with the second one, are GRON objects.
Optional arguments:
range
: a range. The main range of airs. Default: 1-100.target
: a string. Airs quality name.
(req)[edit]
Signature: (req; <ITEM_1>; <ITEM_2>; <...>; target:<TARGET>; ranges:<RANGES>).
Requirement-dependent behaviour (static requirements only).
Required arguments:
target
: a string. The name of the prerequisite.ranges
: a tuple of ranges. The number of ranges shall be equal to the number of remaining numbered arguments; ranges shall not intersect; ranges should cover the entire range of possibletarget
values.- If a value is not covered by the ranges,
(nop)
is substituted.
- If a value is not covered by the ranges,
- Numbered arguments, starting with the second one, are GRON objects.
One special feature of (req) is that it can be used for non-GRON values as well. See the examples section.
(pswitch)[edit]
Signature: (pswitch; <ITEM_1>; <ITEM_2>; <...>; target:<TARGET>; ranges:<RANGES>; action:<ACTION>).
Progress-dependent resolutions. The action is performed once to gain the progress quality, then, depending on its amount being in the specified ranges, different resolutions are assumed to be applied. For that, the distribution of the progress quality gains is estimated.
Required arguments:
target
: a string. The name of the progress quality/item.ranges
: a tuple of ranges. The number of ranges shall be equal to the number of remaining numbered arguments; ranges shall not intersect; ranges shall cover the entire range of possibletarget
values.action
: a GRON object. The action providing the progress quality. It is not repeated. It is always optimised to maximise the progress quality.- Numbered arguments, starting with the second one, are GRON objects corresponding to the resolutions.
(best)[edit]
Signature: (best; <ITEM_1>; <ITEM_2>; <...>).
Selects the item with the best resource-per-action or the best resource value for the resource in priority.
Numbered arguments, starting with the second one, are GRON objects corresponding to the possible choices.
(gate)[edit]
Signature: (gate; target:<TARGET>; action:<ACTION>[; resolution:<RESOLUTION>; reset:<RESET>; must_succeed:<MUST_SUCCEED>; failure_cost:<FAILURE_COST>; preserve_progress:<PRESERVE_PROGRESS>; optimise:<OPTIMISE>]).
An action with a grindable prerequisite.
Required arguments:
target
: a tuple of two strings. The resource to be collected and the amount required.- The amount might be a distribution.
action
: a GRON object providing the prerequisite quality. It is assumed it can be repeated as much as needed, be it 100 times or 2.15 times. Also see thereset
optional parameter.
Optional arguments:
resolution
: a GRON object. It is assumed that the specified amount of the prerequisite quality is consumed by the action; you should not specify its loss explicitly.reset
: boolean. Default: false. If yes, it will be assumed the resolution (even if not specified) resets the progress quality.must_succeed
: boolean. Default: false. If yes, and if the resolution is an action, it will be repeated as much as needed until success.failure_cost
: string. Default: 0. Ifmust_succeed
, it will be assumed that a failure during the resolution action will cost the player the specified amount of the progress resource.- It might be a distribution.
preserve_progress
: boolean. Default: false. If set, the usual behaviour will be overridden and the progress quality gained will not be removed from the output effect. Generally useful only whenresolution
is undefined.optimise
: boolean. Default: true. If set to false, (gate).action will be optimised against the previous resource in priority rather than for (gate).target.
If you need to grind for more that one prerequisite, use the following pattern:
(seq;(gate;target:<target1>;action:<action1>);(gate;target:<target2>;action:<action2>);<resolution>)
.
(filter)[edit]
Signature: (filter; <ACTION>[; target:<TARGET>]).
An effect output filter.
Required arguments:
- The second numbered argument: GRON. The action to be filtered.
Optional arguments:
target
: either a string or a tuple of strings. Default: empty. The whitelist for the effect outputs.- You cannot avoid considering menace and material costs with (filter): they directly modify the number of actions associated with an (action).
(sell)[edit]
Signature: (sell; action: <ACTION>[; market: <MARKET>; optimise: <OPTIMISE>; <...>]).
A block selling the specified resources in 0 actions.
Required arguments:
action
: a GRON object. The inner action generating the resources to be sold.
Optional arguments:
market
: a string. If specified, market name will be included during formatting. Include "the" if necessary.optimise
: a string. If specified, it should be a name of a resource gained from selling other resources. If specified, the block will make a reasonable attempt to maximise output of the specified resource.- Other named arguments. Keys are resources that can be sold. Values are effects from selling 1 of the corresponding resource in the same format as in (action).success. Note that distributions are currently not supported for sell effects.
Examples[edit]
(best) and (airs)[edit]
(best; <1>; (airs; ranges: 1-50, 51-100; <2>; <3> ) )
- Choose the best: either <1> or the airs-dependent option, which might be <2> or <3>.
(airs; ranges: 1-50, 51-100; (best; <1>; <2> ); (best; <1>; <3> ) )
- Depending on airs, choose the best available option.
(ref)erences[edit]
To avoid repeating <1>
in cases similar to the previous example, you can use references:
(airs; ranges: 1-50, 51-100; (best; (action; id: "literally any string"; title: "A big GRON here" ); <2> ); (best; (ref; literally any string); <3> ) )
(seq; (gate; target: Rostygold, 10; action: (best; id: choice; (action; success: (Rostygold: 1) ); (action; success: (Moon-Pearl: 1) ) ) ); (gate; target: Moon-Pearl, 10; action: (ref; choice) ) )
That (best) object is copied before the preprocessing removes the progress-irrelevant options from it. After preprocessing it will be:
(seq; (gate; target: Rostygold, 10; action: (action; success: (Rostygold: 1) ) ); (gate; target: Moon-Pearl, 10; action: (action; success: (Moon-Pearl: 1) ) ) )
(seq; (ref; nothing); (nop; id: nothing) )
- References may appear before the element with corresponding ID is defined.
(best; id: "we need to go deeper"; (nop); (ref; "we need to go deeper") )
- Oh no! Never do this.
(req) for non-GRON values[edit]
Sometimes you might want to make a non-GRON value stat-dependent. You can do it:
(action; title: "Zail against the Currents"; a: (req; target: "Zailing Speed"; ranges: 45, 55, 75; 5; 4; 3 ) )
(gate).preserve_progress[edit]
Sometimes you might want to specify that:
- a fixed amount of a resource should be collected;
- a GRON subobject should be optimised for a specific resource without it being consumed immediately.
Both cases can be resolved with a (gate) object that would not reset its progress resource in the output effect.
Consider the following fragment from a possible GRON representation of a Night-Whisper grind using Tribute (which is still technically consumed, just in another place).
(gate; target: Night-Whisper, 12; preserve_progress: yes; action: (action; title: "Enjoy a bubbling hookah with the Minister of Culture"; a: 4; success: (Night-Whisper: 1) ) )
The player usually intends to convert all the Tribute there as zailing takes actions. The chosen approximation is the assumption that 240 x Tribute is always spent there, hence 12 x Night-Whisper. It is definitely desirable that those Night-Whispers remain in the output effect.
Now consider the problem: writing a GRON object to optimise gain of some chosen resource.
The initial version could look like this:
(best; (import; "Source 1"); (import; "Source 2"); (import; "Source 3") )
If one of sources has irrelevant outputs associated with irrelevant choices, this grind will have all of those outputs. Most annoyingly, the outputs will be valid options for calculators associated with the GRON object.
(gate) objects specifically optimise (gate).action for their targets, also excluding irrelevant choices. The corrected version would then be:
(gate; target: Resource, 1; preserve_progress: yes; action: (best; (import; "Source 1"); (import; "Source 2"); (import; "Source 3") ) )
(filter)[edit]
To filter out undesired effect outputs completely, use (filter).
(filter; target: Rostygold; (action; success: (Rostygold: 1; Moon-Pearl: 1) ) )
In this example, only the Rostygold output will be considered when scanning GRON metadata and optimising the grind. Moon-Pearls will still be shown in formatted GRON.
Optimisation process[edit]
During optimisation, the following things are tracked: the resource in priority, the distribution of possible effects, the number of actions associated with the effects, optimisation target (which is either absolute resource value or resource per action).
(action)[edit]
Actions are only evaluated. Based on player data, the probability of passing the challenge is computed. Then, a distribution consisting of success and failure effects with their probabilities is returned.
(seq)[edit]
Sequences optimise items independently and return the distribution of the sum of their distributions.
(airs)[edit]
Airs optimise items independently and mix the resulting distributions with the corresponding airs range probabilities.
(pswitch)[edit]
Progress switches optimise the main action against the tracked quality. Then, based on resulting distribution of the quality, probabilities of each range are computed. Each resolution is optimised, and their effect distributions are mixed with the computed probabilities. This distribution is returned.
(best)[edit]
Best blocks optimise each item and choose the one with the highest expected resource-per-action or absolute resource value. The latter depends on whether a (gate) or a (pswitch) is the nearest GRON superobject.
(gate)[edit]
Gates optimise their repeatable action against the tracked resource and compute the expected resource-per-action for it. Based on it, the number of actions spent on gathering the required amount of the tracked resource is estimated. The resolution is optimised, and its distribution of effects is returned.
(sell)[edit]
Sell blocks forward received priorities to the inner action with (sell).optimise undefined.
If it is defined, the block will also try to optimise its action against resources that can be sold. Among all options, (sell) will choose the one that is best for (sell).optimise for the active optimisation target.
Missing features and limitations[edit]
The following features are planned to be implemented:
- Drawing cards.
The following features are not supported by design (which might or might not change):
- Progress qualities in (formula)e and (req)uirements. That would require changes to the optimisation process which would make it as inefficient as simulating distinct states for all possible progress quality values. This could be implemented in a standalone program, but not in a MediaWiki module.
- (pswitch) always optimises its action to maximise the tracked resource. Sometimes a lower range might be more beneficial. But optimising GRON for a specific range of a resource would require iterating all possible choices rather than optimising each against a clear criterion. It would be very inefficient.
- (pswitch) and (gate) cannot optimise for a combination of a number of resources. That would, again, require iterating all possible choices. Therefore there is no good way to build a GRON object for certain Newspaper editions.
- Formulae in range parameters.
- Some of more complex patterns in grind strategies:
- Example: getting tribute, then zailing to the Court of the Wakeful Eye and converting it to other resources (this example can still be approximated).
Using Module:Grind[edit]
The module exports the following functions:
analyse
[edit]
- The main function for grind evaluation.
- Argument 1: string. The placeholder text.
#RPA#
will be replaced with the actual resource-per-action number.#OPTIMAL#
will be replaced with the optimal strategy, nicely formatted.#EFFECT#
will be replaced with the expected effect, nicely formatted.
- Argument 2: string. The resource against which this GRON object will be optimised.
- Argument 3: GRON string.
- Named arguments: player stats.
- Named arguments from the parent frame will be used as well.
- Named arguments starting with
Menace:
orAntiresource:
are interpreted as antiresource specifications; the value is interpreted as a number of actions to remove a single point of that antiresource.- Example:
Menace:Wounds
with value0.5
means that the player can remove 2 CP Wounds per action.
- Example:
- Named arguments starting with
Material:
are interpreted as material specifications; the value is interpreted as a number of actions to acquire a single point of that material. - Named arguments starting with
Input:
and having a type suffix are interpreted as inputs; the value is substituted into the corresponding input placeholder.
- Output: the placeholder text with the specified substitutions.
Usage examples[edit]
Input:
{{#invoke:Grind|analyse|EPA: #RPA# (#EFFECT#) #OPTIMAL#|Echo|(action;title:"Do something";challenge:(Watchful:12);success:(Echo:1.666))|Watchful=12}}
Output:
EPA: 0.9996 (1 action, +99.96 x Penny)
format
[edit]
- Argument 1: GRON string.
- Output: GRON object, nicely formatted.
Usage examples[edit]
Input:
{{#invoke:Grind|format|(action;title:"Do something";success:(Echo:1.4))}}
Output:
- Success: +140 x Penny.
preprocess
[edit]
- Preprocesses the GRON object, removing some pointless branches.
- Argument 1: GRON string.
- Output: GRON string.
Usage examples[edit]
Input:
{{#invoke:Grind|preprocess|(gate;target:Moon-Pearl,100;action:(best;(action; title:"Useful action";success:(Moon-Pearl:10));(action;title:"Useless action";success:(Rostygold:10))))}}
Output:
(gate;target:(Moon-Pearl;100);action:(action;success:(Moon-Pearl:10);title:"Useful action"))
compose
[edit]
- Computes grind composition, substituting provided values into input placeholders.
- Argument 1: GRON string.
- Named arguments: values for input placeholders. The key in a key-value pair must be equal to the ID of the corresponding placeholder
- Named arguments from the parent frame will be used as well.
- The type suffix of the key determines the manner in which the value is interpreted.
- Output: GRON string.
Usage examples[edit]
Input:{{#invoke:Grind|compose|(input;test)|test=(nop)}}
Output:
(nop)
metainfo
[edit]
- Scans the grind, finds (input) placeholders, determines the outputs, adds SMW qualities and lists both the inputs and the outputs. Used in the {{Grind}}.
- Argument 1: GRON string.
- Output: as stated above.
autocalc
[edit]
- Produces a calculator form for the specified grind page.
- Argument 1: page. This page must have the
Has grind definition
SMW quality. - Argument 2: string. HTML id part associated with the calculator. Shall be unique in the page.
- Title: string. Calculator title. Empty by default.
- Template: string. The Template parameter passed to {{GrindAnalyse}}. Shall not contain
|
and newlines. - GRON: GRON string. The GRON object associated with the specified page. Used to build calculator inputs. It is a workaround to have calculators in sync with GRON data in grind pages. Shall not be used outside of {{Grind}}.
- Named arguments starting with
Extra
: strings. Each value is added to calculator inputs as a quality or an item or an additional GRON input.- Add a
:Boolean
suffix to have a toggleswitch with 0/1 values instead of a float input. - Add an
Input:
prefix and a type suffix for inputs.
- Add a
- Named arguments starting with
Material
: strings. Each value is added to calculator inputs as a material (a quality or an item) with some action cost to acquire. - Output: a calculator form.
Usage examples[edit]
Input:{{#invoke:Grind|autocalc|Grind:Sunken Embassy Progress|testid}}
Output:
template = GrindAnalyse name = form = form-testid result = result-testid param = 1||Grind:Sunken Embassy Progress|hidden param = Resource|Optimise for:||select|Fragments of Infernal Affairs;Nothing param = RPA||per action|hidden param = maingroup|Main stats||group|Watchful,Shadowy,Dangerous,Persuasive param = Dangerous|Dangerous|100|int|0-500 param = Watchful|Watchful|100|int|0-500 param = advgroup|Advanced skills||group|Kataleptic Toxicology,Monstrous Anatomy,A Player of Chess,Glasswork,Shapeling Arts,Artisan of the Red Science,Mithridacy,Steward of the Discordance,Zeefaring param = Mithridacy|Mithridacy|0|int|0-20 param = menacegroup|Menace costs (in actions per point)||group|Menace:Wounds,Menace:Scandal,Menace:Suspicion,Menace:Nightmares,Menace:A Turncoat,Menace:Irrigo,Menace:Plagued by a Popular Song,Menace:Unaccountably Peckish,Menace:Ravages of Parabolan Warfare param = Menace:Nightmares|Nightmares|0.33|float|0- param = materialgroup|Material costs (in actions per point)||group|Material:Palimpsest Scrap param = Material:Palimpsest Scrap|Palimpsest Scrap|0.2|float|0-
gron_string
[edit]
- Processes the input string and formats it as a GRON string.
- Kills all strip markers: their initial values would be unrecoverable in another context anyway.
- It means some wikitext may be impossible to store this way.
- Argument 1: text.
- Output: GRON string.
Usage examples[edit]
Input:{{#invoke:Grind|gron_string|{{IL|Watchful|+1 CP}}, "test"}}
Output:
" +1 CP Watchful, \"test\""
local p = {}
-- common game data
local common = require('Module:Grind/common')
-- common utilities
local util = require('Module:Grind/util')
-- HTML utilities
local html = require('Module:Grind/html')
-- GRON parsing and manipulation
local GRON = require('Module:Grind/GRON')
-- distribution manipulation
local dist = require('Module:Grind/dist')
-- formulae parsing and manipulation
local fml = require('Module:Grind/fml')
-- GRON processing utilities
local proc = require('Module:Grind/proc')
-- optimisation
local opt = require('Module:Grind/opt')
-- autocalculators
local ac = require('Module:Grind/autocalc')
-- formatting utilities
local fmt = require('Module:Grind/fmt')
-- == Functions used in exported ones ==
-- Collects arguments from the frame, from its parent frame, from its parent frame...
-- The depth is limited by `max_depth`.
-- `force_numbers`: whether to convert values to numbers.
local function collect_args(frame, max_depth, depth, force_numbers)
depth = depth or 0
if force_numbers == nil then
force_numbers = true
end
if max_depth ~= nil and depth >= max_depth then
return {}
end
if frame == nil then
return {}
end
local args = {}
if frame.getParent ~= nil then
args = collect_args(frame:getParent(), max_depth, depth + 1, force_numbers)
end
for k, v in pairs(frame.args) do
if type(k) == 'string' then
-- expand HTML characters escaped in mw.html.escape()
k = k:gsub(''', "'")
k = k:gsub('"', '"')
k = k:gsub('<', '<')
k = k:gsub('>', '>')
k = k:gsub('&', '&')
if force_numbers then
args[k] = tonumber(v)
else
args[k] = v
end
end
end
return args
end
-- Extracts arguments with provided prefixes, removing the prefixes.
local function extract_by_prefix(args, prefix_list, preserve_original)
preserve_original = preserve_original or false
if type(prefix_list) == 'string' then
prefix_list = {prefix_list}
end
local extracted = {}
for key, val in pairs(args) do
if type(key) == 'string' then
if not key:sub(1, 1):match('%w') then
args[key] = nil
end
for _, prefix in ipairs(prefix_list) do
if key:sub(1, #prefix) == prefix then
extracted[key:sub(#prefix + 1)] = val
if not preserve_original then
args[key] = nil
end
break
end
end
end
end
return extracted
end
local function preprocess_inputs(frame, args, type_data)
local inputs = {}
local cache = {}
for k, v in pairs(args) do
local ktype
k, ktype = util.normalise_input(k)
if ktype == 'unknown' and type_data[k] ~= nil then
ktype = type_data[k]
end
if ktype == 'gron' then
local gron_v = GRON.parse(v)
if gron_v == nil then
gron_v = {'error', 'Error parsing GRON: ' .. v}
end
gron_v = proc.resolve_refs(gron_v)
gron_v = proc.preprocess_gron(frame, gron_v, nil, cache)
if gron_v == nil then
gron_v = {'error', 'Error preprocessing GRON: ' .. v}
end
inputs[k] = gron_v
elseif ktype == 'boolean' or ktype == 'number' then
inputs[k] = v
elseif ktype == 'string' then
inputs[k] = v
else
-- guesswork
if tonumber(v) ~= nil then
inputs[k] = v
else
local gron_v = GRON.parse(v)
if gron_v == nil then
-- Probably just a string? Or an incorrect GRON. Let's assume it's a string.
inputs[k] = v
else
gron_v = proc.resolve_refs(gron_v)
gron_v = proc.preprocess_gron(frame, gron_v, nil, cache)
if gron_v == nil then
gron_v = {'error', 'Could not preprocess GRON parameter ' .. k .. '!'}
end
inputs[k] = gron_v
end
end
end
end
return inputs
end
-- == Exported functions ==
-- Analyses the grind.
-- See module documentation.
function p.analyse(frame)
local text = frame.args[1]
local resource = frame.args[2]
local grind = frame.args[3]
local gron = GRON.parse(grind)
if gron == nil then
return html.span('Error parsing GRON: ' .. (grind or ''), html.red, true)
end
gron = proc.preprocess_gron(frame, gron, nil, {}, true)
if gron == nil then
return html.span('Error preprocessing GRON', html.red, true)
end
local data = collect_args(frame, 2, 0, false)
local menaces = extract_by_prefix(data, {'Menace:', 'Antiresource:'})
local materials = extract_by_prefix(data, {'Cost:', 'ItemCost:', 'QualityCost:', 'Expense:', 'Material:', 'Fuel:'})
for k, v in pairs(materials) do
if tonumber(v) ~= nil and menaces[k] == nil then
-- menaces cost actions to reduce; materials cost actions to acquire
menaces[k] = -tonumber(v)
end
end
local inputs = extract_by_prefix(data, 'Input:', true)
for k, v in pairs(data) do
local k, _ = k, nil
if k:sub(1, 6) == 'Input:' then
k, _ = util.normalise_input(k)
end
data[k] = tonumber(v)
end
local type_data = GRON.inputs(gron)
inputs = preprocess_inputs(frame, inputs, type_data)
if next(inputs) ~= nil then
-- a composition is needed
gron = proc.compose(gron, inputs)
gron = proc.preprocess_gron(frame, gron, nil, {}, true)
if gron == nil then
return html.span('Error preprocessing GRON', html.red, true)
end
end
gron = proc.resolve_formulae(gron, data)
gron = proc.resolve_reqs(gron, data)
local m = 1
if resource == 'Echo' then
resource = 'Penny'
m = 0.01
end
local optimisation_target = 'rpa'
local effect, optimal_gron = opt.optimise(gron, resource, data, menaces, optimisation_target)
local expected = util.complete_effect(effect.a, dist.expected_effect(effect.effect))
local rpa = dist.resource_per_action(expected, resource) * m
local cache = {}
local formatted_gron = fmt.format_gron(frame, cache, optimal_gron)
formatted_gron = formatted_gron:gsub('%%', '%%%%')
formatted_gron = html.widebox(formatted_gron)
local formatted_effect = fmt.format_expected_effect(frame, expected, cache)
text = text:gsub('#RPA#', util.f2s(rpa))
text = text:gsub('#OPTIMAL#', formatted_gron)
text = text:gsub('#EFFECT#', formatted_effect)
return text
end
-- Formats the GRON.
-- See module documentation.
function p.format(frame)
local grind = frame.args[1]
local gron = GRON.parse(grind)
if gron == nil then
return html.span('Error parsing GRON', html.red, true)
end
gron = proc.preprocess_gron(frame, gron, nil, {}, true)
if gron == nil then
return html.span('Error preprocessing GRON', html.red, true)
end
local cache = {}
local formatted_gron = fmt.format_gron(frame, cache, gron)
formatted_gron = html.widebox(formatted_gron)
return formatted_gron
end
-- Prepares the GRON for further processing.
-- See module documentation.
function p.preprocess(frame)
local grind = frame.args[1]
local gron = GRON.parse(grind)
if gron == nil then
return '(error; "Could not parse GRON")'
end
gron = proc.resolve_refs(gron)
local cache = {}
local prepared = proc.preprocess_gron(frame, gron, nil, cache)
if prepared == nil then
return '(error; "Could not preprocess GRON")'
end
return GRON.encode(prepared)
end
-- Composes GRONs.
-- See module documentation.
function p.compose(frame)
local grind = frame.args[1]
local gron = GRON.parse(grind)
if gron == nil then
return '(error; "Could not parse GRON")'
end
local cache = {}
gron = proc.resolve_refs(gron)
gron = proc.preprocess_gron(frame, gron, nil, cache, true)
if gron == nil then
return '(error; "Could not preprocess GRON")'
end
local collected = collect_args(frame, 2, 0, false)
local type_data = GRON.inputs(gron)
local args = preprocess_inputs(frame, collected, type_data)
gron = proc.compose(gron, args)
gron = proc.preprocess_gron(frame, gron, nil, cache)
if gron == nil then
return '(error; "Could not preprocess GRON")'
end
return GRON.encode(gron)
end
-- Returns GRON metainformation.
-- Sets SMW qualities - use only on grind pages!
function p.metainfo(frame)
local grind = frame.args[1]
local gron = GRON.parse(grind)
if gron == nil then
return html.span('Error parsing GRON: ' .. (grind or ''), html.red, true)
end
local cache = {}
gron = proc.preprocess_gron(frame, gron, nil, cache, true)
if gron == nil then
return html.span('Error preprocessing GRON', html.red, true)
end
local args = collect_args(frame, 1)
local s = ''
-- inputs
local inputs = GRON.inputs(gron)
local has_inputs = next(inputs) ~= nil
if has_inputs then
s = s .. 'This grind might need the following inputs:<ul>'
for k, t in pairs(inputs) do
s = s .. '<li>' .. html.span(k, html.orange, true)
if t ~= 'unknown' then
s = s .. ' of type ' .. html.span(tostring(t), html.cyan, true)
end
s = s .. frame:callParserFunction{
name = '#set',
args = {'Has grind input=' .. k}
}
s = s .. '</li>'
end
s = s .. '</ul>'
end
s = s .. frame:callParserFunction{
name = '#set',
args = {'Has grind inputs=' .. tostring(has_inputs)}
}
-- materials
local args = collect_args(frame, 2, 0, false)
local materials_table = extract_by_prefix(args, {'Cost', 'ItemCost', 'QualityCost', 'Expense', 'Material', 'Fuel'})
local materials = {}
for _, material in pairs(materials_table) do
materials[material] = true
end
if next(materials) ~= nil then
local cache = {}
s = s .. 'This grind might consume the following materials:<ul>'
for material, _ in pairs(materials) do
s = s .. '<li>'
if material == 'Echo' then
s = s .. fmt.get_il(frame, cache, 'Penny')
else
s = s .. fmt.get_il(frame, cache, material)
end
s = s .. frame:callParserFunction{
name = '#set',
args = {'Has grind material=' .. material}
}
s = s .. '</li>'
end
s = s .. '</ul>'
end
-- outputs
local outputs = GRON.outputs(gron)
for k, v in pairs(args) do
if type(k) == 'string' and k:sub(1, 6) == 'Output' then
outputs[v] = true
end
end
for material, _ in pairs(materials) do
outputs[material] = nil
end
local menaces = {}
for v, _ in pairs(outputs) do
if common.identify(v) == 'menace' then
outputs[v] = nil
menaces[v] = true
end
end
local has_outputs = next(outputs) ~= nil
if has_outputs then
local cache = {}
s = s .. 'This grind is a source of:<ul>'
for resource, _ in pairs(outputs) do
s = s .. '<li>'
if resource == 'Echo' then
s = s .. fmt.get_il(frame, cache, 'Penny')
else
s = s .. fmt.get_il(frame, cache, resource)
end
s = s .. frame:callParserFunction{
name = '#set',
args = {'Has grind objective=' .. resource}
}
s = s .. '</li>'
end
s = s .. '</ul>'
end
-- menaces
if next(menaces) ~= nil then
local cache = {}
s = s .. 'This grind might cause:<ul>'
for menace, _ in pairs(menaces) do
s = s .. '<li>'
s = s .. fmt.get_il(frame, cache, menace)
s = s .. frame:callParserFunction{
name = '#set',
args = {'Has grind antiresource=' .. menace}
}
s = s .. '</li>'
end
s = s .. '</ul>'
end
return s
end
-- Builds a calculator form for the specified grind.
function p.autocalc(frame)
local grind_link = frame.args[1]
if grind_link == nil then
return html.span('No grind specified in autocalc()!', html.red, true)
end
local id = frame.args[2]
if id == nil then
return html.span('No calculator id specified in autocalc()!', html.red, true)
end
if id:match('[%w_%-]*') ~= id then
return html.span('Invalid calculator id: ' .. id, html.red, true)
end
local title = frame.args['Title'] or ''
local template = frame.args['Template']
if template ~= nil and template:match('.*%|') then
return html.span('Templates cannot contain the character <code>|</code>.', html.red, true)
end
local grind
local smw_materials = {}
if frame.args['GRON'] ~= nil then
-- Why? To resolve the double purge problem for grind pages.
-- (a SMW quality is both set and queried on the same page)
grind = frame.args['GRON']
else
grind = frame:callParserFunction{
name = '#show',
args = {grind_link, '?Has grind definition'}
}
local smw_materials_raw = frame:callParserFunction{
name = '#show',
args = {grind_link, '?Has grind material', format='plainlist', valuesep='|'}
}
if type(smw_materials_raw) == 'string' and #smw_materials_raw > 0 then
for material in smw_materials_raw:gmatch('([^|]+)') do
smw_materials[material] = true
end
end
end
local args = collect_args(frame, 2, 0, false)
local extras = extract_by_prefix(args, 'Extra')
local materials = extract_by_prefix(args, {'Cost', 'ItemCost', 'QualityCost', 'Expense', 'Material', 'Fuel'})
for material, _ in pairs(smw_materials) do
table.insert(materials, material)
end
if grind == nil then
return html.span('Grind not found: ' .. grind_link, html.red, true)
end
local gron = GRON.parse(grind)
if gron == nil then
return html.span('Error parsing GRON: ' .. grind, html.red, true)
end
gron = proc.preprocess_gron(frame, gron, nil, {}, true)
if gron == nil then
return html.span('Error preprocessing GRON', html.red, true)
end
local outputs = GRON.outputs(gron)
for _, material in pairs(materials) do
outputs[material] = nil
end
local resources = {}
for resource, _ in pairs(outputs) do
if common.identify(resource) ~= 'menace' then
if resource == 'Penny' then
table.insert(resources, 'Echo')
else
table.insert(resources, resource)
end
end
end
local entries = ac.generate(gron, extras, materials)
local calc = ac.buildcalc(grind_link, title, id, template, resources, entries)
return calc
end
-- Encodes a string as a GRON string.
function p.gron_string(frame)
local text = frame.args[1]
-- Removing strip markers.
-- There is no way their initial values could be recovered later.
text = mw.text.killMarkers(text)
text = GRON.encode_string(text)
return text
end
return p