Initial
This commit is contained in:
9
resources/app/client-esm/dice/_module.mjs
Normal file
9
resources/app/client-esm/dice/_module.mjs
Normal file
@@ -0,0 +1,9 @@
|
||||
/** @module dice */
|
||||
|
||||
export * as types from "./_types.mjs";
|
||||
export * as terms from "./terms/_module.mjs";
|
||||
|
||||
export {default as Roll} from "./roll.mjs";
|
||||
export {default as RollGrammar} from "./grammar.pegjs";
|
||||
export {default as RollParser} from "./parser.mjs";
|
||||
export {default as MersenneTwister} from "./twister.mjs";
|
||||
75
resources/app/client-esm/dice/_types.mjs
Normal file
75
resources/app/client-esm/dice/_types.mjs
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* @typedef {Object} DiceTermResult
|
||||
* @property {number} result The numeric result
|
||||
* @property {boolean} [active] Is this result active, contributing to the total?
|
||||
* @property {number} [count] A value that the result counts as, otherwise the result is not used directly as
|
||||
* @property {boolean} [success] Does this result denote a success?
|
||||
* @property {boolean} [failure] Does this result denote a failure?
|
||||
* @property {boolean} [discarded] Was this result discarded?
|
||||
* @property {boolean} [rerolled] Was this result rerolled?
|
||||
* @property {boolean} [exploded] Was this result exploded?
|
||||
*/
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Roll Parsing Types */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @typedef {object} RollParseNode
|
||||
* @property {string} class The class name for this node.
|
||||
* @property {string} formula The original matched text for this node.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {RollParseNode} RollParseTreeNode
|
||||
* @property {string} operator The binary operator.
|
||||
* @property {[RollParseNode, RollParseNode]} operands The two operands.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {RollParseNode} FlavorRollParseNode
|
||||
* @property {object} options
|
||||
* @property {string} options.flavor Flavor text associated with the node.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {FlavorRollParseNode} ModifiersRollParseNode
|
||||
* @property {string} modifiers The matched modifiers string.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {FlavorRollParseNode} NumericRollParseNode
|
||||
* @property {number} number The number.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {FlavorRollParseNode} FunctionRollParseNode
|
||||
* @property {string} fn The function name.
|
||||
* @property {RollParseNode[]} terms The arguments to the function.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {ModifiersRollParseNode} PoolRollParseNode
|
||||
* @property {RollParseNode[]} terms The pool terms.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {FlavorRollParseNode} ParentheticalRollParseNode
|
||||
* @property {string} term The inner parenthetical term.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {FlavorRollParseNode} StringParseNode
|
||||
* @property {string} term The unclassified string term.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {ModifiersRollParseNode} DiceRollParseNode
|
||||
* @property {number|ParentheticalRollParseNode} number The number of dice.
|
||||
* @property {string|number|ParentheticalRollParseNode} faces The number of faces or a string denomination like "c" or
|
||||
* "f".
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {null|number|string|RollParseNode|RollParseArg[]} RollParseArg
|
||||
*/
|
||||
91
resources/app/client-esm/dice/grammar.pegjs
Normal file
91
resources/app/client-esm/dice/grammar.pegjs
Normal file
@@ -0,0 +1,91 @@
|
||||
{
|
||||
/*
|
||||
|
||||
This is a per-parser initialization block. It runs whenever the parser is invoked. Any variables declared here are
|
||||
available in any javascript blocks in the rest of the grammar.
|
||||
|
||||
In addition to the parser generated by peggy, we allow for certain parts of the process to be delegated to client
|
||||
code. A class implementing this 'parser' API may be passed-in here as an option when the peggy parser is invoked,
|
||||
otherwise we use the one configured at CONFIG.Dice.parser.
|
||||
|
||||
*/
|
||||
|
||||
const Parser = options.parser ?? CONFIG.Dice.parser;
|
||||
const parser = new Parser(input);
|
||||
}
|
||||
|
||||
Expression = _ leading:(_ @Additive)* _ head:Term tail:(_ @Operators _ @Term)* _ {
|
||||
|
||||
/*
|
||||
|
||||
The grammar rules are matched in order of precedence starting at the top of the file, so the rules that match the
|
||||
largest portions of a string should generally go at the top, with matches for smaller substrings going at the bottom.
|
||||
|
||||
Here we have a rule that matches the overall roll formula. If a given formula does not match this rule, it means that
|
||||
it is an invalid formula and will throw a parsing error.
|
||||
|
||||
Prefixing a pattern with 'foo:' is a way to give a name to the sub-match in the associated javascript code. We use
|
||||
this fairly heavily since we want to forward these sub-matches onto the 'parser'. We can think of them like named
|
||||
capture groups.
|
||||
|
||||
Prefixing a pattern with '@' is called 'plucking', and is used to identify sub-matches that should be assigned to the
|
||||
overall capture name (like 'foo:'), ignoring any that are not 'plucked'.
|
||||
|
||||
For example 'tail:(_ @Operators _ @Term)*' matches operators followed by terms, with any amount of whitespace
|
||||
in-between, however only the operator and term matches are assigned to the 'tail' variable, the whitespace is ignored.
|
||||
|
||||
The 'head:A tail:(Delimiter B)*' pattern is a way of representing a string of things separated by a delimiter,
|
||||
like 'A + B + C' for formulas, or 'A, B, C' for Pool and Math terms.
|
||||
|
||||
In each of these cases we follow the same pattern: We match a term, then we call 'parser.on*', and that method is
|
||||
responsible for taking the raw matched sub-strings and returning a 'parse node', i.e. a plain javascript object that
|
||||
contains all the information we need to instantiate a real `RollTerm`.
|
||||
|
||||
*/
|
||||
|
||||
return parser._onExpression(head, tail, leading, text(), error);
|
||||
}
|
||||
|
||||
Term = FunctionTerm / DiceTerm / NumericTerm / PoolTerm / Parenthetical / StringTerm
|
||||
|
||||
FunctionTerm = fn:FunctionName "(" _ head:Expression? _ tail:(_ "," _ @Expression)* _ ")" flavor:Flavor? {
|
||||
return parser._onFunctionTerm(fn, head, tail, flavor, text());
|
||||
}
|
||||
|
||||
DiceTerm = number:(Parenthetical / Constant)? [dD] faces:([a-z]i / Parenthetical / Constant) modifiers:Modifiers? flavor:Flavor? {
|
||||
return parser._onDiceTerm(number, faces, modifiers, flavor, text());
|
||||
}
|
||||
|
||||
NumericTerm = number:Constant flavor:Flavor? !StringTerm { return parser._onNumericTerm(number, flavor); }
|
||||
|
||||
PoolTerm = "{" _ head:Expression tail:("," _ @Expression)* "}" modifiers:Modifiers? flavor:Flavor? {
|
||||
return parser._onPoolTerm(head, tail, modifiers, flavor, text());
|
||||
}
|
||||
|
||||
Parenthetical = "(" _ term:Expression _ ")" flavor:Flavor? { return parser._onParenthetical(term, flavor, text()); }
|
||||
|
||||
StringTerm = term:(ReplacedData / PlainString) flavor:Flavor? { return parser._onStringTerm(term, flavor); }
|
||||
|
||||
ReplacedData = $("$" $[^$]+ "$")
|
||||
|
||||
PlainString = $[^ (){}[\]$,+\-*%/]+
|
||||
|
||||
FunctionName = $([a-z$_]i [a-z$_0-9]i*)
|
||||
|
||||
Modifiers = $([^ (){}[\]$+\-*/,]+)
|
||||
|
||||
Constant = _ [0-9]+ ("." [0-9]+)? { return Number(text()); }
|
||||
|
||||
Operators = MultiplicativeFirst / AdditiveOnly
|
||||
|
||||
MultiplicativeFirst = head:Multiplicative tail:(_ @Additive)* { return [head, ...tail]; }
|
||||
|
||||
AdditiveOnly = head:Additive tail:(_ @Additive)* { return [null, head, ...tail]; }
|
||||
|
||||
Multiplicative = "*" / "/" / "%"
|
||||
|
||||
Additive = "+" / "-"
|
||||
|
||||
Flavor = "[" @$[^[\]]+ "]"
|
||||
|
||||
_ "whitespace" = [ ]*
|
||||
370
resources/app/client-esm/dice/parser.mjs
Normal file
370
resources/app/client-esm/dice/parser.mjs
Normal file
@@ -0,0 +1,370 @@
|
||||
import { getType } from "../../common/utils/helpers.mjs";
|
||||
import OperatorTerm from "./terms/operator.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("../_types.mjs").RollParseNode} RollParseNode
|
||||
* @typedef {import("../_types.mjs").RollParseTreeNode} RollParseTreeNode
|
||||
* @typedef {import("../_types.mjs").NumericRollParseNode} NumericRollParseNode
|
||||
* @typedef {import("../_types.mjs").FunctionRollParseNode} FunctionRollParseNode
|
||||
* @typedef {import("../_types.mjs").PoolRollParseNode} PoolRollParseNode
|
||||
* @typedef {import("../_types.mjs").ParentheticalRollParseNode} ParentheticalRollParseNode
|
||||
* @typedef {import("../_types.mjs").DiceRollParseNode} DiceRollParseNode
|
||||
* @typedef {import("../_types.mjs").RollParseArg} RollParseArg
|
||||
*/
|
||||
|
||||
/**
|
||||
* A class for transforming events from the Peggy grammar lexer into various formats.
|
||||
*/
|
||||
export default class RollParser {
|
||||
/**
|
||||
* @param {string} formula The full formula.
|
||||
*/
|
||||
constructor(formula) {
|
||||
this.formula = formula;
|
||||
}
|
||||
|
||||
/**
|
||||
* The full formula.
|
||||
* @type {string}
|
||||
*/
|
||||
formula;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Parse Events */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle a base roll expression.
|
||||
* @param {RollParseNode} head The first operand.
|
||||
* @param {[string[], RollParseNode][]} tail Zero or more subsequent (operators, operand) tuples.
|
||||
* @param {string} [leading] A leading operator.
|
||||
* @param {string} formula The original matched text.
|
||||
* @param {function} error The peggy error callback to invoke on a parse error.
|
||||
* @returns {RollParseTreeNode}
|
||||
* @internal
|
||||
* @protected
|
||||
*/
|
||||
_onExpression(head, tail, leading, formula, error) {
|
||||
if ( CONFIG.debug.rollParsing ) console.debug(this.constructor.formatDebug("onExpression", head, tail));
|
||||
if ( leading.length ) leading = this._collapseOperators(leading);
|
||||
if ( leading === "-" ) head = this._wrapNegativeTerm(head);
|
||||
|
||||
// We take the list of (operator, operand) tuples and arrange them into a left-skewed binary tree.
|
||||
return tail.reduce((acc, [operators, operand]) => {
|
||||
let operator;
|
||||
let [multiplicative, ...additive] = operators;
|
||||
if ( additive.length ) additive = this._collapseOperators(additive);
|
||||
if ( multiplicative ) {
|
||||
operator = multiplicative;
|
||||
if ( additive === "-" ) operand = this._wrapNegativeTerm(operand);
|
||||
}
|
||||
else operator = additive;
|
||||
if ( typeof operator !== "string" ) error(`Failed to parse ${formula}. Unexpected operator.`);
|
||||
const operands = [acc, operand];
|
||||
return { class: "Node", formula: `${acc.formula} ${operator} ${operand.formula}`, operands, operator };
|
||||
}, head);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle a dice term.
|
||||
* @param {NumericRollParseNode|ParentheticalRollParseNode|null} number The number of dice.
|
||||
* @param {string|NumericRollParseNode|ParentheticalRollParseNode|null} faces The number of die faces or a string
|
||||
* denomination like "c" or "f".
|
||||
* @param {string|null} modifiers The matched modifiers string.
|
||||
* @param {string|null} flavor Associated flavor text.
|
||||
* @param {string} formula The original matched text.
|
||||
* @returns {DiceRollParseNode}
|
||||
* @internal
|
||||
* @protected
|
||||
*/
|
||||
_onDiceTerm(number, faces, modifiers, flavor, formula) {
|
||||
if ( CONFIG.debug.rollParsing ) {
|
||||
console.debug(this.constructor.formatDebug("onDiceTerm", number, faces, modifiers, flavor, formula));
|
||||
}
|
||||
return { class: "DiceTerm", formula, modifiers, number, faces, evaluated: false, options: { flavor } };
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle a numeric term.
|
||||
* @param {number} number The number.
|
||||
* @param {string} flavor Associated flavor text.
|
||||
* @returns {NumericRollParseNode}
|
||||
* @internal
|
||||
* @protected
|
||||
*/
|
||||
_onNumericTerm(number, flavor) {
|
||||
if ( CONFIG.debug.rollParsing ) console.debug(this.constructor.formatDebug("onNumericTerm", number, flavor));
|
||||
return {
|
||||
class: "NumericTerm", number,
|
||||
formula: `${number}${flavor ? `[${flavor}]` : ""}`,
|
||||
evaluated: false,
|
||||
options: { flavor }
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle a math term.
|
||||
* @param {string} fn The Math function.
|
||||
* @param {RollParseNode} head The first term.
|
||||
* @param {RollParseNode[]} tail Zero or more additional terms.
|
||||
* @param {string} flavor Associated flavor text.
|
||||
* @param {string} formula The original matched text.
|
||||
* @returns {FunctionRollParseNode}
|
||||
* @internal
|
||||
* @protected
|
||||
*/
|
||||
_onFunctionTerm(fn, head, tail, flavor, formula) {
|
||||
if ( CONFIG.debug.rollParsing ) {
|
||||
console.debug(this.constructor.formatDebug("onFunctionTerm", fn, head, tail, flavor, formula));
|
||||
}
|
||||
const terms = [];
|
||||
if ( head ) terms.push(head, ...tail);
|
||||
return { class: "FunctionTerm", fn, terms, formula, evaluated: false, options: { flavor } };
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle a pool term.
|
||||
* @param {RollParseNode} head The first term.
|
||||
* @param {RollParseNode[]} tail Zero or more additional terms.
|
||||
* @param {string|null} modifiers The matched modifiers string.
|
||||
* @param {string|null} flavor Associated flavor text.
|
||||
* @param {string} formula The original matched text.
|
||||
* @returns {PoolRollParseNode}
|
||||
* @internal
|
||||
* @protected
|
||||
*/
|
||||
_onPoolTerm(head, tail, modifiers, flavor, formula) {
|
||||
if ( CONFIG.debug.rollParsing ) {
|
||||
console.debug(this.constructor.formatDebug("onPoolTerm", head, tail, modifiers, flavor, formula));
|
||||
}
|
||||
const terms = [];
|
||||
if ( head ) terms.push(head, ...tail);
|
||||
return { class: "PoolTerm", terms, formula, modifiers, evaluated: false, options: { flavor } };
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle a parenthetical.
|
||||
* @param {RollParseNode} term The inner term.
|
||||
* @param {string|null} flavor Associated flavor text.
|
||||
* @param {string} formula The original matched text.
|
||||
* @returns {ParentheticalRollParseNode}
|
||||
* @internal
|
||||
* @protected
|
||||
*/
|
||||
_onParenthetical(term, flavor, formula) {
|
||||
if ( CONFIG.debug.rollParsing ) {
|
||||
console.debug(this.constructor.formatDebug("onParenthetical", term, flavor, formula));
|
||||
}
|
||||
return { class: "ParentheticalTerm", term, formula, evaluated: false, options: { flavor } };
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle some string that failed to be classified.
|
||||
* @param {string} term The term.
|
||||
* @param {string|null} [flavor] Associated flavor text.
|
||||
* @returns {StringParseNode}
|
||||
* @protected
|
||||
*/
|
||||
_onStringTerm(term, flavor) {
|
||||
return { class: "StringTerm", term, evaluated: false, options: { flavor } };
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Collapse multiple additive operators into a single one.
|
||||
* @param {string[]} operators A sequence of additive operators.
|
||||
* @returns {string}
|
||||
* @protected
|
||||
*/
|
||||
_collapseOperators(operators) {
|
||||
let head = operators.pop();
|
||||
for ( const operator of operators ) {
|
||||
if ( operator === "-" ) head = head === "+" ? "-" : "+";
|
||||
}
|
||||
return head;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Wrap a term with a leading minus.
|
||||
* @param {RollParseNode} term The term to wrap.
|
||||
* @returns {RollParseNode}
|
||||
* @protected
|
||||
*/
|
||||
_wrapNegativeTerm(term) {
|
||||
// Special case when we have a numeric term, otherwise we wrap it in a parenthetical.
|
||||
if ( term.class === "NumericTerm" ) {
|
||||
term.number *= -1;
|
||||
term.formula = `-${term.formula}`;
|
||||
return term;
|
||||
}
|
||||
|
||||
return foundry.dice.RollGrammar.parse(`(${term.formula} * -1)`, { parser: this.constructor });
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Tree Manipulation */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Flatten a tree structure (either a parse tree or AST) into an array with operators in infix notation.
|
||||
* @param {RollParseNode} root The root of the tree.
|
||||
* @returns {RollParseNode[]}
|
||||
*/
|
||||
static flattenTree(root) {
|
||||
const list = [];
|
||||
|
||||
/**
|
||||
* Flatten the given node.
|
||||
* @param {RollParseNode} node The node.
|
||||
*/
|
||||
function flattenNode(node) {
|
||||
if ( node.class !== "Node" ) {
|
||||
list.push(node);
|
||||
return;
|
||||
}
|
||||
|
||||
const [left, right] = node.operands;
|
||||
flattenNode(left);
|
||||
list.push({ class: "OperatorTerm", operator: node.operator, evaluated: false });
|
||||
flattenNode(right);
|
||||
}
|
||||
|
||||
flattenNode(root);
|
||||
return list;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Use the Shunting Yard algorithm to convert a parse tree or list of terms into an AST with correct operator
|
||||
* precedence.
|
||||
* See https://en.wikipedia.org/wiki/Shunting_yard_algorithm for a description of the algorithm in detail.
|
||||
* @param {RollParseNode|RollTerm[]} root The root of the parse tree or a list of terms.
|
||||
* @returns {RollParseNode} The root of the AST.
|
||||
*/
|
||||
static toAST(root) {
|
||||
// Flatten the parse tree to an array representing the original formula in infix notation.
|
||||
const list = Array.isArray(root) ? root : this.flattenTree(root);
|
||||
const operators = [];
|
||||
const output = [];
|
||||
|
||||
/**
|
||||
* Pop operators from the operator stack and push them onto the output stack until we reach an operator with lower
|
||||
* or equal precedence and left-associativity.
|
||||
* @param {RollParseNode} op The target operator to push.
|
||||
*/
|
||||
function pushOperator(op) {
|
||||
let peek = operators.at(-1);
|
||||
// We assume all our operators are left-associative, so we only check if the precedence is lower or equal here.
|
||||
while ( peek && ((OperatorTerm.PRECEDENCE[peek.operator] ?? 0) >= (OperatorTerm.PRECEDENCE[op.operator] ?? 0)) ) {
|
||||
output.push(operators.pop());
|
||||
peek = operators.at(-1);
|
||||
}
|
||||
operators.push(op);
|
||||
}
|
||||
|
||||
for ( const node of list ) {
|
||||
// If this is an operator, push it onto the operators stack.
|
||||
if ( this.isOperatorTerm(node) ) {
|
||||
pushOperator(node);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Recursively reorganize inner terms to AST sub-trees.
|
||||
if ( node.class === "ParentheticalTerm" ) node.term = this.toAST(node.term);
|
||||
else if ( (node.class === "FunctionTerm") || (node.class === "PoolTerm") ) {
|
||||
node.terms = node.terms.map(term => this.toAST(term));
|
||||
}
|
||||
|
||||
// Push the node onto the output stack.
|
||||
output.push(node);
|
||||
}
|
||||
|
||||
// Pop remaining operators off the operator stack and onto the output stack.
|
||||
while ( operators.length ) output.push(operators.pop());
|
||||
|
||||
// The output now contains the formula in postfix notation, with correct operator precedence applied. We recombine
|
||||
// it into a tree by matching each postfix operator with two operands.
|
||||
const ast = [];
|
||||
for ( const node of output ) {
|
||||
if ( !this.isOperatorTerm(node) ) {
|
||||
ast.push(node);
|
||||
continue;
|
||||
}
|
||||
const right = ast.pop();
|
||||
const left = ast.pop();
|
||||
ast.push({ class: "Node", operator: node.operator, operands: [left, right] });
|
||||
}
|
||||
|
||||
// The postfix array has been recombined into an array of one element, which is the root of the new AST.
|
||||
return ast.pop();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Determine if a given node is an operator term.
|
||||
* @param {RollParseNode|RollTerm} node
|
||||
*/
|
||||
static isOperatorTerm(node) {
|
||||
return (node instanceof OperatorTerm) || (node.class === "OperatorTerm");
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Debug Formatting */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Format a list argument.
|
||||
* @param {RollParseArg[]} list The list to format.
|
||||
* @returns {string}
|
||||
*/
|
||||
static formatList(list) {
|
||||
if ( !list ) return "[]";
|
||||
return `[${list.map(RollParser.formatArg).join(", ")}]`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Format a parser argument.
|
||||
* @param {RollParseArg} arg The argument.
|
||||
* @returns {string}
|
||||
*/
|
||||
static formatArg(arg) {
|
||||
switch ( getType(arg) ) {
|
||||
case "null": return "null";
|
||||
case "number": return `${arg}`;
|
||||
case "string": return `"${arg}"`;
|
||||
case "Object": return arg.class;
|
||||
case "Array": return RollParser.formatList(arg);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Format arguments for debugging.
|
||||
* @param {string} method The method name.
|
||||
* @param {...RollParseArg} args The arguments.
|
||||
* @returns {string}
|
||||
*/
|
||||
static formatDebug(method, ...args) {
|
||||
return `${method}(${args.map(RollParser.formatArg).join(", ")})`;
|
||||
}
|
||||
}
|
||||
1108
resources/app/client-esm/dice/roll.mjs
Normal file
1108
resources/app/client-esm/dice/roll.mjs
Normal file
File diff suppressed because it is too large
Load Diff
11
resources/app/client-esm/dice/terms/_module.mjs
Normal file
11
resources/app/client-esm/dice/terms/_module.mjs
Normal file
@@ -0,0 +1,11 @@
|
||||
export {default as Coin} from "./coin.mjs";
|
||||
export {default as DiceTerm} from "./dice.mjs";
|
||||
export {default as Die} from "./die.mjs"
|
||||
export {default as FateDie} from "./fate.mjs";
|
||||
export {default as FunctionTerm} from "./function.mjs";
|
||||
export {default as NumericTerm} from "./numeric.mjs";
|
||||
export {default as OperatorTerm} from "./operator.mjs";
|
||||
export {default as ParentheticalTerm} from "./parenthetical.mjs";
|
||||
export {default as PoolTerm} from "./pool.mjs";
|
||||
export {default as RollTerm} from "./term.mjs";
|
||||
export {default as StringTerm} from "./string.mjs";
|
||||
89
resources/app/client-esm/dice/terms/coin.mjs
Normal file
89
resources/app/client-esm/dice/terms/coin.mjs
Normal file
@@ -0,0 +1,89 @@
|
||||
import DiceTerm from "./dice.mjs";
|
||||
|
||||
/**
|
||||
* A type of DiceTerm used to represent flipping a two-sided coin.
|
||||
* @implements {DiceTerm}
|
||||
*/
|
||||
export default class Coin extends DiceTerm {
|
||||
constructor(termData) {
|
||||
termData.faces = 2;
|
||||
super(termData);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
static DENOMINATION = "c";
|
||||
|
||||
/** @inheritdoc */
|
||||
static MODIFIERS = {
|
||||
"c": "call"
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
async roll({minimize=false, maximize=false, ...options}={}) {
|
||||
const roll = {result: undefined, active: true};
|
||||
if ( minimize ) roll.result = 0;
|
||||
else if ( maximize ) roll.result = 1;
|
||||
else roll.result = await this._roll(options);
|
||||
if ( roll.result === undefined ) roll.result = this.randomFace();
|
||||
this.results.push(roll);
|
||||
return roll;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
getResultLabel(result) {
|
||||
return {
|
||||
"0": "T",
|
||||
"1": "H"
|
||||
}[result.result];
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
getResultCSS(result) {
|
||||
return [
|
||||
this.constructor.name.toLowerCase(),
|
||||
result.result === 1 ? "heads" : "tails",
|
||||
result.success ? "success" : null,
|
||||
result.failure ? "failure" : null
|
||||
]
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
mapRandomFace(randomUniform) {
|
||||
return Math.round(randomUniform);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Term Modifiers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Call the result of the coin flip, marking any coins that matched the called target as a success
|
||||
* 3dcc1 Flip 3 coins and treat "heads" as successes
|
||||
* 2dcc0 Flip 2 coins and treat "tails" as successes
|
||||
* @param {string} modifier The matched modifier query
|
||||
*/
|
||||
call(modifier) {
|
||||
|
||||
// Match the modifier
|
||||
const rgx = /c([01])/i;
|
||||
const match = modifier.match(rgx);
|
||||
if ( !match ) return false;
|
||||
let [target] = match.slice(1);
|
||||
target = parseInt(target);
|
||||
|
||||
// Treat each result which matched the call as a success
|
||||
for ( let r of this.results ) {
|
||||
const match = r.result === target;
|
||||
r.count = match ? 1 : 0;
|
||||
r.success = match;
|
||||
}
|
||||
}
|
||||
}
|
||||
705
resources/app/client-esm/dice/terms/dice.mjs
Normal file
705
resources/app/client-esm/dice/terms/dice.mjs
Normal file
@@ -0,0 +1,705 @@
|
||||
import RollTerm from "./term.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("../_types.mjs").DiceTermResult} DiceTermResult
|
||||
*/
|
||||
|
||||
/**
|
||||
* An abstract base class for any type of RollTerm which involves randomized input from dice, coins, or other devices.
|
||||
* @extends RollTerm
|
||||
*/
|
||||
export default class DiceTerm extends RollTerm {
|
||||
/**
|
||||
* @param {object} termData Data used to create the Dice Term, including the following:
|
||||
* @param {number|Roll} [termData.number=1] The number of dice of this term to roll, before modifiers are applied, or
|
||||
* a Roll instance that will be evaluated to a number.
|
||||
* @param {number|Roll} termData.faces The number of faces on each die of this type, or a Roll instance that
|
||||
* will be evaluated to a number.
|
||||
* @param {string[]} [termData.modifiers] An array of modifiers applied to the results
|
||||
* @param {object[]} [termData.results] An optional array of pre-cast results for the term
|
||||
* @param {object} [termData.options] Additional options that modify the term
|
||||
*/
|
||||
constructor({number=1, faces=6, method, modifiers=[], results=[], options={}}) {
|
||||
super({options});
|
||||
|
||||
this._number = number;
|
||||
this._faces = faces;
|
||||
this.method = method;
|
||||
this.modifiers = modifiers;
|
||||
this.results = results;
|
||||
|
||||
// If results were explicitly passed, the term has already been evaluated
|
||||
if ( results.length ) this._evaluated = true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The resolution method used to resolve this DiceTerm.
|
||||
* @type {string}
|
||||
*/
|
||||
get method() {
|
||||
return this.#method;
|
||||
}
|
||||
|
||||
set method(method) {
|
||||
if ( this.#method || !(method in CONFIG.Dice.fulfillment.methods) ) return;
|
||||
this.#method = method;
|
||||
}
|
||||
|
||||
#method;
|
||||
|
||||
/**
|
||||
* An Array of dice term modifiers which are applied
|
||||
* @type {string[]}
|
||||
*/
|
||||
modifiers;
|
||||
|
||||
/**
|
||||
* The array of dice term results which have been rolled
|
||||
* @type {DiceTermResult[]}
|
||||
*/
|
||||
results;
|
||||
|
||||
/**
|
||||
* Define the denomination string used to register this DiceTerm type in CONFIG.Dice.terms
|
||||
* @type {string}
|
||||
*/
|
||||
static DENOMINATION = "";
|
||||
|
||||
/**
|
||||
* Define the named modifiers that can be applied for this particular DiceTerm type.
|
||||
* @type {Record<string, string|Function>}
|
||||
*/
|
||||
static MODIFIERS = {};
|
||||
|
||||
/**
|
||||
* A regular expression pattern which captures the full set of term modifiers
|
||||
* Anything until a space, group symbol, or arithmetic operator
|
||||
* @type {string}
|
||||
*/
|
||||
static MODIFIERS_REGEXP_STRING = "([^ (){}[\\]+\\-*/]+)";
|
||||
|
||||
/**
|
||||
* A regular expression used to separate individual modifiers
|
||||
* @type {RegExp}
|
||||
*/
|
||||
static MODIFIER_REGEXP = /([A-z]+)([^A-z\s()+\-*\/]+)?/g
|
||||
|
||||
/** @inheritdoc */
|
||||
static REGEXP = new RegExp(`^([0-9]+)?[dD]([A-z]|[0-9]+)${this.MODIFIERS_REGEXP_STRING}?${this.FLAVOR_REGEXP_STRING}?$`);
|
||||
|
||||
/** @inheritdoc */
|
||||
static SERIALIZE_ATTRIBUTES = ["number", "faces", "modifiers", "results", "method"];
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Dice Term Attributes */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The number of dice of this term to roll. Returns undefined if the number is a complex term that has not yet been
|
||||
* evaluated.
|
||||
* @type {number|void}
|
||||
*/
|
||||
get number() {
|
||||
if ( typeof this._number === "number" ) return this._number;
|
||||
else if ( this._number?._evaluated ) return this._number.total;
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of dice of this term to roll, before modifiers are applied, or a Roll instance that will be evaluated to
|
||||
* a number.
|
||||
* @type {number|Roll}
|
||||
* @protected
|
||||
*/
|
||||
_number;
|
||||
|
||||
set number(value) {
|
||||
this._number = value;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The number of faces on the die. Returns undefined if the faces are represented as a complex term that has not yet
|
||||
* been evaluated.
|
||||
* @type {number|void}
|
||||
*/
|
||||
get faces() {
|
||||
if ( typeof this._faces === "number" ) return this._faces;
|
||||
else if ( this._faces?._evaluated ) return this._faces.total;
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of faces on the die, or a Roll instance that will be evaluated to a number.
|
||||
* @type {number|Roll}
|
||||
* @protected
|
||||
*/
|
||||
_faces;
|
||||
|
||||
set faces(value) {
|
||||
this._faces = value;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
get expression() {
|
||||
const x = this.constructor.DENOMINATION === "d" ? this._faces : this.constructor.DENOMINATION;
|
||||
return `${this._number}d${x}${this.modifiers.join("")}`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The denomination of this DiceTerm instance.
|
||||
* @type {string}
|
||||
*/
|
||||
get denomination() {
|
||||
return this.constructor.DENOMINATION;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* An array of additional DiceTerm instances involved in resolving this DiceTerm.
|
||||
* @type {DiceTerm[]}
|
||||
*/
|
||||
get dice() {
|
||||
const dice = [];
|
||||
if ( this._number instanceof Roll ) dice.push(...this._number.dice);
|
||||
if ( this._faces instanceof Roll ) dice.push(...this._faces.dice);
|
||||
return dice;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
get total() {
|
||||
if ( !this._evaluated ) return undefined;
|
||||
let total = this.results.reduce((t, r) => {
|
||||
if ( !r.active ) return t;
|
||||
if ( r.count !== undefined ) return t + r.count;
|
||||
else return t + r.result;
|
||||
}, 0);
|
||||
if ( this.number < 0 ) total *= -1;
|
||||
return total;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Return an array of rolled values which are still active within this term
|
||||
* @type {number[]}
|
||||
*/
|
||||
get values() {
|
||||
return this.results.reduce((arr, r) => {
|
||||
if ( !r.active ) return arr;
|
||||
arr.push(r.result);
|
||||
return arr;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
get isDeterministic() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Dice Term Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Alter the DiceTerm by adding or multiplying the number of dice which are rolled
|
||||
* @param {number} multiply A factor to multiply. Dice are multiplied before any additions.
|
||||
* @param {number} add A number of dice to add. Dice are added after multiplication.
|
||||
* @returns {DiceTerm} The altered term
|
||||
*/
|
||||
alter(multiply, add) {
|
||||
if ( this._evaluated ) throw new Error(`You may not alter a DiceTerm after it has already been evaluated`);
|
||||
multiply = Number.isFinite(multiply) && (multiply >= 0) ? multiply : 1;
|
||||
add = Number.isInteger(add) ? add : 0;
|
||||
if ( multiply >= 0 ) {
|
||||
if ( this._number instanceof Roll ) this._number = Roll.create(`(${this._number} * ${multiply})`);
|
||||
else this._number = Math.round(this.number * multiply);
|
||||
}
|
||||
if ( add ) {
|
||||
if ( this._number instanceof Roll ) this._number = Roll.create(`(${this._number} + ${add})`);
|
||||
else this._number += add;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
_evaluate(options={}) {
|
||||
if ( RollTerm.isDeterministic(this, options) ) return this._evaluateSync(options);
|
||||
return this._evaluateAsync(options);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Evaluate this dice term asynchronously.
|
||||
* @param {object} [options] Options forwarded to inner Roll evaluation.
|
||||
* @returns {Promise<DiceTerm>}
|
||||
* @protected
|
||||
*/
|
||||
async _evaluateAsync(options={}) {
|
||||
for ( const roll of [this._faces, this._number] ) {
|
||||
if ( !(roll instanceof Roll) ) continue;
|
||||
if ( this._root ) roll._root = this._root;
|
||||
await roll.evaluate(options);
|
||||
}
|
||||
if ( Math.abs(this.number) > 999 ) {
|
||||
throw new Error("You may not evaluate a DiceTerm with more than 999 requested results");
|
||||
}
|
||||
// If this term was an intermediate term, it has not yet been added to the resolver, so we add it here.
|
||||
if ( this.resolver && !this._id ) await this.resolver.addTerm(this);
|
||||
for ( let n = this.results.length; n < Math.abs(this.number); n++ ) await this.roll(options);
|
||||
await this._evaluateModifiers();
|
||||
return this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Evaluate deterministic values of this term synchronously.
|
||||
* @param {object} [options]
|
||||
* @param {boolean} [options.maximize] Force the result to be maximized.
|
||||
* @param {boolean} [options.minimize] Force the result to be minimized.
|
||||
* @param {boolean} [options.strict] Throw an error if attempting to evaluate a die term in a way that cannot be
|
||||
* done synchronously.
|
||||
* @returns {DiceTerm}
|
||||
* @protected
|
||||
*/
|
||||
_evaluateSync(options={}) {
|
||||
if ( this._faces instanceof Roll ) this._faces.evaluateSync(options);
|
||||
if ( this._number instanceof Roll ) this._number.evaluateSync(options);
|
||||
if ( Math.abs(this.number) > 999 ) {
|
||||
throw new Error("You may not evaluate a DiceTerm with more than 999 requested results");
|
||||
}
|
||||
for ( let n = this.results.length; n < Math.abs(this.number); n++ ) {
|
||||
const roll = { active: true };
|
||||
if ( options.minimize ) roll.result = Math.min(1, this.faces);
|
||||
else if ( options.maximize ) roll.result = this.faces;
|
||||
else if ( options.strict ) throw new Error("Cannot synchronously evaluate a non-deterministic term.");
|
||||
else continue;
|
||||
this.results.push(roll);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Roll the DiceTerm by mapping a random uniform draw against the faces of the dice term.
|
||||
* @param {object} [options={}] Options which modify how a random result is produced
|
||||
* @param {boolean} [options.minimize=false] Minimize the result, obtaining the smallest possible value.
|
||||
* @param {boolean} [options.maximize=false] Maximize the result, obtaining the largest possible value.
|
||||
* @returns {Promise<DiceTermResult>} The produced result
|
||||
*/
|
||||
async roll({minimize=false, maximize=false, ...options}={}) {
|
||||
const roll = {result: undefined, active: true};
|
||||
roll.result = await this._roll(options);
|
||||
if ( minimize ) roll.result = Math.min(1, this.faces);
|
||||
else if ( maximize ) roll.result = this.faces;
|
||||
else if ( roll.result === undefined ) roll.result = this.randomFace();
|
||||
this.results.push(roll);
|
||||
return roll;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Generate a roll result value for this DiceTerm based on its fulfillment method.
|
||||
* @param {object} [options] Options forwarded to the fulfillment method handler.
|
||||
* @returns {Promise<number|void>} Returns a Promise that resolves to the fulfilled number, or undefined if it could
|
||||
* not be fulfilled.
|
||||
* @protected
|
||||
*/
|
||||
async _roll(options={}) {
|
||||
return this.#invokeFulfillmentHandler(options);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Invoke the configured fulfillment handler for this term to produce a result value.
|
||||
* @param {object} [options] Options forwarded to the fulfillment method handler.
|
||||
* @returns {Promise<number|void>} Returns a Promise that resolves to the fulfilled number, or undefined if it could
|
||||
* not be fulfilled.
|
||||
*/
|
||||
async #invokeFulfillmentHandler(options={}) {
|
||||
const config = game.settings.get("core", "diceConfiguration");
|
||||
const method = config[this.denomination] || CONFIG.Dice.fulfillment.defaultMethod;
|
||||
if ( (method === "manual") && !game.user.hasPermission("MANUAL_ROLLS") ) return;
|
||||
const { handler, interactive } = CONFIG.Dice.fulfillment.methods[method] ?? {};
|
||||
if ( interactive && this.resolver ) return this.resolver.resolveResult(this, method, options);
|
||||
return handler?.(this, options);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Maps a randomly-generated value in the interval [0, 1) to a face value on the die.
|
||||
* @param {number} randomUniform A value to map. Must be in the interval [0, 1).
|
||||
* @returns {number} The face value.
|
||||
*/
|
||||
mapRandomFace(randomUniform) {
|
||||
return Math.ceil((1 - randomUniform) * this.faces);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Generate a random face value for this die using the configured PRNG.
|
||||
* @returns {number}
|
||||
*/
|
||||
randomFace() {
|
||||
return this.mapRandomFace(CONFIG.Dice.randomUniform());
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Return a string used as the label for each rolled result
|
||||
* @param {DiceTermResult} result The rolled result
|
||||
* @returns {string} The result label
|
||||
*/
|
||||
getResultLabel(result) {
|
||||
return String(result.result);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the CSS classes that should be used to display each rolled result
|
||||
* @param {DiceTermResult} result The rolled result
|
||||
* @returns {string[]} The desired classes
|
||||
*/
|
||||
getResultCSS(result) {
|
||||
const hasSuccess = result.success !== undefined;
|
||||
const hasFailure = result.failure !== undefined;
|
||||
const isMax = result.result === this.faces;
|
||||
const isMin = result.result === 1;
|
||||
return [
|
||||
this.constructor.name.toLowerCase(),
|
||||
"d" + this.faces,
|
||||
result.success ? "success" : null,
|
||||
result.failure ? "failure" : null,
|
||||
result.rerolled ? "rerolled" : null,
|
||||
result.exploded ? "exploded" : null,
|
||||
result.discarded ? "discarded" : null,
|
||||
!(hasSuccess || hasFailure) && isMin ? "min" : null,
|
||||
!(hasSuccess || hasFailure) && isMax ? "max" : null
|
||||
]
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Render the tooltip HTML for a Roll instance
|
||||
* @returns {object} The data object used to render the default tooltip template for this DiceTerm
|
||||
*/
|
||||
getTooltipData() {
|
||||
const { total, faces, flavor } = this;
|
||||
const method = CONFIG.Dice.fulfillment.methods[this.method];
|
||||
const icon = method?.interactive ? (method.icon ?? '<i class="fas fa-bluetooth"></i>') : null;
|
||||
return {
|
||||
total, faces, flavor, icon,
|
||||
method: method?.label,
|
||||
formula: this.expression,
|
||||
rolls: this.results.map(r => {
|
||||
return {
|
||||
result: this.getResultLabel(r),
|
||||
classes: this.getResultCSS(r).filterJoin(" ")
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Modifier Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Sequentially evaluate each dice roll modifier by passing the term to its evaluation function
|
||||
* Augment or modify the results array.
|
||||
* @internal
|
||||
*/
|
||||
async _evaluateModifiers() {
|
||||
const cls = this.constructor;
|
||||
const requested = foundry.utils.deepClone(this.modifiers);
|
||||
this.modifiers = [];
|
||||
|
||||
// Sort modifiers from longest to shortest to ensure that the matching algorithm greedily matches the longest
|
||||
// prefixes first.
|
||||
const allModifiers = Object.keys(cls.MODIFIERS).sort((a, b) => b.length - a.length);
|
||||
|
||||
// Iterate over requested modifiers
|
||||
for ( const m of requested ) {
|
||||
let command = m.match(/[A-z]+/)[0].toLowerCase();
|
||||
|
||||
// Matched command
|
||||
if ( command in cls.MODIFIERS ) {
|
||||
await this._evaluateModifier(command, m);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Unmatched compound command
|
||||
while ( command ) {
|
||||
let matched = false;
|
||||
for ( const modifier of allModifiers ) {
|
||||
if ( command.startsWith(modifier) ) {
|
||||
matched = true;
|
||||
await this._evaluateModifier(modifier, modifier);
|
||||
command = command.replace(modifier, "");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( !matched ) command = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Asynchronously evaluate a single modifier command, recording it in the array of evaluated modifiers
|
||||
* @param {string} command The parsed modifier command
|
||||
* @param {string} modifier The full modifier request
|
||||
* @internal
|
||||
*/
|
||||
async _evaluateModifier(command, modifier) {
|
||||
let fn = this.constructor.MODIFIERS[command];
|
||||
if ( typeof fn === "string" ) fn = this[fn];
|
||||
if ( fn instanceof Function ) {
|
||||
const result = await fn.call(this, modifier);
|
||||
const earlyReturn = (result === false) || (result === this); // handling this is backwards compatibility
|
||||
if ( !earlyReturn ) this.modifiers.push(modifier.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A helper comparison function.
|
||||
* Returns a boolean depending on whether the result compares favorably against the target.
|
||||
* @param {number} result The result being compared
|
||||
* @param {string} comparison The comparison operator in [=,<,<=,>,>=]
|
||||
* @param {number} target The target value
|
||||
* @returns {boolean} Is the comparison true?
|
||||
*/
|
||||
static compareResult(result, comparison, target) {
|
||||
switch ( comparison ) {
|
||||
case "=":
|
||||
return result === target;
|
||||
case "<":
|
||||
return result < target;
|
||||
case "<=":
|
||||
return result <= target;
|
||||
case ">":
|
||||
return result > target;
|
||||
case ">=":
|
||||
return result >= target;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A helper method to modify the results array of a dice term by flagging certain results are kept or dropped.
|
||||
* @param {object[]} results The results array
|
||||
* @param {number} number The number to keep or drop
|
||||
* @param {boolean} [keep] Keep results?
|
||||
* @param {boolean} [highest] Keep the highest?
|
||||
* @returns {object[]} The modified results array
|
||||
*/
|
||||
static _keepOrDrop(results, number, {keep=true, highest=true}={}) {
|
||||
|
||||
// Sort remaining active results in ascending (keep) or descending (drop) order
|
||||
const ascending = keep === highest;
|
||||
const values = results.reduce((arr, r) => {
|
||||
if ( r.active ) arr.push(r.result);
|
||||
return arr;
|
||||
}, []).sort((a, b) => ascending ? a - b : b - a);
|
||||
|
||||
// Determine the cut point, beyond which to discard
|
||||
number = Math.clamp(keep ? values.length - number : number, 0, values.length);
|
||||
const cut = values[number];
|
||||
|
||||
// Track progress
|
||||
let discarded = 0;
|
||||
const ties = [];
|
||||
let comp = ascending ? "<" : ">";
|
||||
|
||||
// First mark results on the wrong side of the cut as discarded
|
||||
results.forEach(r => {
|
||||
if ( !r.active ) return; // Skip results which have already been discarded
|
||||
let discard = this.compareResult(r.result, comp, cut);
|
||||
if ( discard ) {
|
||||
r.discarded = true;
|
||||
r.active = false;
|
||||
discarded++;
|
||||
}
|
||||
else if ( r.result === cut ) ties.push(r);
|
||||
});
|
||||
|
||||
// Next discard ties until we have reached the target
|
||||
ties.forEach(r => {
|
||||
if ( discarded < number ) {
|
||||
r.discarded = true;
|
||||
r.active = false;
|
||||
discarded++;
|
||||
}
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A reusable helper function to handle the identification and deduction of failures
|
||||
*/
|
||||
static _applyCount(results, comparison, target, {flagSuccess=false, flagFailure=false}={}) {
|
||||
for ( let r of results ) {
|
||||
let success = this.compareResult(r.result, comparison, target);
|
||||
if (flagSuccess) {
|
||||
r.success = success;
|
||||
if (success) delete r.failure;
|
||||
}
|
||||
else if (flagFailure ) {
|
||||
r.failure = success;
|
||||
if (success) delete r.success;
|
||||
}
|
||||
r.count = success ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A reusable helper function to handle the identification and deduction of failures
|
||||
*/
|
||||
static _applyDeduct(results, comparison, target, {deductFailure=false, invertFailure=false}={}) {
|
||||
for ( let r of results ) {
|
||||
|
||||
// Flag failures if a comparison was provided
|
||||
if (comparison) {
|
||||
const fail = this.compareResult(r.result, comparison, target);
|
||||
if ( fail ) {
|
||||
r.failure = true;
|
||||
delete r.success;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise treat successes as failures
|
||||
else {
|
||||
if ( r.success === false ) {
|
||||
r.failure = true;
|
||||
delete r.success;
|
||||
}
|
||||
}
|
||||
|
||||
// Deduct failures
|
||||
if ( deductFailure ) {
|
||||
if ( r.failure ) r.count = -1;
|
||||
}
|
||||
else if ( invertFailure ) {
|
||||
if ( r.failure ) r.count = -1 * r.result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Factory Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Determine whether a string expression matches this type of term
|
||||
* @param {string} expression The expression to parse
|
||||
* @param {object} [options={}] Additional options which customize the match
|
||||
* @param {boolean} [options.imputeNumber=true] Allow the number of dice to be optional, i.e. "d6"
|
||||
* @returns {RegExpMatchArray|null}
|
||||
*/
|
||||
static matchTerm(expression, {imputeNumber=true}={}) {
|
||||
const match = expression.match(this.REGEXP);
|
||||
if ( !match ) return null;
|
||||
if ( (match[1] === undefined) && !imputeNumber ) return null;
|
||||
return match;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Construct a term of this type given a matched regular expression array.
|
||||
* @param {RegExpMatchArray} match The matched regular expression array
|
||||
* @returns {DiceTerm} The constructed term
|
||||
*/
|
||||
static fromMatch(match) {
|
||||
let [number, denomination, modifiers, flavor] = match.slice(1);
|
||||
|
||||
// Get the denomination of DiceTerm
|
||||
denomination = denomination.toLowerCase();
|
||||
const cls = denomination in CONFIG.Dice.terms ? CONFIG.Dice.terms[denomination] : CONFIG.Dice.terms.d;
|
||||
if ( !foundry.utils.isSubclass(cls, foundry.dice.terms.DiceTerm) ) {
|
||||
throw new Error(`DiceTerm denomination ${denomination} not registered to CONFIG.Dice.terms as a valid DiceTerm class`);
|
||||
}
|
||||
|
||||
// Get the term arguments
|
||||
number = Number.isNumeric(number) ? parseInt(number) : 1;
|
||||
const faces = Number.isNumeric(denomination) ? parseInt(denomination) : null;
|
||||
|
||||
// Match modifiers
|
||||
modifiers = Array.from((modifiers || "").matchAll(this.MODIFIER_REGEXP)).map(m => m[0]);
|
||||
|
||||
// Construct a term of the appropriate denomination
|
||||
return new cls({number, faces, modifiers, options: {flavor}});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static fromParseNode(node) {
|
||||
let { number, faces } = node;
|
||||
let denomination = "d";
|
||||
if ( number === null ) number = 1;
|
||||
if ( number.class ) {
|
||||
number = Roll.defaultImplementation.fromTerms(Roll.defaultImplementation.instantiateAST(number));
|
||||
}
|
||||
if ( typeof faces === "string" ) denomination = faces.toLowerCase();
|
||||
else if ( faces.class ) {
|
||||
faces = Roll.defaultImplementation.fromTerms(Roll.defaultImplementation.instantiateAST(faces));
|
||||
}
|
||||
const modifiers = Array.from((node.modifiers || "").matchAll(this.MODIFIER_REGEXP)).map(([m]) => m);
|
||||
const cls = CONFIG.Dice.terms[denomination];
|
||||
const data = { ...node, number, modifiers, class: cls.name };
|
||||
if ( denomination === "d" ) data.faces = faces;
|
||||
return this.fromData(data);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Serialization & Loading */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
toJSON() {
|
||||
const data = super.toJSON();
|
||||
if ( this._number instanceof Roll ) data._number = this._number.toJSON();
|
||||
if ( this._faces instanceof Roll ) data._faces = this._faces.toJSON();
|
||||
return data;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
static _fromData(data) {
|
||||
if ( data._number ) data.number = Roll.fromData(data._number);
|
||||
if ( data._faces ) data.faces = Roll.fromData(data._faces);
|
||||
return super._fromData(data);
|
||||
}
|
||||
}
|
||||
421
resources/app/client-esm/dice/terms/die.mjs
Normal file
421
resources/app/client-esm/dice/terms/die.mjs
Normal file
@@ -0,0 +1,421 @@
|
||||
import DiceTerm from "./dice.mjs";
|
||||
|
||||
/**
|
||||
* A type of DiceTerm used to represent rolling a fair n-sided die.
|
||||
* @implements {DiceTerm}
|
||||
*
|
||||
* @example Roll four six-sided dice
|
||||
* ```js
|
||||
* let die = new Die({faces: 6, number: 4}).evaluate();
|
||||
* ```
|
||||
*/
|
||||
export default class Die extends DiceTerm {
|
||||
/** @inheritdoc */
|
||||
static DENOMINATION = "d";
|
||||
|
||||
/** @inheritdoc */
|
||||
static MODIFIERS = {
|
||||
r: "reroll",
|
||||
rr: "rerollRecursive",
|
||||
x: "explode",
|
||||
xo: "explodeOnce",
|
||||
k: "keep",
|
||||
kh: "keep",
|
||||
kl: "keep",
|
||||
d: "drop",
|
||||
dh: "drop",
|
||||
dl: "drop",
|
||||
min: "minimum",
|
||||
max: "maximum",
|
||||
even: "countEven",
|
||||
odd: "countOdd",
|
||||
cs: "countSuccess",
|
||||
cf: "countFailures",
|
||||
df: "deductFailures",
|
||||
sf: "subtractFailures",
|
||||
ms: "marginSuccess"
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
get total() {
|
||||
const total = super.total;
|
||||
if ( this.options.marginSuccess ) return total - parseInt(this.options.marginSuccess);
|
||||
else if ( this.options.marginFailure ) return parseInt(this.options.marginFailure) - total;
|
||||
else return total;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
get denomination() {
|
||||
return `d${this.faces}`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Term Modifiers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Re-roll the Die, rolling additional results for any values which fall within a target set.
|
||||
* If no target number is specified, re-roll the lowest possible result.
|
||||
*
|
||||
* 20d20r reroll all 1s
|
||||
* 20d20r1 reroll all 1s
|
||||
* 20d20r=1 reroll all 1s
|
||||
* 20d20r1=1 reroll a single 1
|
||||
*
|
||||
* @param {string} modifier The matched modifier query
|
||||
* @param {boolean} recursive Reroll recursively, continuing to reroll until the condition is no longer met
|
||||
* @returns {Promise<false|void>} False if the modifier was unmatched
|
||||
*/
|
||||
async reroll(modifier, {recursive=false}={}) {
|
||||
|
||||
// Match the re-roll modifier
|
||||
const rgx = /rr?([0-9]+)?([<>=]+)?([0-9]+)?/i;
|
||||
const match = modifier.match(rgx);
|
||||
if ( !match ) return false;
|
||||
let [max, comparison, target] = match.slice(1);
|
||||
|
||||
// If no comparison or target are provided, treat the max as the target
|
||||
if ( max && !(target || comparison) ) {
|
||||
target = max;
|
||||
max = null;
|
||||
}
|
||||
|
||||
// Determine target values
|
||||
max = Number.isNumeric(max) ? parseInt(max) : null;
|
||||
target = Number.isNumeric(target) ? parseInt(target) : 1;
|
||||
comparison = comparison || "=";
|
||||
|
||||
// Recursively reroll until there are no remaining results to reroll
|
||||
let checked = 0;
|
||||
const initial = this.results.length;
|
||||
while ( checked < this.results.length ) {
|
||||
const r = this.results[checked];
|
||||
checked++;
|
||||
if ( !r.active ) continue;
|
||||
|
||||
// Maybe we have run out of rerolls
|
||||
if ( (max !== null) && (max <= 0) ) break;
|
||||
|
||||
// Determine whether to re-roll the result
|
||||
if ( DiceTerm.compareResult(r.result, comparison, target) ) {
|
||||
r.rerolled = true;
|
||||
r.active = false;
|
||||
await this.roll({ reroll: true });
|
||||
if ( max !== null ) max -= 1;
|
||||
}
|
||||
|
||||
// Limit recursion
|
||||
if ( !recursive && (checked >= initial) ) checked = this.results.length;
|
||||
if ( checked > 1000 ) throw new Error("Maximum recursion depth for exploding dice roll exceeded");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see {@link Die#reroll}
|
||||
*/
|
||||
async rerollRecursive(modifier) {
|
||||
return this.reroll(modifier, {recursive: true});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Explode the Die, rolling additional results for any values which match the target set.
|
||||
* If no target number is specified, explode the highest possible result.
|
||||
* Explosion can be a "small explode" using a lower-case x or a "big explode" using an upper-case "X"
|
||||
*
|
||||
* @param {string} modifier The matched modifier query
|
||||
* @param {boolean} recursive Explode recursively, such that new rolls can also explode?
|
||||
* @returns {Promise<false|void>} False if the modifier was unmatched.
|
||||
*/
|
||||
async explode(modifier, {recursive=true}={}) {
|
||||
|
||||
// Match the "explode" or "explode once" modifier
|
||||
const rgx = /xo?([0-9]+)?([<>=]+)?([0-9]+)?/i;
|
||||
const match = modifier.match(rgx);
|
||||
if ( !match ) return false;
|
||||
let [max, comparison, target] = match.slice(1);
|
||||
|
||||
// If no comparison or target are provided, treat the max as the target value
|
||||
if ( max && !(target || comparison) ) {
|
||||
target = max;
|
||||
max = null;
|
||||
}
|
||||
|
||||
// Determine target values
|
||||
target = Number.isNumeric(target) ? parseInt(target) : this.faces;
|
||||
comparison = comparison || "=";
|
||||
|
||||
// Determine the number of allowed explosions
|
||||
max = Number.isNumeric(max) ? parseInt(max) : null;
|
||||
|
||||
// Recursively explode until there are no remaining results to explode
|
||||
let checked = 0;
|
||||
const initial = this.results.length;
|
||||
while ( checked < this.results.length ) {
|
||||
const r = this.results[checked];
|
||||
checked++;
|
||||
if ( !r.active ) continue;
|
||||
|
||||
// Maybe we have run out of explosions
|
||||
if ( (max !== null) && (max <= 0) ) break;
|
||||
|
||||
// Determine whether to explode the result and roll again!
|
||||
if ( DiceTerm.compareResult(r.result, comparison, target) ) {
|
||||
r.exploded = true;
|
||||
await this.roll({ explode: true });
|
||||
if ( max !== null ) max -= 1;
|
||||
}
|
||||
|
||||
// Limit recursion
|
||||
if ( !recursive && (checked === initial) ) break;
|
||||
if ( checked > 1000 ) throw new Error("Maximum recursion depth for exploding dice roll exceeded");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see {@link Die#explode}
|
||||
*/
|
||||
async explodeOnce(modifier) {
|
||||
return this.explode(modifier, {recursive: false});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Keep a certain number of highest or lowest dice rolls from the result set.
|
||||
*
|
||||
* 20d20k Keep the 1 highest die
|
||||
* 20d20kh Keep the 1 highest die
|
||||
* 20d20kh10 Keep the 10 highest die
|
||||
* 20d20kl Keep the 1 lowest die
|
||||
* 20d20kl10 Keep the 10 lowest die
|
||||
*
|
||||
* @param {string} modifier The matched modifier query
|
||||
*/
|
||||
keep(modifier) {
|
||||
const rgx = /k([hl])?([0-9]+)?/i;
|
||||
const match = modifier.match(rgx);
|
||||
if ( !match ) return false;
|
||||
let [direction, number] = match.slice(1);
|
||||
direction = direction ? direction.toLowerCase() : "h";
|
||||
number = parseInt(number) || 1;
|
||||
DiceTerm._keepOrDrop(this.results, number, {keep: true, highest: direction === "h"});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Drop a certain number of highest or lowest dice rolls from the result set.
|
||||
*
|
||||
* 20d20d Drop the 1 lowest die
|
||||
* 20d20dh Drop the 1 highest die
|
||||
* 20d20dl Drop the 1 lowest die
|
||||
* 20d20dh10 Drop the 10 highest die
|
||||
* 20d20dl10 Drop the 10 lowest die
|
||||
*
|
||||
* @param {string} modifier The matched modifier query
|
||||
*/
|
||||
drop(modifier) {
|
||||
const rgx = /d([hl])?([0-9]+)?/i;
|
||||
const match = modifier.match(rgx);
|
||||
if ( !match ) return false;
|
||||
let [direction, number] = match.slice(1);
|
||||
direction = direction ? direction.toLowerCase() : "l";
|
||||
number = parseInt(number) || 1;
|
||||
DiceTerm._keepOrDrop(this.results, number, {keep: false, highest: direction !== "l"});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Count the number of successful results which occurred in a given result set.
|
||||
* Successes are counted relative to some target, or relative to the maximum possible value if no target is given.
|
||||
* Applying a count-success modifier to the results re-casts all results to 1 (success) or 0 (failure)
|
||||
*
|
||||
* 20d20cs Count the number of dice which rolled a 20
|
||||
* 20d20cs>10 Count the number of dice which rolled higher than 10
|
||||
* 20d20cs<10 Count the number of dice which rolled less than 10
|
||||
*
|
||||
* @param {string} modifier The matched modifier query
|
||||
*/
|
||||
countSuccess(modifier) {
|
||||
const rgx = /(?:cs)([<>=]+)?([0-9]+)?/i;
|
||||
const match = modifier.match(rgx);
|
||||
if ( !match ) return false;
|
||||
let [comparison, target] = match.slice(1);
|
||||
comparison = comparison || "=";
|
||||
target = parseInt(target) ?? this.faces;
|
||||
DiceTerm._applyCount(this.results, comparison, target, {flagSuccess: true});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Count the number of failed results which occurred in a given result set.
|
||||
* Failures are counted relative to some target, or relative to the lowest possible value if no target is given.
|
||||
* Applying a count-failures modifier to the results re-casts all results to 1 (failure) or 0 (non-failure)
|
||||
*
|
||||
* 6d6cf Count the number of dice which rolled a 1 as failures
|
||||
* 6d6cf<=3 Count the number of dice which rolled less than 3 as failures
|
||||
* 6d6cf>4 Count the number of dice which rolled greater than 4 as failures
|
||||
*
|
||||
* @param {string} modifier The matched modifier query
|
||||
*/
|
||||
countFailures(modifier) {
|
||||
const rgx = /(?:cf)([<>=]+)?([0-9]+)?/i;
|
||||
const match = modifier.match(rgx);
|
||||
if ( !match ) return false;
|
||||
let [comparison, target] = match.slice(1);
|
||||
comparison = comparison || "=";
|
||||
target = parseInt(target) ?? 1;
|
||||
DiceTerm._applyCount(this.results, comparison, target, {flagFailure: true});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Count the number of even results which occurred in a given result set.
|
||||
* Even numbers are marked as a success and counted as 1
|
||||
* Odd numbers are marked as a non-success and counted as 0.
|
||||
*
|
||||
* 6d6even Count the number of even numbers rolled
|
||||
*
|
||||
* @param {string} modifier The matched modifier query
|
||||
*/
|
||||
countEven(modifier) {
|
||||
for ( let r of this.results ) {
|
||||
r.success = ( (r.result % 2) === 0 );
|
||||
r.count = r.success ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Count the number of odd results which occurred in a given result set.
|
||||
* Odd numbers are marked as a success and counted as 1
|
||||
* Even numbers are marked as a non-success and counted as 0.
|
||||
*
|
||||
* 6d6odd Count the number of odd numbers rolled
|
||||
*
|
||||
* @param {string} modifier The matched modifier query
|
||||
*/
|
||||
countOdd(modifier) {
|
||||
for ( let r of this.results ) {
|
||||
r.success = ( (r.result % 2) !== 0 );
|
||||
r.count = r.success ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Deduct the number of failures from the dice result, counting each failure as -1
|
||||
* Failures are identified relative to some target, or relative to the lowest possible value if no target is given.
|
||||
* Applying a deduct-failures modifier to the results counts all failed results as -1.
|
||||
*
|
||||
* 6d6df Subtract the number of dice which rolled a 1 from the non-failed total.
|
||||
* 6d6cs>3df Subtract the number of dice which rolled a 3 or less from the non-failed count.
|
||||
* 6d6cf<3df Subtract the number of dice which rolled less than 3 from the non-failed count.
|
||||
*
|
||||
* @param {string} modifier The matched modifier query
|
||||
*/
|
||||
deductFailures(modifier) {
|
||||
const rgx = /(?:df)([<>=]+)?([0-9]+)?/i;
|
||||
const match = modifier.match(rgx);
|
||||
if ( !match ) return false;
|
||||
let [comparison, target] = match.slice(1);
|
||||
if ( comparison || target ) {
|
||||
comparison = comparison || "=";
|
||||
target = parseInt(target) ?? 1;
|
||||
}
|
||||
DiceTerm._applyDeduct(this.results, comparison, target, {deductFailure: true});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Subtract the value of failed dice from the non-failed total, where each failure counts as its negative value.
|
||||
* Failures are identified relative to some target, or relative to the lowest possible value if no target is given.
|
||||
* Applying a deduct-failures modifier to the results counts all failed results as -1.
|
||||
*
|
||||
* 6d6df<3 Subtract the value of results which rolled less than 3 from the non-failed total.
|
||||
*
|
||||
* @param {string} modifier The matched modifier query
|
||||
*/
|
||||
subtractFailures(modifier) {
|
||||
const rgx = /(?:sf)([<>=]+)?([0-9]+)?/i;
|
||||
const match = modifier.match(rgx);
|
||||
if ( !match ) return false;
|
||||
let [comparison, target] = match.slice(1);
|
||||
if ( comparison || target ) {
|
||||
comparison = comparison || "=";
|
||||
target = parseInt(target) ?? 1;
|
||||
}
|
||||
DiceTerm._applyDeduct(this.results, comparison, target, {invertFailure: true});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Subtract the total value of the DiceTerm from a target value, treating the difference as the final total.
|
||||
* Example: 6d6ms>12 Roll 6d6 and subtract 12 from the resulting total.
|
||||
* @param {string} modifier The matched modifier query
|
||||
*/
|
||||
marginSuccess(modifier) {
|
||||
const rgx = /(?:ms)([<>=]+)?([0-9]+)?/i;
|
||||
const match = modifier.match(rgx);
|
||||
if ( !match ) return false;
|
||||
let [comparison, target] = match.slice(1);
|
||||
target = parseInt(target);
|
||||
if ( [">", ">=", "=", undefined].includes(comparison) ) this.options.marginSuccess = target;
|
||||
else if ( ["<", "<="].includes(comparison) ) this.options.marginFailure = target;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Constrain each rolled result to be at least some minimum value.
|
||||
* Example: 6d6min2 Roll 6d6, each result must be at least 2
|
||||
* @param {string} modifier The matched modifier query
|
||||
*/
|
||||
minimum(modifier) {
|
||||
const rgx = /(?:min)([0-9]+)/i;
|
||||
const match = modifier.match(rgx);
|
||||
if ( !match ) return false;
|
||||
let [target] = match.slice(1);
|
||||
target = parseInt(target);
|
||||
for ( let r of this.results ) {
|
||||
if ( r.result < target ) {
|
||||
r.count = target;
|
||||
r.rerolled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Constrain each rolled result to be at most some maximum value.
|
||||
* Example: 6d6max5 Roll 6d6, each result must be at most 5
|
||||
* @param {string} modifier The matched modifier query
|
||||
*/
|
||||
maximum(modifier) {
|
||||
const rgx = /(?:max)([0-9]+)/i;
|
||||
const match = modifier.match(rgx);
|
||||
if ( !match ) return false;
|
||||
let [target] = match.slice(1);
|
||||
target = parseInt(target);
|
||||
for ( let r of this.results ) {
|
||||
if ( r.result > target ) {
|
||||
r.count = target;
|
||||
r.rerolled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
62
resources/app/client-esm/dice/terms/fate.mjs
Normal file
62
resources/app/client-esm/dice/terms/fate.mjs
Normal file
@@ -0,0 +1,62 @@
|
||||
import DiceTerm from "./dice.mjs";
|
||||
import Die from "./die.mjs";
|
||||
|
||||
/**
|
||||
* A type of DiceTerm used to represent a three-sided Fate/Fudge die.
|
||||
* Mathematically behaves like 1d3-2
|
||||
* @extends {DiceTerm}
|
||||
*/
|
||||
export default class FateDie extends DiceTerm {
|
||||
constructor(termData) {
|
||||
termData.faces = 3;
|
||||
super(termData);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
static DENOMINATION = "f";
|
||||
|
||||
/** @inheritdoc */
|
||||
static MODIFIERS = {
|
||||
"r": Die.prototype.reroll,
|
||||
"rr": Die.prototype.rerollRecursive,
|
||||
"k": Die.prototype.keep,
|
||||
"kh": Die.prototype.keep,
|
||||
"kl": Die.prototype.keep,
|
||||
"d": Die.prototype.drop,
|
||||
"dh": Die.prototype.drop,
|
||||
"dl": Die.prototype.drop
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
async roll({minimize=false, maximize=false, ...options}={}) {
|
||||
const roll = {result: undefined, active: true};
|
||||
if ( minimize ) roll.result = -1;
|
||||
else if ( maximize ) roll.result = 1;
|
||||
else roll.result = await this._roll(options);
|
||||
if ( roll.result === undefined ) roll.result = this.randomFace();
|
||||
if ( roll.result === -1 ) roll.failure = true;
|
||||
if ( roll.result === 1 ) roll.success = true;
|
||||
this.results.push(roll);
|
||||
return roll;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
mapRandomFace(randomUniform) {
|
||||
return Math.ceil((randomUniform * this.faces) - 2);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
getResultLabel(result) {
|
||||
return {
|
||||
"-1": "-",
|
||||
"0": " ",
|
||||
"1": "+"
|
||||
}[result.result];
|
||||
}
|
||||
}
|
||||
177
resources/app/client-esm/dice/terms/function.mjs
Normal file
177
resources/app/client-esm/dice/terms/function.mjs
Normal file
@@ -0,0 +1,177 @@
|
||||
import RollTerm from "./term.mjs";
|
||||
import DiceTerm from "./dice.mjs";
|
||||
|
||||
/**
|
||||
* A type of RollTerm used to apply a function.
|
||||
* @extends {RollTerm}
|
||||
*/
|
||||
export default class FunctionTerm extends RollTerm {
|
||||
constructor({fn, terms=[], rolls=[], result, options}={}) {
|
||||
super({options});
|
||||
this.fn = fn;
|
||||
this.terms = terms;
|
||||
this.rolls = (rolls.length === terms.length) ? rolls : this.terms.map(t => Roll.create(t));
|
||||
this.result = result;
|
||||
if ( result !== undefined ) this._evaluated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the configured function, or one in the Math environment, which should be applied to the term
|
||||
* @type {string}
|
||||
*/
|
||||
fn;
|
||||
|
||||
/**
|
||||
* An array of string argument terms for the function
|
||||
* @type {string[]}
|
||||
*/
|
||||
terms;
|
||||
|
||||
/**
|
||||
* The cached Roll instances for each function argument
|
||||
* @type {Roll[]}
|
||||
*/
|
||||
rolls = [];
|
||||
|
||||
/**
|
||||
* The cached result of evaluating the method arguments
|
||||
* @type {string|number}
|
||||
*/
|
||||
result;
|
||||
|
||||
/** @inheritdoc */
|
||||
isIntermediate = true;
|
||||
|
||||
/** @inheritdoc */
|
||||
static SERIALIZE_ATTRIBUTES = ["fn", "terms", "rolls", "result"];
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Function Term Attributes */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* An array of evaluated DiceTerm instances that should be bubbled up to the parent Roll
|
||||
* @type {DiceTerm[]}
|
||||
*/
|
||||
get dice() {
|
||||
return this.rolls.flatMap(r => r.dice);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
get total() {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
get expression() {
|
||||
return `${this.fn}(${this.terms.join(",")})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The function this term represents.
|
||||
* @returns {RollFunction}
|
||||
*/
|
||||
get function() {
|
||||
return CONFIG.Dice.functions[this.fn] ?? Math[this.fn];
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
get isDeterministic() {
|
||||
if ( this.function?.constructor.name === "AsyncFunction" ) return false;
|
||||
return this.terms.every(t => Roll.create(t).isDeterministic);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Math Term Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
_evaluate(options={}) {
|
||||
if ( RollTerm.isDeterministic(this, options) ) return this._evaluateSync(options);
|
||||
return this._evaluateAsync(options);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Evaluate this function when it contains any non-deterministic sub-terms.
|
||||
* @param {object} [options]
|
||||
* @returns {Promise<RollTerm>}
|
||||
* @protected
|
||||
*/
|
||||
async _evaluateAsync(options={}) {
|
||||
const args = await Promise.all(this.rolls.map(async roll => {
|
||||
if ( this._root ) roll._root = this._root;
|
||||
await roll.evaluate({ ...options, allowStrings: true });
|
||||
roll.propagateFlavor(this.flavor);
|
||||
return this.#parseArgument(roll);
|
||||
}));
|
||||
this.result = await this.function(...args);
|
||||
if ( !options.allowStrings ) this.result = Number(this.result);
|
||||
return this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Evaluate this function when it contains only deterministic sub-terms.
|
||||
* @param {object} [options]
|
||||
* @returns {RollTerm}
|
||||
* @protected
|
||||
*/
|
||||
_evaluateSync(options={}) {
|
||||
const args = [];
|
||||
for ( const roll of this.rolls ) {
|
||||
roll.evaluateSync({ ...options, allowStrings: true });
|
||||
roll.propagateFlavor(this.flavor);
|
||||
args.push(this.#parseArgument(roll));
|
||||
}
|
||||
this.result = this.function(...args);
|
||||
if ( !options.allowStrings ) this.result = Number(this.result);
|
||||
return this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Parse a function argument from its evaluated Roll instance.
|
||||
* @param {Roll} roll The evaluated Roll instance that wraps the argument.
|
||||
* @returns {string|number}
|
||||
*/
|
||||
#parseArgument(roll) {
|
||||
const { product } = roll;
|
||||
if ( typeof product !== "string" ) return product;
|
||||
const [, value] = product.match(/^\$([^$]+)\$$/) || [];
|
||||
return value ? JSON.parse(value) : product;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Saving and Loading */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
static _fromData(data) {
|
||||
data.rolls = (data.rolls || []).map(r => r instanceof Roll ? r : Roll.fromData(r));
|
||||
return super._fromData(data);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
toJSON() {
|
||||
const data = super.toJSON();
|
||||
data.rolls = data.rolls.map(r => r.toJSON());
|
||||
return data;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static fromParseNode(node) {
|
||||
const rolls = node.terms.map(t => {
|
||||
return Roll.defaultImplementation.fromTerms(Roll.defaultImplementation.instantiateAST(t));
|
||||
});
|
||||
const modifiers = Array.from((node.modifiers || "").matchAll(DiceTerm.MODIFIER_REGEXP)).map(([m]) => m);
|
||||
return this.fromData({ ...node, rolls, modifiers, terms: rolls.map(r => r.formula) });
|
||||
}
|
||||
}
|
||||
59
resources/app/client-esm/dice/terms/numeric.mjs
Normal file
59
resources/app/client-esm/dice/terms/numeric.mjs
Normal file
@@ -0,0 +1,59 @@
|
||||
import RollTerm from "./term.mjs";
|
||||
|
||||
/**
|
||||
* A type of RollTerm used to represent static numbers.
|
||||
* @extends {RollTerm}
|
||||
*/
|
||||
export default class NumericTerm extends RollTerm {
|
||||
constructor({number, options}={}) {
|
||||
super({options});
|
||||
this.number = Number(number);
|
||||
}
|
||||
|
||||
/**
|
||||
* The term's numeric value.
|
||||
* @type {number}
|
||||
*/
|
||||
number;
|
||||
|
||||
/** @inheritdoc */
|
||||
static REGEXP = new RegExp(`^([0-9]+(?:\\.[0-9]+)?)${RollTerm.FLAVOR_REGEXP_STRING}?$`);
|
||||
|
||||
/** @inheritdoc */
|
||||
static SERIALIZE_ATTRIBUTES = ["number"];
|
||||
|
||||
/** @inheritdoc */
|
||||
get expression() {
|
||||
return String(this.number);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
get total() {
|
||||
return this.number;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Factory Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Determine whether a string expression matches a NumericTerm
|
||||
* @param {string} expression The expression to parse
|
||||
* @returns {RegExpMatchArray|null}
|
||||
*/
|
||||
static matchTerm(expression) {
|
||||
return expression.match(this.REGEXP) || null;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Construct a term of this type given a matched regular expression array.
|
||||
* @param {RegExpMatchArray} match The matched regular expression array
|
||||
* @returns {NumericTerm} The constructed term
|
||||
*/
|
||||
static fromMatch(match) {
|
||||
const [number, flavor] = match.slice(1);
|
||||
return new this({number, options: {flavor}});
|
||||
}
|
||||
}
|
||||
58
resources/app/client-esm/dice/terms/operator.mjs
Normal file
58
resources/app/client-esm/dice/terms/operator.mjs
Normal file
@@ -0,0 +1,58 @@
|
||||
import RollTerm from "./term.mjs";
|
||||
|
||||
/**
|
||||
* A type of RollTerm used to denote and perform an arithmetic operation.
|
||||
* @extends {RollTerm}
|
||||
*/
|
||||
export default class OperatorTerm extends RollTerm {
|
||||
constructor({operator, options}={}) {
|
||||
super({options});
|
||||
this.operator = operator;
|
||||
}
|
||||
|
||||
/**
|
||||
* The term's operator value.
|
||||
* @type {string}
|
||||
*/
|
||||
operator;
|
||||
|
||||
/**
|
||||
* An object of operators with their precedence values.
|
||||
* @type {Readonly<Record<string, number>>}
|
||||
*/
|
||||
static PRECEDENCE = Object.freeze({
|
||||
"+": 10,
|
||||
"-": 10,
|
||||
"*": 20,
|
||||
"/": 20,
|
||||
"%": 20
|
||||
});
|
||||
|
||||
/**
|
||||
* An array of operators which represent arithmetic operations
|
||||
* @type {string[]}
|
||||
*/
|
||||
static OPERATORS = Object.keys(this.PRECEDENCE);
|
||||
|
||||
/** @inheritdoc */
|
||||
static REGEXP = new RegExp(this.OPERATORS.map(o => "\\"+o).join("|"), "g");
|
||||
|
||||
/** @inheritdoc */
|
||||
static SERIALIZE_ATTRIBUTES = ["operator"];
|
||||
|
||||
/** @inheritdoc */
|
||||
get flavor() {
|
||||
return ""; // Operator terms cannot have flavor text
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
get expression() {
|
||||
return ` ${this.operator} `;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
get total() {
|
||||
return ` ${this.operator} `;
|
||||
}
|
||||
}
|
||||
|
||||
149
resources/app/client-esm/dice/terms/parenthetical.mjs
Normal file
149
resources/app/client-esm/dice/terms/parenthetical.mjs
Normal file
@@ -0,0 +1,149 @@
|
||||
import RollTerm from "./term.mjs";
|
||||
|
||||
/**
|
||||
* A type of RollTerm used to enclose a parenthetical expression to be recursively evaluated.
|
||||
* @extends {RollTerm}
|
||||
*/
|
||||
export default class ParentheticalTerm extends RollTerm {
|
||||
constructor({term, roll, options}) {
|
||||
super({options});
|
||||
this.term = term;
|
||||
this.roll = roll;
|
||||
|
||||
// If a roll was explicitly passed in, the parenthetical may have already been evaluated
|
||||
if ( this.roll ) {
|
||||
this.term = roll.formula;
|
||||
this._evaluated = this.roll._evaluated;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The original provided string term used to construct the parenthetical
|
||||
* @type {string}
|
||||
*/
|
||||
term;
|
||||
|
||||
/**
|
||||
* An already-evaluated Roll instance used instead of the string term.
|
||||
* @type {Roll}
|
||||
*/
|
||||
roll;
|
||||
|
||||
/** @inheritdoc */
|
||||
isIntermediate = true;
|
||||
|
||||
/**
|
||||
* The regular expression pattern used to identify the opening of a parenthetical expression.
|
||||
* This could also identify the opening of a math function.
|
||||
* @type {RegExp}
|
||||
*/
|
||||
static OPEN_REGEXP = /([A-z][A-z0-9]+)?\(/g;
|
||||
|
||||
/**
|
||||
* A regular expression pattern used to identify the closing of a parenthetical expression.
|
||||
* @type {RegExp}
|
||||
*/
|
||||
static CLOSE_REGEXP = new RegExp("\\)(?:\\$\\$F[0-9]+\\$\\$)?", "g");
|
||||
|
||||
/** @inheritdoc */
|
||||
static SERIALIZE_ATTRIBUTES = ["term", "roll"];
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Parenthetical Term Attributes */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* An array of evaluated DiceTerm instances that should be bubbled up to the parent Roll
|
||||
* @type {DiceTerm[]}
|
||||
*/
|
||||
get dice() {
|
||||
return this.roll?.dice;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
get total() {
|
||||
return this.roll.total;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
get expression() {
|
||||
return `(${this.term})`;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
get isDeterministic() {
|
||||
return Roll.create(this.term).isDeterministic;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Parenthetical Term Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
_evaluate(options={}) {
|
||||
const roll = this.roll || Roll.create(this.term);
|
||||
if ( this._root ) roll._root = this._root;
|
||||
if ( options.maximize || options.minimize || roll.isDeterministic ) return this._evaluateSync(roll, options);
|
||||
return this._evaluateAsync(roll, options);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Evaluate this parenthetical when it contains any non-deterministic sub-terms.
|
||||
* @param {Roll} roll The inner Roll instance to evaluate.
|
||||
* @param {object} [options]
|
||||
* @returns {Promise<RollTerm>}
|
||||
* @protected
|
||||
*/
|
||||
async _evaluateAsync(roll, options={}) {
|
||||
this.roll = await roll.evaluate(options);
|
||||
this.roll.propagateFlavor(this.flavor);
|
||||
return this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Evaluate this parenthetical when it contains only deterministic sub-terms.
|
||||
* @param {Roll} roll The inner Roll instance to evaluate.
|
||||
* @param {object} [options]
|
||||
* @returns {RollTerm}
|
||||
* @protected
|
||||
*/
|
||||
_evaluateSync(roll, options={}) {
|
||||
this.roll = roll.evaluateSync(options);
|
||||
this.roll.propagateFlavor(this.flavor);
|
||||
return this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Construct a ParentheticalTerm from an Array of component terms which should be wrapped inside the parentheses.
|
||||
* @param {RollTerm[]} terms The array of terms to use as internal parts of the parenthetical
|
||||
* @param {object} [options={}] Additional options passed to the ParentheticalTerm constructor
|
||||
* @returns {ParentheticalTerm} The constructed ParentheticalTerm instance
|
||||
*
|
||||
* @example Create a Parenthetical Term from an array of component RollTerm instances
|
||||
* ```js
|
||||
* const d6 = new Die({number: 4, faces: 6});
|
||||
* const plus = new OperatorTerm({operator: "+"});
|
||||
* const bonus = new NumericTerm({number: 4});
|
||||
* t = ParentheticalTerm.fromTerms([d6, plus, bonus]);
|
||||
* t.formula; // (4d6 + 4)
|
||||
* ```
|
||||
*/
|
||||
static fromTerms(terms, options) {
|
||||
const roll = Roll.defaultImplementation.fromTerms(terms);
|
||||
return new this({roll, options});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static fromParseNode(node) {
|
||||
const roll = Roll.defaultImplementation.fromTerms(Roll.defaultImplementation.instantiateAST(node.term));
|
||||
return this.fromData({ ...node, roll, term: roll.formula });
|
||||
}
|
||||
}
|
||||
361
resources/app/client-esm/dice/terms/pool.mjs
Normal file
361
resources/app/client-esm/dice/terms/pool.mjs
Normal file
@@ -0,0 +1,361 @@
|
||||
import RollTerm from "./term.mjs";
|
||||
import DiceTerm from "./dice.mjs";
|
||||
import Die from "./die.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("../_types.mjs").DiceTermResult} DiceTermResult
|
||||
*/
|
||||
|
||||
/**
|
||||
* A type of RollTerm which encloses a pool of multiple inner Rolls which are evaluated jointly.
|
||||
*
|
||||
* A dice pool represents a set of Roll expressions which are collectively modified to compute an effective total
|
||||
* across all Rolls in the pool. The final total for the pool is defined as the sum over kept rolls, relative to any
|
||||
* success count or margin.
|
||||
*
|
||||
* @example Keep the highest of the 3 roll expressions
|
||||
* ```js
|
||||
* let pool = new PoolTerm({
|
||||
* terms: ["4d6", "3d8 - 1", "2d10 + 3"],
|
||||
* modifiers: ["kh"]
|
||||
* });
|
||||
* pool.evaluate();
|
||||
* ```
|
||||
*/
|
||||
export default class PoolTerm extends RollTerm {
|
||||
constructor({terms=[], modifiers=[], rolls=[], results=[], options={}}={}) {
|
||||
super({options});
|
||||
this.terms = terms;
|
||||
this.modifiers = modifiers;
|
||||
this.rolls = (rolls.length === terms.length) ? rolls : this.terms.map(t => Roll.create(t));
|
||||
this.results = results;
|
||||
|
||||
// If rolls and results were explicitly passed, the term has already been evaluated
|
||||
if ( rolls.length && results.length ) this._evaluated = true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The original provided terms to the Dice Pool
|
||||
* @type {string[]}
|
||||
*/
|
||||
terms;
|
||||
|
||||
/**
|
||||
* The string modifiers applied to resolve the pool
|
||||
* @type {string[]}
|
||||
*/
|
||||
modifiers;
|
||||
|
||||
/**
|
||||
* Each component term of the dice pool as a Roll instance.
|
||||
* @type {Roll[]}
|
||||
*/
|
||||
rolls;
|
||||
|
||||
/**
|
||||
* The array of dice pool results which have been rolled
|
||||
* @type {DiceTermResult[]}
|
||||
*/
|
||||
results;
|
||||
|
||||
/**
|
||||
* Define the modifiers that can be used for this particular DiceTerm type.
|
||||
* @type {Record<string, function|string>}
|
||||
*/
|
||||
static MODIFIERS = {
|
||||
"k": "keep",
|
||||
"kh": "keep",
|
||||
"kl": "keep",
|
||||
"d": "drop",
|
||||
"dh": "drop",
|
||||
"dl": "drop",
|
||||
"cs": "countSuccess",
|
||||
"cf": "countFailures"
|
||||
};
|
||||
|
||||
/**
|
||||
* The regular expression pattern used to identify the opening of a dice pool expression.
|
||||
* @type {RegExp}
|
||||
*/
|
||||
static OPEN_REGEXP = /{/g;
|
||||
|
||||
/**
|
||||
* A regular expression pattern used to identify the closing of a dice pool expression.
|
||||
* @type {RegExp}
|
||||
*/
|
||||
static CLOSE_REGEXP = new RegExp(`}${DiceTerm.MODIFIERS_REGEXP_STRING}?(?:\\$\\$F[0-9]+\\$\\$)?`, "g");
|
||||
|
||||
/**
|
||||
* A regular expression pattern used to match the entirety of a DicePool expression.
|
||||
* @type {RegExp}
|
||||
*/
|
||||
static REGEXP = new RegExp(`{([^}]+)}${DiceTerm.MODIFIERS_REGEXP_STRING}?(?:\\$\\$F[0-9]+\\$\\$)?`);
|
||||
|
||||
/** @inheritdoc */
|
||||
static SERIALIZE_ATTRIBUTES = ["terms", "modifiers", "rolls", "results"];
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Dice Pool Attributes */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Return an Array of each individual DiceTerm instances contained within the PoolTerm.
|
||||
* @type {DiceTerm[]}
|
||||
*/
|
||||
get dice() {
|
||||
return this.rolls.flatMap(r => r.dice);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
get expression() {
|
||||
return `{${this.terms.join(",")}}${this.modifiers.join("")}`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
get total() {
|
||||
if ( !this._evaluated ) return undefined;
|
||||
return this.results.reduce((t, r) => {
|
||||
if ( !r.active ) return t;
|
||||
if ( r.count !== undefined ) return t + r.count;
|
||||
else return t + r.result;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Return an array of rolled values which are still active within the PoolTerm
|
||||
* @type {number[]}
|
||||
*/
|
||||
get values() {
|
||||
return this.results.reduce((arr, r) => {
|
||||
if ( !r.active ) return arr;
|
||||
arr.push(r.result);
|
||||
return arr;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
get isDeterministic() {
|
||||
return this.terms.every(t => Roll.create(t).isDeterministic);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Alter the DiceTerm by adding or multiplying the number of dice which are rolled
|
||||
* @param {any[]} args Arguments passed to each contained Roll#alter method.
|
||||
* @returns {PoolTerm} The altered pool
|
||||
*/
|
||||
alter(...args) {
|
||||
this.rolls.forEach(r => r.alter(...args));
|
||||
return this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
_evaluate(options={}) {
|
||||
if ( RollTerm.isDeterministic(this, options) ) return this._evaluateSync(options);
|
||||
return this._evaluateAsync(options);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Evaluate this pool term when it contains any non-deterministic sub-terms.
|
||||
* @param {object} [options]
|
||||
* @returns {Promise<PoolTerm>}
|
||||
* @protected
|
||||
*/
|
||||
async _evaluateAsync(options={}) {
|
||||
for ( const roll of this.rolls ) {
|
||||
if ( this._root ) roll._root = this._root;
|
||||
await roll.evaluate(options);
|
||||
roll.propagateFlavor(this.flavor);
|
||||
this.results.push({ result: roll.total, active: true });
|
||||
}
|
||||
await this._evaluateModifiers();
|
||||
return this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Evaluate this pool term when it contains only deterministic sub-terms.
|
||||
* @param {object} [options]
|
||||
* @returns {PoolTerm}
|
||||
* @protected
|
||||
*/
|
||||
_evaluateSync(options={}) {
|
||||
for ( const roll of this.rolls ) {
|
||||
if ( this._root ) roll._root = this._root;
|
||||
roll.evaluateSync(options);
|
||||
roll.propagateFlavor(this.flavor);
|
||||
this.results.push({ result: roll.total, active: true });
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Use the same logic as for the DiceTerm to avoid duplication
|
||||
* @see DiceTerm#_evaluateModifiers
|
||||
*/
|
||||
_evaluateModifiers() {
|
||||
return DiceTerm.prototype._evaluateModifiers.call(this);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Use the same logic as for the DiceTerm to avoid duplication
|
||||
* @see DiceTerm#_evaluateModifier
|
||||
*/
|
||||
_evaluateModifier(command, modifier) {
|
||||
return DiceTerm.prototype._evaluateModifier.call(this, command, modifier);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Saving and Loading */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static _fromData(data) {
|
||||
data.rolls = (data.rolls || []).map(r => r instanceof Roll ? r : Roll.fromData(r));
|
||||
return super._fromData(data);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
toJSON() {
|
||||
const data = super.toJSON();
|
||||
data.rolls = data.rolls.map(r => r.toJSON());
|
||||
return data;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Given a string formula, create and return an evaluated PoolTerm object
|
||||
* @param {string} formula The string formula to parse
|
||||
* @param {object} [options] Additional options applied to the PoolTerm
|
||||
* @returns {PoolTerm|null} The evaluated PoolTerm object or null if the formula is invalid
|
||||
*/
|
||||
static fromExpression(formula, options={}) {
|
||||
const rgx = formula.trim().match(this.REGEXP);
|
||||
if ( !rgx ) return null;
|
||||
let [terms, modifiers] = rgx.slice(1);
|
||||
terms = terms.split(",");
|
||||
modifiers = Array.from((modifiers || "").matchAll(DiceTerm.MODIFIER_REGEXP)).map(m => m[0]);
|
||||
return new this({terms, modifiers, options});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create a PoolTerm by providing an array of existing Roll objects
|
||||
* @param {Roll[]} rolls An array of Roll objects from which to create the pool
|
||||
* @returns {RollTerm} The constructed PoolTerm comprised of the provided rolls
|
||||
*/
|
||||
static fromRolls(rolls=[]) {
|
||||
const allEvaluated = rolls.every(t => t._evaluated);
|
||||
const noneEvaluated = !rolls.some(t => t._evaluated);
|
||||
if ( !(allEvaluated || noneEvaluated) ) {
|
||||
throw new Error("You can only call PoolTerm.fromRolls with an array of Roll instances which are either all evaluated, or none evaluated");
|
||||
}
|
||||
const pool = new this({
|
||||
terms: rolls.map(r => r.formula),
|
||||
modifiers: [],
|
||||
rolls: rolls,
|
||||
results: allEvaluated ? rolls.map(r => ({result: r.total, active: true})) : []
|
||||
});
|
||||
pool._evaluated = allEvaluated;
|
||||
return pool;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static fromParseNode(node) {
|
||||
const rolls = node.terms.map(t => {
|
||||
return Roll.defaultImplementation.fromTerms(Roll.defaultImplementation.instantiateAST(t)).toJSON();
|
||||
});
|
||||
const modifiers = Array.from((node.modifiers || "").matchAll(DiceTerm.MODIFIER_REGEXP)).map(([m]) => m);
|
||||
return this.fromData({ ...node, rolls, modifiers, terms: rolls.map(r => r.formula) });
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Modifiers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Keep a certain number of highest or lowest dice rolls from the result set.
|
||||
*
|
||||
* {1d6,1d8,1d10,1d12}kh2 Keep the 2 best rolls from the pool
|
||||
* {1d12,6}kl Keep the lowest result in the pool
|
||||
*
|
||||
* @param {string} modifier The matched modifier query
|
||||
*/
|
||||
keep(modifier) {
|
||||
return Die.prototype.keep.call(this, modifier);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Keep a certain number of highest or lowest dice rolls from the result set.
|
||||
*
|
||||
* {1d6,1d8,1d10,1d12}dl3 Drop the 3 worst results in the pool
|
||||
* {1d12,6}dh Drop the highest result in the pool
|
||||
*
|
||||
* @param {string} modifier The matched modifier query
|
||||
*/
|
||||
drop(modifier) {
|
||||
return Die.prototype.drop.call(this, modifier);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Count the number of successful results which occurred in the pool.
|
||||
* Successes are counted relative to some target, or relative to the maximum possible value if no target is given.
|
||||
* Applying a count-success modifier to the results re-casts all results to 1 (success) or 0 (failure)
|
||||
*
|
||||
* 20d20cs Count the number of dice which rolled a 20
|
||||
* 20d20cs>10 Count the number of dice which rolled higher than 10
|
||||
* 20d20cs<10 Count the number of dice which rolled less than 10
|
||||
*
|
||||
* @param {string} modifier The matched modifier query
|
||||
*/
|
||||
countSuccess(modifier) {
|
||||
return Die.prototype.countSuccess.call(this, modifier);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Count the number of failed results which occurred in a given result set.
|
||||
* Failures are counted relative to some target, or relative to the lowest possible value if no target is given.
|
||||
* Applying a count-failures modifier to the results re-casts all results to 1 (failure) or 0 (non-failure)
|
||||
*
|
||||
* 6d6cf Count the number of dice which rolled a 1 as failures
|
||||
* 6d6cf<=3 Count the number of dice which rolled less than 3 as failures
|
||||
* 6d6cf>4 Count the number of dice which rolled greater than 4 as failures
|
||||
*
|
||||
* @param {string} modifier The matched modifier query
|
||||
*/
|
||||
countFailures(modifier) {
|
||||
return Die.prototype.countFailures.call(this, modifier);
|
||||
}
|
||||
}
|
||||
45
resources/app/client-esm/dice/terms/string.mjs
Normal file
45
resources/app/client-esm/dice/terms/string.mjs
Normal file
@@ -0,0 +1,45 @@
|
||||
import RollTerm from "./term.mjs";
|
||||
|
||||
/**
|
||||
* A type of RollTerm used to represent strings which have not yet been matched.
|
||||
* @extends {RollTerm}
|
||||
*/
|
||||
export default class StringTerm extends RollTerm {
|
||||
constructor({term, options}={}) {
|
||||
super({options});
|
||||
this.term = term;
|
||||
}
|
||||
|
||||
/**
|
||||
* The term's string value.
|
||||
* @type {string}
|
||||
*/
|
||||
term;
|
||||
|
||||
/** @inheritdoc */
|
||||
static SERIALIZE_ATTRIBUTES = ["term"];
|
||||
|
||||
/** @inheritdoc */
|
||||
get expression() {
|
||||
return this.term;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
get total() {
|
||||
return this.term;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
get isDeterministic() {
|
||||
const classified = Roll.defaultImplementation._classifyStringTerm(this.term, {intermediate: false});
|
||||
if ( classified instanceof StringTerm ) return true;
|
||||
return classified.isDeterministic;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
evaluate({ allowStrings=false }={}) {
|
||||
if ( !allowStrings ) throw new Error(`Unresolved StringTerm ${this.term} requested for evaluation`);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
246
resources/app/client-esm/dice/terms/term.mjs
Normal file
246
resources/app/client-esm/dice/terms/term.mjs
Normal file
@@ -0,0 +1,246 @@
|
||||
import { deepClone } from "../../../common/utils/helpers.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("../_types.mjs").RollParseNode} RollParseNode
|
||||
*/
|
||||
|
||||
/**
|
||||
* An abstract class which represents a single token that can be used as part of a Roll formula.
|
||||
* Every portion of a Roll formula is parsed into a subclass of RollTerm in order for the Roll to be fully evaluated.
|
||||
*/
|
||||
export default class RollTerm {
|
||||
/**
|
||||
* @param {object} [options] An object of additional options which describes and modifies the term.
|
||||
*/
|
||||
constructor({options={}}={}) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object of additional options which describes and modifies the term.
|
||||
* @type {object}
|
||||
*/
|
||||
options;
|
||||
|
||||
/**
|
||||
* An internal flag for whether the term has been evaluated
|
||||
* @type {boolean}
|
||||
* @internal
|
||||
*/
|
||||
_evaluated = false;
|
||||
|
||||
/**
|
||||
* A reference to the Roll at the root of the evaluation tree.
|
||||
* @type {Roll}
|
||||
* @internal
|
||||
*/
|
||||
_root;
|
||||
|
||||
/**
|
||||
* Is this term intermediate, and should be evaluated first as part of the simplification process?
|
||||
* @type {boolean}
|
||||
*/
|
||||
isIntermediate = false;
|
||||
|
||||
/**
|
||||
* A regular expression pattern which identifies optional term-level flavor text
|
||||
* @type {string}
|
||||
*/
|
||||
static FLAVOR_REGEXP_STRING = "(?:\\[([^\\]]+)\\])";
|
||||
|
||||
/**
|
||||
* A regular expression which identifies term-level flavor text
|
||||
* @type {RegExp}
|
||||
*/
|
||||
static FLAVOR_REGEXP = new RegExp(RollTerm.FLAVOR_REGEXP_STRING, "g");
|
||||
|
||||
/**
|
||||
* A regular expression used to match a term of this type
|
||||
* @type {RegExp}
|
||||
*/
|
||||
static REGEXP = undefined;
|
||||
|
||||
/**
|
||||
* An array of additional attributes which should be retained when the term is serialized
|
||||
* @type {string[]}
|
||||
*/
|
||||
static SERIALIZE_ATTRIBUTES = [];
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* RollTerm Attributes */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A string representation of the formula expression for this RollTerm, prior to evaluation.
|
||||
* @type {string}
|
||||
*/
|
||||
get expression() {
|
||||
throw new Error(`The ${this.constructor.name} class must implement the expression attribute`);
|
||||
}
|
||||
|
||||
/**
|
||||
* A string representation of the formula, including optional flavor text.
|
||||
* @type {string}
|
||||
*/
|
||||
get formula() {
|
||||
let f = this.expression;
|
||||
if ( this.flavor ) f += `[${this.flavor}]`;
|
||||
return f;
|
||||
}
|
||||
|
||||
/**
|
||||
* A string or numeric representation of the final output for this term, after evaluation.
|
||||
* @type {number|string}
|
||||
*/
|
||||
get total() {
|
||||
throw new Error(`The ${this.constructor.name} class must implement the total attribute`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional flavor text which modifies and describes this term.
|
||||
* @type {string}
|
||||
*/
|
||||
get flavor() {
|
||||
return this.options.flavor || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this term is entirely deterministic or contains some randomness.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isDeterministic() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* A reference to the RollResolver app being used to externally resolve this term.
|
||||
* @type {RollResolver}
|
||||
*/
|
||||
get resolver() {
|
||||
return this._root?._resolver;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* RollTerm Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Evaluate the term, processing its inputs and finalizing its total.
|
||||
* @param {object} [options={}] Options which modify how the RollTerm is evaluated
|
||||
* @param {boolean} [options.minimize=false] Minimize the result, obtaining the smallest possible value.
|
||||
* @param {boolean} [options.maximize=false] Maximize the result, obtaining the largest possible value.
|
||||
* @param {boolean} [options.allowStrings=false] If true, string terms will not throw an error when evaluated.
|
||||
* @returns {Promise<RollTerm>|RollTerm} Returns a Promise if the term is non-deterministic.
|
||||
*/
|
||||
evaluate(options={}) {
|
||||
if ( this._evaluated ) {
|
||||
throw new Error(`The ${this.constructor.name} has already been evaluated and is now immutable`);
|
||||
}
|
||||
this._evaluated = true;
|
||||
return this._evaluate(options);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Evaluate the term.
|
||||
* @param {object} [options={}] Options which modify how the RollTerm is evaluated, see RollTerm#evaluate
|
||||
* @returns {Promise<RollTerm>|RollTerm} Returns a Promise if the term is non-deterministic.
|
||||
* @protected
|
||||
*/
|
||||
_evaluate(options={}) {
|
||||
return this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Determine if evaluating a given RollTerm with certain evaluation options can be done so deterministically.
|
||||
* @param {RollTerm} term The term.
|
||||
* @param {object} [options] Options for evaluating the term.
|
||||
* @param {boolean} [options.maximize] Force the result to be maximized.
|
||||
* @param {boolean} [options.minimize] Force the result to be minimized.
|
||||
*/
|
||||
static isDeterministic(term, { maximize, minimize }={}) {
|
||||
return maximize || minimize || term.isDeterministic;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Serialization and Loading */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Construct a RollTerm from a provided data object
|
||||
* @param {object} data Provided data from an un-serialized term
|
||||
* @returns {RollTerm} The constructed RollTerm
|
||||
*/
|
||||
static fromData(data) {
|
||||
let cls = CONFIG.Dice.termTypes[data.class];
|
||||
if ( !cls ) {
|
||||
cls = Object.values(CONFIG.Dice.terms).find(c => c.name === data.class) || foundry.dice.terms.Die;
|
||||
}
|
||||
return cls._fromData(data);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Construct a RollTerm from parser information.
|
||||
* @param {RollParseNode} node The node.
|
||||
* @returns {RollTerm}
|
||||
*/
|
||||
static fromParseNode(node) {
|
||||
return this.fromData(deepClone(node));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Define term-specific logic for how a de-serialized data object is restored as a functional RollTerm
|
||||
* @param {object} data The de-serialized term data
|
||||
* @returns {RollTerm} The re-constructed RollTerm object
|
||||
* @protected
|
||||
*/
|
||||
static _fromData(data) {
|
||||
if ( data.roll && !(data.roll instanceof Roll) ) data.roll = Roll.fromData(data.roll);
|
||||
const term = new this(data);
|
||||
term._evaluated = data.evaluated ?? true;
|
||||
return term;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Reconstruct a RollTerm instance from a provided JSON string
|
||||
* @param {string} json A serialized JSON representation of a DiceTerm
|
||||
* @return {RollTerm} A reconstructed RollTerm from the provided JSON
|
||||
*/
|
||||
static fromJSON(json) {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(json);
|
||||
} catch(err) {
|
||||
throw new Error("You must pass a valid JSON string");
|
||||
}
|
||||
return this.fromData(data);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Serialize the RollTerm to a JSON string which allows it to be saved in the database or embedded in text.
|
||||
* This method should return an object suitable for passing to the JSON.stringify function.
|
||||
* @return {object}
|
||||
*/
|
||||
toJSON() {
|
||||
const data = {
|
||||
class: this.constructor.name,
|
||||
options: this.options,
|
||||
evaluated: this._evaluated
|
||||
};
|
||||
for ( let attr of this.constructor.SERIALIZE_ATTRIBUTES ) {
|
||||
data[attr] = this[attr];
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
257
resources/app/client-esm/dice/twister.mjs
Normal file
257
resources/app/client-esm/dice/twister.mjs
Normal file
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* A standalone, pure JavaScript implementation of the Mersenne Twister pseudo random number generator.
|
||||
*
|
||||
* @author Raphael Pigulla <pigulla@four66.com>
|
||||
* @version 0.2.3
|
||||
* @license
|
||||
* Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura,
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. The names of its contributors may not be used to endorse or promote
|
||||
* products derived from this software without specific prior written
|
||||
* permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
export default class MersenneTwister {
|
||||
/**
|
||||
* Instantiates a new Mersenne Twister.
|
||||
* @param {number} [seed] The initial seed value, if not provided the current timestamp will be used.
|
||||
* @constructor
|
||||
*/
|
||||
constructor(seed) {
|
||||
|
||||
// Initial values
|
||||
this.MAX_INT = 4294967296.0;
|
||||
this.N = 624;
|
||||
this.M = 397;
|
||||
this.UPPER_MASK = 0x80000000;
|
||||
this.LOWER_MASK = 0x7fffffff;
|
||||
this.MATRIX_A = 0x9908b0df;
|
||||
|
||||
// Initialize sequences
|
||||
this.mt = new Array(this.N);
|
||||
this.mti = this.N + 1;
|
||||
this.SEED = this.seed(seed ?? new Date().getTime());
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the state vector by using one unsigned 32-bit integer "seed", which may be zero.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @param {number} seed The seed value.
|
||||
*/
|
||||
seed(seed) {
|
||||
this.SEED = seed;
|
||||
let s;
|
||||
this.mt[0] = seed >>> 0;
|
||||
|
||||
for (this.mti = 1; this.mti < this.N; this.mti++) {
|
||||
s = this.mt[this.mti - 1] ^ (this.mt[this.mti - 1] >>> 30);
|
||||
this.mt[this.mti] =
|
||||
(((((s & 0xffff0000) >>> 16) * 1812433253) << 16) + (s & 0x0000ffff) * 1812433253) + this.mti;
|
||||
this.mt[this.mti] >>>= 0;
|
||||
}
|
||||
return seed;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the state vector by using an array key[] of unsigned 32-bit integers of the specified length. If
|
||||
* length is smaller than 624, then each array of 32-bit integers gives distinct initial state vector. This is
|
||||
* useful if you want a larger seed space than 32-bit word.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @param {array} vector The seed vector.
|
||||
*/
|
||||
seedArray(vector) {
|
||||
let i = 1, j = 0, k = this.N > vector.length ? this.N : vector.length, s;
|
||||
this.seed(19650218);
|
||||
for (; k > 0; k--) {
|
||||
s = this.mt[i - 1] ^ (this.mt[i - 1] >>> 30);
|
||||
|
||||
this.mt[i] = (this.mt[i] ^ (((((s & 0xffff0000) >>> 16) * 1664525) << 16) + ((s & 0x0000ffff) * 1664525))) +
|
||||
vector[j] + j;
|
||||
this.mt[i] >>>= 0;
|
||||
i++;
|
||||
j++;
|
||||
if (i >= this.N) {
|
||||
this.mt[0] = this.mt[this.N-1];
|
||||
i = 1;
|
||||
}
|
||||
if (j >= vector.length) {
|
||||
j = 0;
|
||||
}
|
||||
}
|
||||
|
||||
for (k = this.N-1; k; k--) {
|
||||
s = this.mt[i - 1] ^ (this.mt[i - 1] >>> 30);
|
||||
this.mt[i] =
|
||||
(this.mt[i] ^ (((((s & 0xffff0000) >>> 16) * 1566083941) << 16) + (s & 0x0000ffff) * 1566083941)) - i;
|
||||
this.mt[i] >>>= 0;
|
||||
i++;
|
||||
if (i >= this.N) {
|
||||
this.mt[0] = this.mt[this.N - 1];
|
||||
i = 1;
|
||||
}
|
||||
}
|
||||
this.mt[0] = 0x80000000;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a random unsigned 32-bit integer.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @returns {number}
|
||||
*/
|
||||
int() {
|
||||
let y, kk, mag01 = [0, this.MATRIX_A];
|
||||
|
||||
if (this.mti >= this.N) {
|
||||
if (this.mti === this.N+1) {
|
||||
this.seed(5489);
|
||||
}
|
||||
|
||||
for (kk = 0; kk < this.N - this.M; kk++) {
|
||||
y = (this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK);
|
||||
this.mt[kk] = this.mt[kk + this.M] ^ (y >>> 1) ^ mag01[y & 1];
|
||||
}
|
||||
|
||||
for (; kk < this.N - 1; kk++) {
|
||||
y = (this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK);
|
||||
this.mt[kk] = this.mt[kk + (this.M - this.N)] ^ (y >>> 1) ^ mag01[y & 1];
|
||||
}
|
||||
|
||||
y = (this.mt[this.N - 1] & this.UPPER_MASK) | (this.mt[0] & this.LOWER_MASK);
|
||||
this.mt[this.N - 1] = this.mt[this.M - 1] ^ (y >>> 1) ^ mag01[y & 1];
|
||||
this.mti = 0;
|
||||
}
|
||||
|
||||
y = this.mt[this.mti++];
|
||||
|
||||
y ^= (y >>> 11);
|
||||
y ^= (y << 7) & 0x9d2c5680;
|
||||
y ^= (y << 15) & 0xefc60000;
|
||||
y ^= (y >>> 18);
|
||||
|
||||
return y >>> 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a random unsigned 31-bit integer.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @returns {number}
|
||||
*/
|
||||
int31() {
|
||||
return this.int() >>> 1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a random real in the interval [0;1] with 32-bit resolution.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @returns {number}
|
||||
*/
|
||||
real() {
|
||||
return this.int() * (1.0 / (this.MAX_INT - 1));
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a random real in the interval ]0;1[ with 32-bit resolution.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @returns {number}
|
||||
*/
|
||||
realx() {
|
||||
return (this.int() + 0.5) * (1.0 / this.MAX_INT);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a random real in the interval [0;1[ with 32-bit resolution.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @returns {number}
|
||||
*/
|
||||
rnd() {
|
||||
return this.int() * (1.0 / this.MAX_INT);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a random real in the interval [0;1[ with 32-bit resolution.
|
||||
*
|
||||
* Same as .rnd() method - for consistency with Math.random() interface.
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @returns {number}
|
||||
*/
|
||||
random() {
|
||||
return this.rnd();
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a random real in the interval [0;1[ with 53-bit resolution.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @returns {number}
|
||||
*/
|
||||
rndHiRes() {
|
||||
const a = this.int() >>> 5;
|
||||
const b = this.int() >>> 6;
|
||||
return (a * 67108864.0 + b) * (1.0 / 9007199254740992.0);
|
||||
};
|
||||
|
||||
/**
|
||||
* A pseudo-normal distribution using the Box-Muller transform.
|
||||
* @param {number} mu The normal distribution mean
|
||||
* @param {number} sigma The normal distribution standard deviation
|
||||
* @returns {number}
|
||||
*/
|
||||
normal(mu, sigma) {
|
||||
let u = 0;
|
||||
while (u === 0) u = this.random(); // Converting [0,1) to (0,1)
|
||||
let v = 0;
|
||||
while (v === 0) v = this.random(); // Converting [0,1) to (0,1)
|
||||
let n = Math.sqrt( -2.0 * Math.log(u) ) * Math.cos(2.0 * Math.PI * v);
|
||||
return (n * sigma) + mu;
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory method for generating random uniform rolls
|
||||
* @returns {number}
|
||||
*/
|
||||
static random() {
|
||||
return twist.random();
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory method for generating random normal rolls
|
||||
* @return {number}
|
||||
*/
|
||||
static normal(...args) {
|
||||
return twist.normal(...args);
|
||||
}
|
||||
}
|
||||
|
||||
// Global singleton
|
||||
const twist = new MersenneTwister(Date.now());
|
||||
Reference in New Issue
Block a user