Substantial progress on rebuilding computation engine

This commit is contained in:
Stefan Zermatten
2021-09-09 13:47:41 +02:00
parent 23e99565dc
commit 55bca633fc
16 changed files with 401 additions and 244 deletions

View File

@@ -0,0 +1,24 @@
import walkDown from '/imports/api/creature/computation/newEngine/utility/walkdown.js';
export default function computeInactiveStatus(node){
const prop = node.node;
if (isActive(prop)) return;
prop.inactive = true;
prop.deactivatedBySelf = true;
// Mark children as inactive due to ancestor
walkDown(node.children, child => {
child.node.inactive = true;
child.node.deactivatedByAncestor = true;
});
}
function isActive(prop){
if (prop.disabled) return false;
switch (prop.type){
case 'buff': return !!prop.applied;
case 'item': return !!prop.equipped;
case 'spell': return !!prop.prepared || !!prop.alwaysPrepared;
case 'note': return false;
default: return true;
}
}

View File

@@ -0,0 +1,98 @@
/**
* Performs a depth first traversal of the character tree, summing the container
* and inventory contents on the way up the tree
*/
export default function computeInventory(forest, dependencyGraph){
const data = {
weightTotal: 0,
weightEquipment: 0,
weightCarried: 0,
valueTotal: 0,
valueEquipment: 0,
valueCarried: 0,
itemsAttuned: 0,
};
// The stack of properties to still navigate
const stack = [...forest];
// The current containers we are inside of
const containerStack = [];
while(stack.length){
const top = stack[stack.length - 1];
const prop = top.node;
if (prop._computationDetails.inventoryChildrenVisited){
stack.pop();
handleProp(prop, containerStack, data, dependencyGraph);
} else {
// Add all containers to the stack when we first visit them
if (prop.type === 'container'){
containerStack.push(top.node);
setDefaultContainerData(prop);
}
// Push children onto the stack and mark this as children are visited
stack.push(...top.children);
prop._computationDetails.inventoryChildrenVisited = true;
}
}
// Store all the computed values on the dependency graph variables
for (let key in data){
dependencyGraph.addNode(key, {engineValue: data[key]});
}
}
function setDefaultContainerData(container){
container.contentsWeight = 0;
container.carriedWeight = 0;
container.contentsValue = 0;
container.carriedValue = 0;
}
function handleProp(prop, containerStack, data, dependencyGraph){
// Determine if this property is carried, items are carried by default
let carried = prop.type === 'container' ? prop.carried : true;
// Weight and value for this property
const weight = (prop.weight || 0) + (prop.contentsWeight || 0);
const carriedWeight = (prop.weight || 0) + (prop.carriedWeight || 0);
const value = (prop.value || 0) + (prop.value || 0);
const carriedValue = (prop.value || 0) + (prop.carriedValue || 0);
// Sum the item-specific data
if (prop.type === 'item'){
dependencyGraph.addLink('itemsAttuned', prop._id);
if (prop.attuned) data.itemsAttuned += 1;
if (prop.equipped){
dependencyGraph.addLink('weightEquipment', prop._id);
data.weightEquipment += weight;
dependencyGraph.addLink('valueEquipment', prop._id);
data.valueEquipment += value;
}
}
// Get the parent container
const container = containerStack[containerStack.length - 1];
if (container){
// The container depends on this prop for its contents data
dependencyGraph.addLink(container._id, prop._id);
// Add this property's weights and values to the container
if (!container.weightless){
container.contentsWeight += weight;
if (carried) container.carriedWeight += carriedWeight;
}
container.contentsValue += value;
if (carried) container.carriedValue += carriedValue;
} else {
// There is no parent container, add weights/value to the character data
dependencyGraph.addLink('weightTotal', prop._id);
data.weightTotal += weight;
dependencyGraph.addLink('valueTotal', prop._id);
data.valueTotal += value;
if (carried){
dependencyGraph.addLink('weightCarried', prop._id);
data.weightCarried += carriedWeight;
dependencyGraph.addLink('valueCarried', prop._id);
data.valueCarried += carriedValue;
}
}
}

View File

@@ -0,0 +1,17 @@
/**
* Only computes `totalFilled`, need to compute `quantityExpected.value`
* before `spacesLeft` can be computed
*/
export default function computeSlotQuantityFilled(node, dependencyGraph){
let slot = node.node;
slot.totalFilled = 0;
node.children.forEach(child => {
let childProp = child.node;
dependencyGraph.addLink(slot._id, childProp._id)
if (childProp.type === 'slotFiller'){
slot.totalFilled += child.slotQuantityFilled;
} else {
slot.totalFilled++;
}
});
}

View File

@@ -0,0 +1,16 @@
import walkDown from '/imports/api/creature/computation/newEngine/utility/walkdown.js';
export default function computeToggleDependencies(node, dependencyGraph){
const prop = node.node;
// Only for toggles that aren't inactive and aren't set to enabled or disabled
if (
prop.inactive ||
prop.type !== 'toggle' ||
prop.disabled ||
prop.enabled
) return;
walkDown(node.children, child => {
child.node._computationDetails.toggleAncestors.push(prop._id);
dependencyGraph.addLink(child.node._id, prop._id, prop.condition);
});
}

View File

@@ -0,0 +1,24 @@
import SymbolNode from '/imports/parser/parseTree/SymbolNode.js';
import AccessorNode from '/imports/parser/parseTree/AccessorNode.js';
import findAncestorByType from 'imports/api/creature/computation/newEngine/utility/findAncestorByType.js';
export default function linkCalculationDependencies(dependencyGraph, prop, propsById){
prop._computationDetails.calculations.forEach(calcObj => {
// Traverse the parsed calculation looking for variable names
calcObj._parsedCalculation.travese(node => {
if (node instanceof SymbolNode || node instanceof AccessorNode){
// Link ancestor references as direct property dependencies
if (node.name[0] === '#'){
let ancestorProp = findAncestorByType(
prop, node.name.slice(1), propsById
);
if (!ancestorProp) return;
dependencyGraph.addLink(prop._id, ancestorProp._id, calcObj);
} else {
// Link variable name references as variable dependencies
dependencyGraph.addLink(prop._id, node.name, calcObj);
}
}
});
});
}

View File

@@ -0,0 +1,40 @@
const linkDependenciesByType = {
attribute: linkVariableName,
classLevel: linkVariableName,
constant: linkVariableName,
damageMultiplier: linkDamageMultiplier,
proficiency: linkStats,
effect: linkStats,
skill: linkSkill,
}
export default function linkTypeDependencies(dependencyGraph, prop){
linkDependenciesByType[prop.type]?.(prop);
}
function linkVariableName(dependencyGraph, prop){
// The variableName of the prop depends on the prop
if (prop.variableName){
dependencyGraph.addLink(prop.variableName, prop._id);
}
}
function linkDamageMultiplier(dependencyGraph, prop){
prop.damageTypes.forEach(damageType => {
dependencyGraph.addLink(`${damageType}Multiplier`, prop._id);
});
}
function linkStats(dependencyGraph, prop){
// The stats a prop references depend on that prop
prop.stats.forEach(variableName => {
if (!variableName) return;
dependencyGraph.addLink(variableName, prop._id);
});
}
function linkSkill(dependencyGraph, prop){
linkVariableName(dependencyGraph, prop);
// The prop depends on the variable references as the ability
if (prop.ability) dependencyGraph.addLink(prop._id, prop.ability);
}

View File

@@ -0,0 +1,35 @@
import { prettifyParseError, parse } from '/imports/parser/parser.js';
import ErrorNode from '/imports/parser/parseTree/ErrorNode.js';
import applyFnToKey from '/imports/api/creature/computation/newEngine/utility/applyFnToKey.js';
export default function parseCalculationFields(prop, schemas){
// For each key in the schema
schemas[prop.type]._schemaKeys.forEach( key => {
// that ends in '.calculation'
if (key.slice(-12) !== '.calculation') return;
const calcKey = key.sclice(0, -12);
// For all fields matching they keys
// supports `keys.$.with.$.arrays`
applyFnToKey(prop, calcKey, calcObj => {
// Store a reference to all the calculations
prop._computationDetails.calculations.push(calcObj);
// Parse the calculation
parseCalculation(calcObj);
});
});
}
function parseCalculation(calcObj){
let calculation = calcObj.calculation || '';
try {
calcObj._parsedCalculation = parse(calculation);
} catch (e) {
let error = prettifyParseError(e);
calcObj.errors ?
calcObj.errors.push(error) :
calcObj.errors = [error];
calcObj._parsedCalculation = new ErrorNode({error});
}
}

View File

@@ -0,0 +1,126 @@
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
import CreatureProperties,
{ DenormalisedOnlyCreaturePropertySchema as denormSchema }
from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import computedOnlySchemas from '/imports/api/properties/computedOnlyPropertySchemasIndex.js';
import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js';
import applyFnToKey from '/imports/api/creature/computation/newEngine/utility/applyFnToKey.js';
import { cloneDeep, unset } from 'lodash';
import createGraph from 'ngraph.graph';
import computeInventory from '/imports/api/creature/computation/newEngine/buildComputation/computeInventory.js';
import walkDown from '/imports/api/creature/computation/newEngine/utility/walkdown.js';
import parseCalculationFields from '/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js';
import computeInactiveStatus from '/imports/api/creature/computation/newEngine/buildComputation/computeInactiveStatus.js';
import computeToggleDependencies from '/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js';
import linkCalculationDependencies from '/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js';
import linkTypeDependencies from '/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js';
import computeSlotQuantityFilled from '/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js';
/**
* Store index of properties
* recompute static tree-based enabled/disabled status
* Build a dependency graph
* id -> id dependencies for docs that rely on other docs directly
* id -> variable deps for docs that rely on a variable's value
* variable -> id deps for variables that are impacted by docs
* TODO:
* Depth first traversal or dependency graph to:
* Find loops in the dependency graph
* resolve variables in dependency order
*/
/**
* TODO
* compute slots spaces left (after computed field of quantityExpected)
* compute damage multipliers
* compute dependencyGraph variables and properties
*/
export default function buildCreatureComputation(creatureId){
let properties = CreatureProperties.find({
'ancestors.id': creatureId,
'removed': {$ne: true},
}, {
sort: {order: 1}
});
// Dependency graph where edge(a, b) means a depends on b
// The graph includes all dependencies even of inactive properties
// such that any properties changing without changing their dependencies
// can limit the recompute to connected parts of the graph
const dependencyGraph = createGraph();
const computation = {
originalPropsById: {},
propsById: {},
propsByType: {},
propsByVariableName: {},
props: properties,
dependencyGraph,
};
// Process the properties one by one
properties.forEach(prop => {
// Store the prop in the memo by type, variableName and id
storePropInMemo(prop, computation)
// Store the prop in the dependency graph
dependencyGraph.addNode(prop._id, prop);
// Remove old computed only fields
computedOnlySchemas[prop.type]._schemaKeys.forEach(key =>
applyFnToKey(prop, key, unset)
);
// Remove old denormalised fields
denormSchema._schemaKeys.forEach(key =>
applyFnToKey(prop, key, unset)
);
// Add a place to store all the computation details
prop._computationDetails = {
calculations: [],
toggleAncestors: [],
};
// Parse all the calculations
parseCalculationFields(prop, computedSchemas)
});
// Get all the properties as trees based on their ancestors
let forest = nodeArrayToTree(properties);
// Walk the property trees computing things that need to be inherited
walkDown(forest, node => {
computeInactiveStatus(node);
computeToggleDependencies(node);
computeSlotQuantityFilled(node);
});
// Compute the inventory
computeInventory(forest, dependencyGraph);
// Graph functions that rely on the props being stored first
properties.forEach(prop => {
linkTypeDependencies(dependencyGraph, prop, computation.propsById);
linkCalculationDependencies(dependencyGraph, prop, computation.propsById);
});
return computation;
}
function storePropInMemo(prop, memo){
// Store dicts for easy access later
// Store a copy of the unmodified prop
memo.originalPropsById[prop._id] = cloneDeep(prop);
// Store by id
memo.propsById[prop._id] = prop;
// Store by type
memo.propsByType[prop.type] ?
memo.propsByType[prop.type].push(prop) :
memo.propsByType[prop.type] = [prop];
// Store by variableName
memo.propsByVariableName[prop.variableName] ?
memo.propsByVariableName[prop.variableName].push(prop) :
memo.propsByVariableName[prop.variableName]= [prop];
}

View File

@@ -1,239 +0,0 @@
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
import CreatureProperties,
{ DenormalisedOnlyCreaturePropertySchema as denormSchema }
from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import computedOnlySchemas from '/imports/api/properties/computedOnlyPropertySchemasIndex.js';
import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js';
import applyFnToKey from '/imports/api/creature/computation/newEngine/applyFnToKey.js';
import { cloneDeep, unset } from 'lodash';
import { prettifyParseError, parse } from '/imports/parser/parser.js';
import ErrorNode from '/imports/parser/parseTree/ErrorNode.js';
import SymbolNode from '/imports/parser/parseTree/SymbolNode.js';
import AccessorNode from '/imports/parser/parseTree/AccessorNode.js';
import createGraph from 'ngraph.graph';
import findAncestorByType from 'imports/api/creature/computation/newEngine/findAncestorByType.js';
/**
* Store index of properties
* recompute static tree-based enabled/disabled status
* Build a dependency graph
* id -> id dependencies for docs that rely on other docs directly
* id -> variable deps for docs that rely on a variable's value
* TODO:
* variable -> id deps for variables that are impacted by docs
* Depth first traversal or dependency graph to:
* Find loops in the dependency graph
* resolve variables in dependency order
*/
export default function computeCreature(creatureId){
let properties = CreatureProperties.find({
'ancestors.id': creatureId,
'removed': {$ne: true},
}, {
sort: {order: 1}
});
const originalPropsById = {};
const propsById = {};
const propsByType = {};
// Process the properties one by one
properties.forEach(prop => {
// Store the prop by Id and Type
originalPropsById[prop._id] = cloneDeep(prop);
propsById[prop._id] = prop;
if (!propsByType[prop.type]) propsByType[prop.type] = [];
propsByType[prop.type].push(prop);
// Store the prop in the dependency graph
dependencyGraph.addNode(prop._id, prop);
// Remove all computed only fields
computedOnlySchemas[prop.type]._schemaKeys.forEach(key =>
applyFnToKey(prop, key, unset)
);
// Remove all denormalised fields
denormSchema._schemaKeys.forEach(key =>
applyFnToKey(prop, key, unset)
);
// Add a place to store all the computation details
prop._computationDetails = {
calculations: [],
toggleAncestors: [],
};
// parse every calculation field
computedSchemas[prop.type]._schemaKeys.forEach( key => {
if (key.slice(-11) !== 'calculation') return;
const calcKey = key.sclice(0, -11);
applyFnToKey(prop, calcKey, calcObj => {
// Store a reference to all the calculations
prop._computationDetails.calculations.push(calcObj);
// Parse the calculation
parseCalculation(calcObj);
return calcObj;
});
});
});
// Process the properties in tree format
let creatureTree = nodeArrayToTree(properties);
walkDown(creatureTree, node => {
denormaliseInactiveStatus(node);
inheritToggleDependencies(node);
computeInventory(node);
});
// Dependency graph where edge(a, b) means a depends on b
const dependencyGraph = createGraph();
// Build graph now that all props are stored
properties.forEach(prop => {
linkTypeDependencies(dependencyGraph, prop, propsById);
if (prop.inactive) return;
linkCalculationDependencies(dependencyGraph, prop, propsById);
});
}
function walkDown(tree, callback){
let stack = [...tree];
while(stack.length){
let node = stack.pop();
callback(node);
stack.push(...node.children);
}
}
function denormaliseInactiveStatus(node){
const prop = node.node;
if (isActive(prop)) return;
prop.inactive = true;
prop.deactivatedBySelf = true;
// Mark children as inactive due to ancestor
walkDown(node.children, child => {
child.node.inactive = true;
child.node.deactivatedByAncestor = true;
});
}
function isActive(prop){
if (prop.disabled) return false;
switch (prop.type){
case 'buff': return !!prop.applied;
case 'item': return !!prop.equipped;
case 'spell': return !!prop.prepared || !!prop.alwaysPrepared;
default: return true;
}
}
function inheritToggleDependencies(node, dependencyGraph){
const prop = node.node;
// Only for toggles that aren't inactive and aren't set to enabled or disabled
if (
prop.inactive ||
prop.type !== 'toggle' ||
prop.disabled ||
prop.enabled
) return;
walkDown(node.children, child => {
child.node._computationDetails.toggleAncestors.push(prop._id);
dependencyGraph.addLink(child.node._id, prop._id, prop.condition);
});
}
function computeInventory(forest){
const data = {
weightTotal: 0,
weightEquipment: 0,
weightCarried: 0,
valueTotal: 0,
valueEquipment: 0,
valueCarried: 0,
itemsAttuned: 0,
}
// The stack of properties to still navigate
const stack = [...forest];
// The current containers we are inside of
const containerStack = [];
const visitedNodes = new Set();
while(stack.length){
const top = stack[stack.length - 1];
// Leaf node
if (top.children.length === 0){
} else {
}
}
}
function parseCalculation(calcObj){
let calculation = calcObj.calculation || '';
try {
calcObj._parsedCalculation = parse(calculation);
} catch (e) {
let error = prettifyParseError(e);
calcObj.errors ?
calcObj.errors.push(error) :
calcObj.errors = [error];
calcObj._parsedCalculation = new ErrorNode({error});
}
}
function linkCalculationDependencies(dependencyGraph, prop, propsById){
let variableNames = [];
prop._computationDetails.calculations.forEach(calcObj => {
calcObj._parsedCalculation.travese(node => {
if (node instanceof SymbolNode || node instanceof AccessorNode){
if (node.name[0] !== '#'){
dependencyGraph.addLink(prop._id, node.name, calcObj);
} else {
let ancestorProp = findAncestorByType(
prop, node.name.slice(1), propsById
);
if (!ancestorProp) return;
dependencyGraph.addLink(prop._id, ancestorProp._id, calcObj);
}
}
});
});
return variableNames;
}
const inventoryVariables = [
'weightTotal',
'weightEquipment',
'weightCarried',
'valueTotal',
'valueEquipment',
'valueCarried',
'itemsAttuned',
];
const linkDependenciesByType = {
attribute: linkVariableName,
classLevel: linkVariableName,
constant: linkVariableName,
container: linkInventoryVariables,
}
function linkVariableName(dependencyGraph, prop){
if (prop.inactive) return;
if (prop.variableName){
dependencyGraph.addLink(prop.variableName, prop._id);
}
}
function linkInventoryVariables(dependencyGraph, prop){
inventoryVariables.forEach(variableName => {
dependencyGraph.addLink(variableName, prop._id);
});
}
function linkTypeDependencies(dependencyGraph, prop){
linkDependenciesByType[prop.type]?.(prop);
}

