doomeer / kalandralang

A programming language for Path of Exile crafting recipes.
MIT License
58 stars 4 forks source link

Problem with List of good mods #19

Closed AR-234 closed 2 years ago

AR-234 commented 2 years ago

I want to craft an item were a combination of different mods is allowed. I got a list of mods (6 t1, 5 t2) that are good to keep, but the item needs atleast 2 to be worth it and craft further. How would I do this?

Currently I would need to have a if statement with every combination or am I wrong?

Was thinking of a overhaul of the condition system e.g. if prefix_count > 2 then Then integers could be added with basic functinality to solve problems like that, or even counters that are integers but only with simple instructions like

counter total
if has "ChaosResist6" then +counter_total
if has "Dexterity4" then +counter_total
if has "ChanceToSuppressSpells5__" then +counter_total
if has "ChanceToSuppressSpells4" then -counter_total

if counter_total < 2 then scour

this is obiously not tought through just a quick mock up

an other approach would be to have a list and check how many of the conditions are true.. if 2 of [has "ChaosResist6", has "Dexterity4", has "ChanceToSuppressSpells5__"]

What do you think about this problem?

doomeer commented 2 years ago

I agree with you, I often had similar needs and in fact I was planning to implement exactly what you suggest at the end: a predicate that says "at least N predicates in the following list hold". I was thinking of a syntax like:

count [
  has "ChaosResist6";
  has "Dexterity4";
  open_prefix
] >= 2

which is very close to your suggestion.

Another orthogonal idea I want to get to at some point is to be able to express things like "tier 4 or better". It could look like this:

has_group "IncreasedLife" tier <= 4

although I'm not fond of this particular syntax proposal, and I'm not sure how to extract tier information yet from the game data (maybe I can just extract the number from the mod name and reverse it, but there may be cases where GGG were not very consistent in their names?…).

One could even combine the two ideas to be able to express something like "the sum of the tiers of resistance mods is 4 or better". For instance:

tier_sum [ "FireResist"; "LightningResist"; "ColdResist" ] <= 4

Assuming that the absence of a mod group counts as having tier 999 or something, the above effectively means "all 3 resists are present as T1 except one which is T2".

Another idea would be to be able to have variables (not necessarily mutable though) to avoid repeating stuff, like:

let $count = count [ has "ChaosResist6"; has "Dexterity4"; open_prefix ]
if $count >= 2 then gain 2 exalt
if $count >= 3 then gain 3 exalt

(the dollar $ is because this is a very keyword-heavy language and it makes sure that variable names do not become keywords later, which would break existing scripts).

I need to spend a bit more time to design something good and you're right, it may be more consistent, if we do that, to also have prefix_count be used like prefix_count <= 2. We may want to be able to define intervals like 1 <= prefix_count <= 2 (i.e. prefix count is between 1 and 2).

All of these are possible, but I want the result to be consistent and not look like a patchwork of ideas that don't play well with each other, so it'll take a bit of time to get right.

AR-234 commented 2 years ago

yes mod tiers.. for example flasks, the lowest tier is 1 in the id for boots it is the other way around were for example Dex T1 has Dexterity9 I hope that it is a per base thing, but that needs more research. I was thinking maybe it is better to sort the mod group by ilvl required so you get the tier. (would only need a small script to verify, if and mod groups have the same ilvl for 2 mods or if all are in order)

the first approach with a condition list is a good one combined with the variables this would eliminate a lot of repeating code. maybe even a more php like approach were even let isn't there, but a keychar $ is absolutly needed and makes sense even from a readability point of view. Because if a command line is $<variable> = <int/bool>

I also like the interval approach.

Maybe even something like this:

$count = count [ 
    has "ChaosResist" == 1;  
    has "Dexterity" <= 2 ; 
    has "Strength" <= 2; 
    2 <= has "FireResist" <= 4; 
    has "MovementVelocity";
]

$limit = count [
    has "Dexterity"; 
    has "Strength"; 
]
if $count >= 2 and $limit == 1 then gain 2.5 exalt

in my example has would always check mod group and resolve them always to booleans. has "MovementVelocity" would resolve to has any movement speed mod if so true otherwise false the other onces are quite self explaning

