Module:Grind

From Fallen London Wiki

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: ,:;()".
  • 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 \.
Decorative text divider 3.svg

GRON objects[edit]

GRON objects consist of:

  1. An opening bracket (;
  2. 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)).
  3. A closing bracket ).
Decorative text divider 3.svg

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.
  • function calls f(A,B,C);
    • err($(MESSAGE)) is a special function-like pattern which parses directly into an error with message MESSAGE.
    • 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 constant C.

Examples:

  • random.range(1,$(Watchful)).
  • floor($(Persuasive)/3).
Abstract floral dash, Ingall’s.svg

Supported GRON objects[edit]

Conventions

  1. The first numbered argument of a basic block is its name in lower case.
    • If a block action accepts a named argument challenge, the former will sometimes be referred to as (action) and the latter as (action).challenge.
  2. 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.
  3. Wherever a boolean value is required, it must be one of the following strings:
    • true/false
    • yes/no
    • on/off
    • 1/0
  4. Wherever a pyramidal quality is specified, you should provide CP amounts and values instead of levels.
  5. Wherever a number is required, you may provide a formula (without random.range calls). Wherever a distribution is accepted, you may use random.range as well.
    • All variables used must be static: progress qualities for other blocks cannot be used.
  6. 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.
Abstract floral dash, Ingall’s.svg

(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.
Decorative text divider 3.svg

(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.
Decorative text divider 3.svg

(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.
Decorative text divider 3.svg

(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 the link 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.
  • 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 probability alt_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.
Decorative text divider 3.svg

(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.
Decorative text divider 3.svg

(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.

Decorative text divider 3.svg

(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 entire range.
  • 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.
Decorative text divider 3.svg

(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 possible target values.
    • If a value is not covered by the ranges, (nop) is substituted.
  • 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.

Decorative text divider 3.svg

(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 possible target 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.
Decorative text divider 3.svg

(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.

Decorative text divider 3.svg

(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 the reset 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. If must_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 when resolution 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>).
Decorative text divider 3.svg

(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).
Decorative text divider 3.svg

(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.
Abstract floral dash, Ingall’s.svg

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>.
Decorative text divider 3.svg
(airs;
 ranges: 1-50, 51-100;
 (best;
  <1>;
  <2>
 );
 (best;
  <1>;
  <3>
 )
)
  • Depending on airs, choose the best available option.
Abstract floral dash, Ingall’s.svg

(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>
 )
)
Decorative text divider 3.svg
(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)
  )
 )
)
Decorative text divider 3.svg
(seq;
 (ref; nothing);
 (nop; id: nothing)
)
  • References may appear before the element with corresponding ID is defined.
Decorative text divider 3.svg
(best;
 id: "we need to go deeper";
 (nop);
 (ref; "we need to go deeper")
)
  • Oh no! Never do this.
Abstract floral dash, Ingall’s.svg

(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
 )
)
Abstract floral dash, Ingall’s.svg

(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.

Decorative text divider 3.svg

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")
 )
)
Abstract floral dash, Ingall’s.svg

(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.

Abstract floral dash, Ingall’s.svg

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.

Abstract floral dash, Ingall’s.svg

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).
Abstract floral dash, Ingall’s.svg

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: or Antiresource: 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 value 0.5 means that the player can remove 2 CP Wounds per action.
    • 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)

Do something ( Watchful 12 challenge: 60.00%).
  • Success: +166.60 x Penny.
  • Expected action effect: +99.96 x Penny.
Abstract floral dash, Ingall’s.svg

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:

Do something.
Abstract floral dash, Ingall’s.svg

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"))

Abstract floral dash, Ingall’s.svg

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)

Abstract floral dash, Ingall’s.svg

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.
Abstract floral dash, Ingall’s.svg

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.
  • 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-
Loading...
Abstract floral dash, Ingall’s.svg

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\""

Abstract floral dash, Ingall’s.svg

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('&#039;', "'")
			k = k:gsub('&quot;', '"')
			k = k:gsub('&lt;', '<')
			k = k:gsub('&gt;', '>')
			k = k:gsub('&amp;', '&')
			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