View File

@@ -0,0 +1,8 @@
export default function walkDown(tree, callback){
let stack = [...tree];
while(stack.length){
let node = stack.pop();
callback(node);
stack.push(...node.children);
}
}

View File

@@ -44,10 +44,20 @@ const ComputedOnlyContainerSchema = createPropertySchema({
type: Number,
optional: true,
},
// Weight of all the carried contents (some sub-containers might not be carried)
// zero if `contentsWeightless` is true
carriedWeight:{
type: Number,
optional: true,
},
contentsValue:{
type: Number,
optional: true,
},
carriedValue:{
type: Number,
optional: true,
},
});
const ComputedContainerSchema = new SimpleSchema()

View File

@@ -1,5 +1,4 @@
import SimpleSchema from 'simpl-schema';
import DAMAGE_TYPES from '/imports/constants/DAMAGE_TYPES.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
/*
@@ -20,7 +19,7 @@ let DamageMultiplierSchema = new SimpleSchema({
// The technical, lowercase, single-word name used in formulae
'damageTypes.$': {
type: String,
allowedValues: DAMAGE_TYPES,
max: STORAGE_LIMITS.calculation,
},
// The value of the damage multiplier
value: {

View File

@@ -1,6 +1,6 @@
import SimpleSchema from 'simpl-schema';
import DAMAGE_TYPES from '/imports/constants/DAMAGE_TYPES.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
const DamageSchema = createPropertySchema({
// The roll that determines how much to damage the attribute
@@ -22,7 +22,7 @@ const DamageSchema = createPropertySchema({
},
damageType: {
type: String,
allowedValues: DAMAGE_TYPES,
max: STORAGE_LIMITS.calculation,
defaultValue: 'slashing',
},
});

View File

@@ -30,7 +30,6 @@ let SlotFillerSchema = new SimpleSchema({
slotQuantityFilled: {
type: SimpleSchema.Integer,
defaultValue: 1,
min: 0,
},
// Filters out of UI if condition isn't met, but isn't otherwise enforced
slotFillerCondition: {