also the ending of every list entry with a semicolon is not by mistake, would reduce risk to forget one, maybe allow it but not enforce it? so the last entry can have a semicolon but doesn't need it.

with proper syntax highlighting I think this can look kind of clean and it is faster to write code for then looking up every id, atleast that is my hope

So a little todo list would be:

and the rest would need a complete overhaul

doomeer commented 2 years ago

Sorting by ilvl is probably the best way, you're probably right, good idea!

Agree with allowing trailing semicolons. This allows to move lines around without having to fix semicolons.

Here is something else to consider: be able to specify a value for each mod. For instance:

$value = case_sum
  | has "ChaosResist" tier 1 -> 10
  | has "Dexterity" tier 1 -> 5
  | has "Dexterity" tier 2 -> 2
  | has "Strength" tier 1 -> 5
  | has "Strength" tier 2 -> 2

if $value < 17 then goto .restart

gain $value exalt

This effectively means "must have chaos res T1, must have dex and str at least T1 and T2, then sell the item for the sum in exalt"

Syntax is just me playing around with ideas but basically I think it could be useful to assign value to each mod and be able to do stuff with the sum of the values.

AR-234 commented 2 years ago

I think this would be a little bit to complicated to wrap your head around while writing this because prices are not always fixed and if a price changes you have to change a lot of values.

or if you want to keep this feature just to give options like this:

case_sum $value
  with  has "ChaosResist"  = 1   for 10 chaos
  with  has "Dexterity"    = 1   for 10 chaos
  with  has "Dexterity"    = 2   for  5 chaos
  with  has "Strength"     = 1   for 10 chaos
  with  has "Strength"     = 3   for  5 chaos

just writing something that is elsewhere in use but this would look cleaner

case_sum $value
  | has "ChaosResist"  = 1   for 10 chaos
  | has "Dexterity"    = 1   for 10 chaos
  | has "Dexterity"    = 2   for 10 chaos
  | has "Strength"     = 1   for 10 chaos
  | has "Strength"     = 2   for 10 chaos
AR-234 commented 2 years ago

also just to post it count would look with this like

  count $value
    with has "ChaosResist"  = 1
    with has "Dexterity"    = 1
doomeer commented 2 years ago

Actually after sleeping over it I think I have a simpler solution. One could write something like this:

$value =
  5 * (3 - tier "ChaosResist") +
  4 * (2 - tier "Dexterity") +
  4 * (2 - tier "Strength")

For instance, with T1 chaos res, T2 dex and T1 strength, value is 14.

If variables are mutable, you can also write:

$value = 0
if tier "ChaosResist" = 1 then $value += 10
if tier "ChaosResist" = 2 then $value += 5
if tier "Dexterity" = 1 then $value += 4
if tier "Strength" = 1 then $value += 4

for a similar result, which may be more readable (but a bit more verbose).

I think this would be easier to understand and more general than the case_sum operation I proposed earlier.

What is needed to achieve this:

This is not that much work I believe and would provide a lot of expressive power.

AR-234 commented 2 years ago

yeah probably the best approach to the problem. I guess wouldn't hurt if there is no mod group to set the tier to 1, since it is the tier 1 of that mod since there is only one?

A thing we need to look out for is the orb of dominance since they are higher in the list but are named tier "E" for elevated so no clue how we should handle that maybe as tier 0 internally but allow as E aswell? if tier "ChaosResist" = E then $value += 100 which would be the same as if tier "ChaosResist" = 0 then $value += 100

or should we just stick with Tier 0 for them?

AR-234 commented 2 years ago

Also for optimization purposes i was thinking to calculate the tier of each mod when creating the cache and writing it in there.

doomeer commented 2 years ago

Yes, I planned to cache tiers. But actually it's more complicated than that: tiers depend on the item. For instance, T1 life is not the same mod on a body armour and on gloves. So my plan is to store global tiers in the cache and also have in-memory caching (memoization) for local tiers.

That being said, I had a quick look and reached unfortunate conclusions:

However, it looks like detecting elevated mods is easy:

I wonder how Craft of Exile does it.

AR-234 commented 2 years ago

nebuchenazarr (Craft of Exile):

