From b9588c83b16bc5ee4a1ab98605e6e399b256ff6c Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Sat, 1 Apr 2023 11:27:52 +0200 Subject: [PATCH] Added dice functions to parse engine --- app/imports/parser/functions.js | 203 ++++++++++++++++++---- app/imports/parser/parseTree/call.js | 64 ++++--- app/imports/parser/parseTree/rollArray.js | 20 ++- 3 files changed, 223 insertions(+), 64 deletions(-) diff --git a/app/imports/parser/functions.js b/app/imports/parser/functions.js index 90a7e8df..9482fd9b 100644 --- a/app/imports/parser/functions.js +++ b/app/imports/parser/functions.js @@ -1,11 +1,12 @@ import resolve from '/imports/parser/resolve.js' +import rollDice from '/imports/parser/rollDice.js'; export default { 'abs': { comment: 'Returns the absolute value of a number', examples: [ - {input: 'abs(9)', result: '9'}, - {input: 'abs(-3)', result: '3'}, + { input: 'abs(9)', result: '9' }, + { input: 'abs(-3)', result: '3' }, ], arguments: ['number'], resultType: 'number', @@ -14,8 +15,8 @@ export default { 'sqrt': { comment: 'Returns the square root of a number', examples: [ - {input: 'sqrt(16)', result: '4'}, - {input: 'sqrt(10)', result: '3.1622776601683795'}, + { input: 'sqrt(16)', result: '4' }, + { input: 'sqrt(10)', result: '3.1622776601683795' }, ], arguments: ['number'], resultType: 'number', @@ -23,14 +24,14 @@ export default { }, 'max': { comment: 'Returns the largest of the given numbers', - examples: [{input: 'max(12, 6, 3, 168)', result: '168'}], + examples: [{ input: 'max(12, 6, 3, 168)', result: '168' }], arguments: anyNumberOf('number'), resultType: 'number', fn: Math.max, }, 'min': { comment: 'Returns the smallest of the given numbers', - examples: [{input: 'min(12, 6, 3, 168)', result: '3'}], + examples: [{ input: 'min(12, 6, 3, 168)', result: '3' }], arguments: anyNumberOf('number'), resultType: 'number', fn: Math.min, @@ -38,9 +39,9 @@ export default { 'round': { comment: 'Returns the value of a number rounded to the nearest integer', examples: [ - {input: 'round(5.95)', result: '6'}, - {input: 'round(5.5)', result: '6'}, - {input: 'round(5.05)', result: '5'}, + { input: 'round(5.95)', result: '6' }, + { input: 'round(5.5)', result: '6' }, + { input: 'round(5.05)', result: '5' }, ], arguments: ['number'], resultType: 'number', @@ -49,10 +50,10 @@ export default { 'floor': { comment: 'Rounds a number down to the next smallest integer', examples: [ - {input: 'floor(5.95)', result: '5'}, - {input: 'floor(5.05)', result: '5'}, - {input: 'floor(5)', result: '5'}, - {input: 'floor(-5.5)', result: '-6'}, + { input: 'floor(5.95)', result: '5' }, + { input: 'floor(5.05)', result: '5' }, + { input: 'floor(5)', result: '5' }, + { input: 'floor(-5.5)', result: '-6' }, ], arguments: ['number'], resultType: 'number', @@ -61,10 +62,10 @@ export default { 'ceil': { comment: 'Rounds a number up to the next largest integer', examples: [ - {input: 'ceil(5.95)', result: '6'}, - {input: 'ceil(5.05)', result: '6'}, - {input: 'ceil(5)', result: '5'}, - {input: 'ceil(-5.5)', result: '-5'}, + { input: 'ceil(5.95)', result: '6' }, + { input: 'ceil(5.05)', result: '6' }, + { input: 'ceil(5)', result: '5' }, + { input: 'ceil(-5.5)', result: '-5' }, ], arguments: ['number'], resultType: 'number', @@ -73,21 +74,21 @@ export default { 'trunc': { comment: 'Returns the integer part of a number by removing any fractional digits', examples: [ - {input: 'trunc(5.95)', result: '5'}, - {input: 'trunc(5.05)', result: '5'}, - {input: 'trunc(5)', result: '5'}, - {input: 'trunc(-5.5)', result: '-5'}, + { input: 'trunc(5.95)', result: '5' }, + { input: 'trunc(5.05)', result: '5' }, + { input: 'trunc(5)', result: '5' }, + { input: 'trunc(-5.5)', result: '-5' }, ], - arguments:[ 'number'], + arguments: ['number'], resultType: 'number', fn: Math.trunc, }, 'sign': { comment: 'Returns either a positive or negative 1, indicating the sign of a number, or zero', examples: [ - {input: 'sign(-3)', result: '-1'}, - {input: 'sign(3)', result: '1'}, - {input: 'sign(0)', result: '0'}, + { input: 'sign(-3)', result: '-1' }, + { input: 'sign(3)', result: '1' }, + { input: 'sign(0)', result: '0' }, ], arguments: ['number'], resultType: 'number', @@ -96,15 +97,15 @@ export default { 'tableLookup': { comment: 'Returns the index of the last value in the array that is less than the specified amount', examples: [ - {input: 'tableLookup([100, 300, 900], 457)', result: '2'}, - {input: 'tableLookup([100, 300, 900], 23)', result: '0'}, - {input: 'tableLookup([100, 300, 900, 1200], 900)', result: '3'}, - {input: 'tableLookup([100, 300], 594)', result: '2'}, + { input: 'tableLookup([100, 300, 900], 457)', result: '2' }, + { input: 'tableLookup([100, 300, 900], 23)', result: '0' }, + { input: 'tableLookup([100, 300, 900, 1200], 900)', result: '3' }, + { input: 'tableLookup([100, 300], 594)', result: '2' }, ], arguments: ['array', 'number'], resultType: 'number', - fn: function tableLookup(arrayNode, number){ - for(let i in arrayNode.values){ + fn: function tableLookup(arrayNode, number) { + for (let i in arrayNode.values) { let node = arrayNode.values[i]; if (node.value > number) return +i; } @@ -114,18 +115,146 @@ export default { 'resolve': { comment: 'Forces the given calcultion to resolve into a number, even in calculations where it would usually keep the unknown values as is', examples: [ - {input: 'resolve(someUndefinedVariable + 3 + 4)', result: '7'}, - {input: 'resolve(1d6)', result: '4'}, + { input: 'resolve(someUndefinedVariable + 3 + 4)', result: '7' }, + { input: 'resolve(1d6)', result: '4' }, ], arguments: ['parseNode'], - fn: function resolveFn(node){ - let {result} = resolve('reduce', node, this.scope, this.context); + fn: function resolveFn(node) { + let { result } = resolve('reduce', node, this.scope, this.context); return result; } - } + }, + 'dropLowest': { + comment: 'Removes one or more of the lowest values in a roll', + examples: [ + ], + arguments: ['rollArray', 'number'], + maxResolveLevels: ['roll', 'reduce'], + minArguments: 1, + maxArguments: 2, + resultType: 'rollArray', + fn: function dropLowestFn(rollArray, numberToDrop = 1) { + // Create a new array where the values are sorted in ascending order + const sortedArray = [...rollArray.values].sort(function (a, b) { + return a.value - b.value; + }); + + // mark the N smallest elements as dropped + for (let i = 0; i < numberToDrop; i += 1) { + console.log('dropped ' + sortedArray[i].value); + sortedArray[i].disabled = true; + sortedArray[i].disabledBy = 'dropLowest'; + } + return rollArray; + }, + }, + 'dropHighest': { + comment: 'Removes one or more of the highest values in a roll', + examples: [ + ], + arguments: ['rollArray', 'number'], + maxResolveLevels: ['roll', 'reduce'], + minArguments: 1, + maxArguments: 2, + resultType: 'rollArray', + fn: function dropHighestFn(rollArray, numberToDrop = 1) { + // Create a new array where the values are sorted in ascending order + const sortedArray = [...rollArray.values].sort(function (a, b) { + return b.value - a.value; + }); + + // mark the N smallest elements as dropped + for (let i = 0; i < numberToDrop; i += 1) { + sortedArray[i].disabled = true; + sortedArray[i].disabledBy = 'dropHighest'; + } + return rollArray; + }, + }, + 'reroll': { + comment: 'Rerolls if a number is less than or equal to the given value', + examples: [ + ], + arguments: ['rollArray', 'number', 'boolean'], + maxResolveLevels: ['roll', 'reduce'], + minArguments: 1, + maxArguments: 3, + resultType: 'rollArray', + fn: function rerollFn(rollArray, numberToReroll = 1, keepNewRoll = false) { + let rollValues = rollArray.values + // Iterate through the roll values + for (let i = 0; i < rollValues.length; i += 1) { + // If the number is less than the reroll limit + if (rollValues[i].value <= numberToReroll) { + // Disable it + rollValues[i].disabled = true; + rollValues[i].disabledBy = 'reroll'; + // Roll it again, insert the new roll into the list at the next index + rollValues.splice(i + 1, 0, { + value: rollDice(1, rollArray.diceSize)[0], + }); + // Skip iterating the inserted roll if we are forced to keep it + if (keepNewRoll) { + i += 1; + } + } + if (i >= 100) { + this.context.error('Can\'t roll more than 100 dice at once'); + return rollArray; + } + } + return rollArray; + }, + }, + 'explode': { + comment: 'Rerolls if a number is greater than or equal to the given value', + examples: [ + ], + arguments: ['rollArray', 'number', 'number'], + maxResolveLevels: ['roll', 'reduce', 'reduce'], + minArguments: 1, + maxArguments: 3, + resultType: 'rollArray', + fn: function explodeFn(rollArray, depth = 1, numberToReroll = rollArray.diceSize) { + let overflowErrored = false; + if (depth > 99) depth = 99; + let rollValues = rollArray.values + // Iterate through the roll values + for (let i = 0; i < rollValues.length; i += 1) { + // If the number is greater than or equal to the reroll limit + // And there is space to reroll it + if (rollValues[i].value >= numberToReroll) { + rollValues[i].bold = true; + let explodeDepth = 1; + let explodeRoll; + do { + // Before inserting this roll, make sure the total dice in the roll + // Doesn't exceed 100 + if (rollValues.length >= 100) { + if (!overflowErrored) { + this.context.error('Can\'t roll more than 100 dice at once'); + overflowErrored = true; + } + break; + } + explodeDepth += 1; + explodeRoll = rollDice(1, rollArray.diceSize)[0]; + const rollObj = { + value: explodeRoll, + italics: true, + }; + // Insert the roll + rollValues.splice(i + 1, 0, rollObj); + i += 1; + } while (explodeDepth <= depth && explodeRoll >= numberToReroll) + } + } + return rollArray; + }, + }, } -function anyNumberOf(type){ +function anyNumberOf(type) { let argumentArray = [type]; argumentArray.anyLength = true; return argumentArray; diff --git a/app/imports/parser/parseTree/call.js b/app/imports/parser/parseTree/call.js index 658c5f49..24b7d3c9 100644 --- a/app/imports/parser/parseTree/call.js +++ b/app/imports/parser/parseTree/call.js @@ -4,14 +4,14 @@ import functions from '/imports/parser/functions.js'; import resolve, { toString, traverse, map } from '../resolve.js'; const call = { - create({functionName, args}) { + create({ functionName, args }) { return { parseType: 'call', functionName, args, } }, - resolve(fn, node, scope, context){ + resolve(fn, node, scope, context) { let func = functions[node.functionName]; // Check that the function exists if (!func) { @@ -25,9 +25,22 @@ const call = { }; } + // Resolve a given node to a maximum depth of resolution + const resolveToLevel = (node, maxResolveFn = 'reduce') => { + // Determine the actual depth to resolve to + let resolveFn = 'reduce'; + if (fn === 'compile' || maxResolveFn === 'compile') { + resolveFn = 'compile'; + } else if (fn === 'roll' || maxResolveFn === 'roll') { + resolveFn = 'roll'; + } + // Resolve + return resolve(resolveFn, node, scope, context); + } + // Resolve the arguments - let resolvedArgs = node.args.map(arg => { - let { result } = resolve(fn, arg, scope, context); + let resolvedArgs = node.args.map((arg, i) => { + let { result } = resolveToLevel(arg, func.maxResolveLevels?.[i]); return result; }); @@ -36,12 +49,12 @@ const call = { node, fn, resolvedArgs, - argumentsExpected: func.arguments, + func, context, }); - if (checkFailed){ - if (fn === 'reduce'){ + if (checkFailed) { + if (fn === 'reduce') { context.error(`Invalid arguments to ${node.functionName} function`); return { result: error.create({ @@ -66,7 +79,7 @@ const call = { if ( arg.parseType === 'constant' && func.arguments[index] !== 'parseNode' - ){ + ) { return arg.value; } else { return arg; @@ -75,20 +88,21 @@ const call = { try { // Run the function - let value = func.fn.apply({scope, context}, mappedArgs); + let value = func.fn.apply({ + scope, + context, + }, mappedArgs); let valueType = typeof value; - if (valueType === 'number' || valueType === 'string' || valueType === 'boolean'){ + if (valueType === 'number' || valueType === 'string' || valueType === 'boolean') { // Convert constant results into constant nodes return { - result: constant.create({ value, valueType }), + result: constant.create({ value }), context, }; } else { - return { - result: value, - context, - }; + // Resolve the return value + return resolve(fn, value, scope, context); } } catch (error) { context.error(error.message || error); @@ -101,26 +115,28 @@ const call = { } } }, - toString(node){ + toString(node) { return `${node.functionName}(${node.args.map(arg => toString(arg)).join(', ')})`; }, - traverse(node, fn){ + traverse(node, fn) { fn(node); node.args.forEach(arg => traverse(arg, fn)); }, - map(node, fn){ + map(node, fn) { const resultingNode = fn(node); - if (resultingNode === node){ + if (resultingNode === node) { node.args = node.args.map(arg => map(arg, fn)); } return resultingNode; }, - checkArugments({node, fn, argumentsExpected, resolvedArgs, context}){ + checkArugments({ node, fn, func, resolvedArgs, context }) { + const argumentsExpected = func.arguments; // Check that the number of arguments matches the number expected if ( !argumentsExpected.anyLength && - argumentsExpected.length !== resolvedArgs.length - ){ + resolvedArgs.length > (func.maxArguments ?? argumentsExpected.length) || + resolvedArgs.length < (func.minArguments ?? argumentsExpected.length) + ) { context.error('Incorrect number of arguments ' + `to ${node.functionName} function, ` + `expected ${argumentsExpected.length} got ${resolvedArgs.length}`); @@ -131,14 +147,14 @@ const call = { // Check that each argument is of the correct type resolvedArgs.forEach((node, index) => { let type; - if (argumentsExpected.anyLength){ + if (argumentsExpected.anyLength) { type = argumentsExpected[0]; } else { type = argumentsExpected[index]; } if (type === 'parseNode') return; if (node.parseType !== type && node.valueType !== type) failed = true; - if (failed && fn === 'reduce'){ + if (failed && fn === 'reduce') { let typeName = typeof type === 'string' ? type : type.constructor.name; let nodeName = node.parseType; context.error(`Incorrect arguments to ${node.functionName} function` + diff --git a/app/imports/parser/parseTree/rollArray.js b/app/imports/parser/parseTree/rollArray.js index 969b948d..30f37240 100644 --- a/app/imports/parser/parseTree/rollArray.js +++ b/app/imports/parser/parseTree/rollArray.js @@ -4,7 +4,7 @@ const rollArray = { create({ values, diceSize, diceNum }) { return { parseType: 'rollArray', - values, + values: values.map(v => ({ value: v })), diceSize, diceNum, }; @@ -16,10 +16,13 @@ const rollArray = { }; }, toString(node) { - return `${node.diceNum || ''}d${node.diceSize} [ ${node.values.join(', ')} ]`; + return `${node.diceNum || ''}d${node.diceSize} [${valuesToString(node.values)}]`; }, reduce(node, scope, context) { - const total = node.values.reduce((a, b) => a + b, 0); + const total = node.values.reduce((a, b) => { + if (b.disabled) return a; + return a + b.value; + }, 0); return { result: constant.create({ value: total, @@ -29,4 +32,15 @@ const rollArray = { }, } +function valuesToString(values) { + return values.map(v => { + let text = `${v.value}`; + if (v.disabled) text = `~~${text}~~`; + if (v.italics) text = `*${text}*`; + if (v.bold) text = `**${text}**`; + if (v.underline) text = `__${text}__`; + return text; + }).join(', '); +} + export default rollArray;