Added dice functions to parse engine

This commit is contained in:
Stefan Zermatten
2023-04-01 11:27:52 +02:00
parent 8e610c2cd8
commit b9588c83b1
3 changed files with 223 additions and 64 deletions

View File

@@ -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;

View File

@@ -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` +

View File

@@ -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;