I group things by mod name + affix type (prefix, suffix) + affix group (base, elder, crafted, etc). If these all match its assumed that they are the same mod and thus tiers of that mod. Then they are ordered by ilvl. In rare cases where they have the same ilvl they are ordered by values. Since i group by mod name i had to do some custom handling for special cases where the wording change even though its the same mod. Like with "an additionnal arrow" and "additionnal arrows" not grouping together. So its not perfect but these cases are rare.

Regarding Maven he thinks he has done it with the ID containing Maven

doomeer commented 2 years ago

That's very useful, thanks!

rbardou commented 2 years ago

I have pushed a commit that causes the mod tiers to be displayed next to the modifiers. For instance:

--------
Citrine Amulet (Rare)
--------
(prefix) [T1] 26% increased Spell Damage (SpellDamage5)
(prefix) +1 to Level of all Fire Skill Gems (GlobalFireGemLevel1_)
(suffix) [T1] 24% increased Fire Damage (FireDamagePercent5)
(suffix) [T1] 20% increased Cast Speed (IncreasedCastSpeed4)
(suffix) [T4] +30% to Cold Resistance (ColdResist5)
(prefix) {crafted} +49 to maximum Life (EinharMasterIncreasedLife3)
--------

As you can see it doesn't display a tier for +1 to Level of all Fire Skill Gems and it actually makes sense because the mod group has all the +1 to Level of all X Skill Gems mods but they all have the same tier basically. In other words tiers do not make sense for this particular mod group because tiers would not define a total order.

It may be annoying for weapons where there are two tiers of +X to Level of all X Skill Gems, and we can fix that later by considering that all those mods are Tier 1 or something, but as a first implementation I think it is quite satisfying. Now I can actually add expressions like tier X <= Y and they will work for most mods, including stuff like resists and attributes where it would be quite useful.

My implementation would not immediately support elevated mods yet but I think I should still be able to implement orb of dominance based on it.

rbardou commented 2 years ago

I have pushed a commit which implements arithmetic operations and comparisons, with the possibility to use prefix_count, suffix_count, affix_count and tier <mod_group>. I have also added an example:

# This recipe shows how to use arithmetic and tiers to express complex conditions
# on modifiers.

# We start from an ilvl 100 Agate Amulet.
buy "Metadata/Items/Amulets/Amulet9"

# Then we chaos spam until the sum of the tiers of attribute modifiers
# is 6 or less, meaning that we have all three modifiers in one of the following combinations:
# - T1 + T1 + T4 or better
# - T1 + T2 + T3 or better
# - T2 + T2 + T2 or better
until
  tier "Dexterity" +
  tier "Intelligence" +
  tier "Strength" <= 6
do
  chaos

I still need to document this in the manual.

doomeer commented 2 years ago

Arithmetic expressions are now documented in the manual.

Now we may still want to count the number of predicates that hold. Maybe we can have a way to convert a predicate to an integer, e.g. using brackets like [has X] + [has Y] + [tier Z <= 3].

And we may also still want variables.

AR-234 commented 2 years ago

A problem I am currently still facing with the current implementation is that I got a lot of disired mods for example: Fire/Cold/Lightning/Chaos Resistance Dexterity/Strength SpellSuppression

I only want to keep items that have atleast t3 mods and all suffixes taken by one of the list above.. so its something like (but not really since a t5 and a t1 mod would also match, if tiers that are not found are ignored..)

buy "Metadata/Items/Armours/Boots/BootsStrDex8"
until 
    tier "ColdResistance" +
    tier "FireResistance" +
    tier "LightningResistance" +
    tier "ChaosResistance" +
    tier "Intelligence" +
    tier "Dexterity" +
    tier "ChanceToSuppressSpells" <= 6
do 
    chaos

but with this I end up in an infinte loop, hadn't really the time to check how you handle if a mod is not found but I imagen you give it a real high number which leads to an infinite loop once again :)

maybe a function that counts boolean expressions would still be helpful since that kind of stuff would be cleaner to read and possible more intuitive for users to implement, since the trade site got filters like and (implemented) not (implemented) if (implemented) count (not really implemented) weighted sum (not implemented)

also with a count function you could implement far more complex statement then in the current setup or am i missing something? :D

doomeer commented 2 years ago

Indeed your example doesn't work because tier returns 999 if no mod with the requested mod group exists. If tier returned 0 instead, it would not be better: your condition would be true for items that have none of the requested mods.

So yes, we need a way to count predicates. I see two choices.

Choice 1 — Predicates As Ints

One would be able to convert any condition into an integer, with true = 1 and false = 0, using brackets [ ... ].

Example to count the number of attribute mods:

[tier "Strength" <= 20] + [tier "Intelligence" <= 20] + [tier "Dexterity" <= 20]

This returns a number between 0 and 3 corresponding to the number of attribute mods.

You can even use weights. For instance, let's say that you like Dexterity more than Intelligence, and Intelligence more than Strength. You could write:

[tier "Strength" <= 20] + 2 * [tier "Intelligence" <= 20] + 3 * [tier "Dexterity" <= 20]

Pros:

Why brackets by the way? Why not just allow has "Strength8" + has "Intelligence7"?

Choice 2 — A Dedicated count Operation

We already discussed this and gave examples in this thread.

Pros:

I'm currently leaning heavily towards choice 1.

AR-234 commented 2 years ago

I would say approach 1 is also better since it enables weights aswell, but I dislike the <= 20 to filter out the 999, because it isn't really intuitive (if you know the code / background its obvious, otherwise it isn't really) which would be a pro for count since it is more readable as you already said.

also the weights would be weired since t1 would be worse then t3 lol, but could be done with something like 3 * (1 / [tier "Dexterity" <= 20]), but what is the maven tier.. if it's 0 this will not work anymore..

maybe the anwser is just both.. most languages do have for and while and they are mostly the same except that one is more like a raw version and the other one is a shortcut to a longer more complex problem..

doomeer commented 2 years ago

About using <= 20 to filter out 999: I agree it's not very readable. We can add a function has_group instead:

[has_group "Strength"] + 2 * [has_group "Intelligence"] + 3 * [has_group "Dexterity"]

About 3 * (1 / [tier "Dexterity" <= 20]): did you mean 3 * (1 / tier "Dexterity")? I would use a subtraction instead:

3 * (2 - tier "Dexterity")

Value is 6 for tier 0, 3 for tier 1, 0 for tier 2, -3 for tier 3, etc. We may need a way to cap it at 0, such as 3 * max 0 (2 - tier "Dexterity") but it's not very intuitive when you're not used to it.

Currently if you want to give more value to better tiers you can write:

[tier "Dexterity" <= 1] + [tier "Dexterity" <= 2] + [tier "Dexterity" <= 3]

the value of this is 1 for T3, 2 for T2, 3 for T1.

It's still not ideal. It's probably more readable to just use if but it would be better with variables:

$value = 0
if tier "Dexterity" <= 3 then $value = 4 - tier "Dexterity"

or with if as expressions:

if tier "Dexterity" <= 3 then 4 - tier "Dexterity" else 0

or

4 - (if tier "Dexterity" <= 3 then tier "Dexterity" else 4)

which is the same as

4 - (min 4 (tier "Dexterity"))

None of these is very readable but count would not help here I think.

I think we just need to write more recipes and see what we often use in practice to see how we would want to write it. I try to avoid generalizing a solution without at least 3 use cases :) But I do believe that the bracket thing would be useful.

AR-234 commented 2 years ago

One Thing to consider ist you would have to write the highest Tier always in to the code.

Which isnt optimal.

doomeer commented 2 years ago

By "in the code" do you mean in recipes? I don't think it is needed since we often don't care about low-tier mods and we can just ignore tiers lower than, say, 4. For mods with only 2 or 3 tiers, we can just use the mod names directly with has.

AR-234 commented 2 years ago

oh yeah mistake on my part you are right. (and yes i meant in the recipe)

doomeer commented 2 years ago

I have implemented the bracket syntax. Here is an extract from the user manual:

buy "Metadata/Items/Amulets/Amulet9"
until
  [tier "Intelligence" <= 3] +
  [tier "Dexterity" <= 3] +
  [tier "Strength" <= 3]
  >= 2
do chaos

is a recipe that uses Chaos Orbs on an Agate Amulet until it has at least two attribute modifiers, both of them tier 3 or better.

I think this is a good first step. Let's play with it a bit and see if it is enough in practice.

AR-234 commented 2 years ago

awesome thanks alot, will close this since it is done with this update, if I find anything not behaving right, will try to fix it or report it <3