New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Computation Rewriting Part 2: Calculations #501
Comments
More specifically, a directed acyclic graph.
I'm actually quite concerned about the UI topic. Instead of going into detail here, I have created a separate issue: #502 |
While I also see a directed acyclic graph as the end goal, a forest (one tree for every stat you want to look at) could be a good idea for a first solution. It should be much simpler (no need to think about where nodes can be merged/reused) but slower because it'd be a lot bigger from all the duplications. This may or may not be something that can gradually be transformed into the DAG end goal by removing duplication and merging trees. I haven't spent much time thinking about the data structure yet, but I wanted to mention this possibility.
Where to draw the line between mechanics being part of the data and being part of the calculation logic is probably something that should be figured out for each mechanic on its own. There are a lot of things that can easily be represented as data (e.g. the str/dex/int conversions to other stats are already part of the given data) and others that don't really make sense there (e.g. the calculation logic should handle the different forms -- base, increased, more, overrides -- itself).
I don't really have any ideas on that yet. Except that the stat (with conditions), form and value distinction made by parsing probably also makes a lot of sense as the key structuring points for calculation graph nodes. Also, it probably make sense to keep each added modifier as a separate node, at least if they are not exactly the same and come from exactly the same source (see all the different conditions).
Not much I can add to that atm. It probably would be a good solution not to solve everything with the same data structure but build at least three (slightly to a lot) different structures:
|
Really simple example of how I imagined the data structure: Things obviously get a lot more complex once you take conditions and conversions into account. Basic idea of an algorithm for adding mods into the data structure:
The naive implementation would also throw away subtrees that were not added anywhere. Preventing that should avoid reiterating all mods in the first step: if the mod links to values of other nodes, those other nodes should still be stored and would not need to be recreated. I see the "Total" nodes as the key points for speeding up the traversal. If references to these are stored somewhere and they contain enough additional information to decide whether they (or their subtree) are affected by mods, traversal could mostly be reduced to looking at these nodes (except for mods that actually affect them). |
That seems like a good start (and obviously matches your mod "syntax" very well). Some thoughts:
Still wrapping my head around it, I probably deleted most of what I wrote while making this comment. (PS: What did you create those trees in?) |
I'm not sure I understand you correctly. If you are talking about the stats that don't depend on the tree/items/anything variable, there's already the concept of "given" stats in the parsing data. While there's no Pantheon in the program at the moment, that would be another of the variable things.
For most things, if they are not supported, they can be added to the builder interfaces. This comes down to the topic of representing mechanics as data or logic again. There will be some things that don't make much sense as modifiers/stats, these would need to be supported by the data structure itself. Another example for that are conversions/gains ("x% of Foo converted to Bar", "gain x% of Foo as extra Bar"). They are not just "take the value of Foo, multiply it by x/100 and add the result as base to Bar", e.g. increases only apply once.
I'm not entirely sure yet how to handle conditions. For your examples, "enemy is chilled" is a global condition that has the same evaluation for the entire structure. For those my idea was that each node has access to an object it can query these global conditions from. It would probably make sense to represent both evaluation cases in the data structure at once so it doesn't need to be rebuilt when conditions change. Some/most of these global conditions need to be user-specified. "Your crit chance is lucky" would probably be implemented as a stat that modifies how effective crit chance is calculated (if we add an effective crit chance stat similar to Path of Building's). However there are also modifier parts currently parsed as conditions that are not globally true/false. E.g. "... with main hand", "minions have ...", "... from equipped shield". These would need to be handled differently in the structure on a case-by-case basis.
If we only calculate one set main skill, which I think is a very useful simplification to make, there would only be a constant number of "total fire damage" nodes: one if the damage is not hand-specific (spells), or one for main-hand and one for off-hand if the damage is hand-specific. And maybe three for each of those depending on how min/max/average damage is handled. Other nodes linking to fire damage would need to specify which one of these they want (main-hand and off-hand would probably be calculated independently, so most nodes linking to fire damage are either main-hand or off-hand themselves, same for min/max).
For "10% increased fire damage with main hand" it could easily be decided which "total fire damage" node it affects: it does affect the main-hand one (both "fire damage" and "with main hand" match that node) and it doesn't affect the off-hand one ("with main hand" doesn't match).
I'll do one now.
http://www.umlet.com/ |
I spent some time figuring out what a subgraph for a stat needs to look like independent of the mods added to it. The graph below is this subgraph with all inherent nodes explicitly added, otherwise it is similar to the graphs I posted before. It's also no longer a tree. I also went through the builders and wrote down some notes for the mechanics I found that can't be data-driven and would need to be supported by the graph itself. These are not represented in the graph as that would make it a lot less readable. If you don't see any issues or possible improvements here, I think it is now enough specification to actually start implementing the calculation structure. I can also wait with pushing anything if you want more time to think about this and sketch out your own ideas, @MLanghof . There's still a lot missing, especially regarding conditions, but I feel like I need to start diving into it to solve those things as they come up. |
Don't let me hold you up, you seem to have a very clear picture already, it's not like I have a better idea lying around right now. I'm still slightly skeptical about the conversion/gain part, because the set of applicable modifiers is potentially different for every path through the "gain/convert x ->y" graph (at least in my understanding), meaning you need a separate base and uncapped subtotal node for each such path. For example, if you convert 50% of cold to fire, 50% of lightning to fire, 50% of phys to cold and 50% of phys to lightning (and have base damage for each of those), there are five fire base values (two from phys, one from cold, one from lighting and one from fire itself), each with a unique set of applicable increases etc. - I don't see that fitting into your scheme at the moment. But maybe there's an easy solution (I don't see "make sure each mod is only applied once" being able to resolve that without intricate inter-node logic). |
Yeah, what I wrote in the graph was without thinking about conversion/gain chains. With chains, there will be repetition in the subgraphs of stats. From a design perspective, it has a somewhat easy solution. Whether that's easy to implement is another question. For your example: "Make sure each mod is only applied once" is relevant for the Increase (and More) nodes. They may link to the same mod for different damage types but shouldn't apply them multiple times. |
(contains no functionality changes, only file moves) Move files to new Computation.Common project so Computation, Computation.Parsing and Computation.Data don't need to reference each other at all and so files previously in Computation.Parsing that aren't really related to parsing (Builders and Data namespaces) are moved away from there. Remove Computation root project as it was empty after the moves.
So shouldn't the graph maybe be built in multiple passes then? First all nodes that depend on other nodes, then all the static ones? I guess it wouldn't solve all issues but it's probably more efficient to do it this way rather than search through all increases again after you add the fifth fire base value. |
I don't think that is possible when the graph is built incrementally with each skill tree/gear change. But it wouldn't have to search through all increases again. My idea (not shown in the graph from my previous comment) is to store the modifier nodes for each stat and form somewhere and have other nodes like those Increase nodes link to that collection and only indirectly to the actual modifier nodes. So e.g. "Increase Fire, Light" would link to the increase collections of Fire damage and Lightning damage. But when the entire graph is rebuild (e.g. on startup. changing builds or whenever else it might be necessary), it sounds like a good idea to order the modifiers in a way that makes the initial building process more efficient. Maybe the object responsible for constructing/changing the graph should always takes a collection of modifiers as parameter so it can order them (which would also be applicable to the incremental changes because a lot of tree nodes have multiple modifiers, but I don't see much speedup potential there). |
they are more or less detailed depending on the topic but should cover everything (together with the comments in #501)
The core graph structure is now done (ignoring everything "special" that has to be built in, e.g. conversion paths, see the list at the end). The idea behind the subgraph for a stat is basically the same as my "core building block" diagram. There will be one such subgraph for each instance of an Edges of the graph are represented by events: When the value calculation of a node uses the value of another node, it subscribes to the There are two types of nodes:
Adding modifiers can be done simply by retrieving the stat subgraphs for the modifier's That is all graph construction that needs to be done when adding modifiers. The subgraph's nodes will only be created when they are necessary, i.e. when their value is requested from another node's value calculation. This model can handle most of what is required. E.g. conditions can either determine the stat of a modifier (e.g. for main hand/off hand) or be part of the value calculation (set the value to 0/null if the condition is not met). However, there a few things still left to implement for what I'd consider the first part of this issue:
|
Calculation as a whole works now and can be used through I'm hoping to get that and a very simple UI integration (task 3 of this issue) done before the next league starts. Would be nice to release the calculation rewriting in an alpha version with the next league. Short example usage of
|
The second part of the computation rewriting (see #463) is the core calculation part.
This should do pretty much what you'd expect: put parsed stats in and get the calculated stats out.
The goal here would again be to separate the mechanics from the calculation logic as much as possible. That means, there are mostly two parts to this:
(PR: Calculation Rewriting Part 2.1: Core Data Structure #509) The calculation data structure. Some kind of graph structure built using parsed stats where you can select nodes you want to receive value updates about (the nodes representing the stats that are shown in the UI).
(PR: Calculation Rewriting Part 2.2: Mechanics and Builder Implementation #512) The mechanics. The builder interfaces from parsing need a non-stub implementation that will define the interaction between calculation and parsing as they (or something built from them) will be passed into the calculation data structure.
There are also the given stats and effect stats already defined together with the parsing data that are not yet used. They will need to be passed to and used by the calculation data structure in some parsed form.
For the main mechanics, e.g. how DPS is calculated, my idea was to define them as data similar to the given stats. E.g. add a new entry to the given stats that says something to the effect of "Value of the DPS stat = value of the damage per hit stat * value of the attacks per second stat". Defining these as data leads to a calculation data structure that doesn't need to know anything about mechanics and what it actually calculates.
(PR: Calculation Rewriting Part 2.3: Skill and Item parsing #515) There needs to be a way to turn items and skills into modifiers
(PR: Calculation Rewriting Part 2.4: Basic UI #517) For the computation rewriting to become usable while it is work in progress, some minor UI integration should also be done in this part. I'm thinking about a simple separate component that just shows the values of a few stats here (e.g. as a new tab next to skill tree and equipment). The larger work for that will probably be using the user's actual tree and equipment and showing the calculation conditions that need to be user specified.
I'll start with the calculation data structure. That is the most interesting part from an algorithmic point of view, so I'll post my ideas for it here. But it shouldn't be developed completely in isolation, so it will also require a bit of work on the mechanics and integration parts (not UI integration, probably some extension to the console program from parsing).
The text was updated successfully, but these errors were encountered: