Evaluating strings as equations

zarqon

Well-known member
I'm trying to make a map of combat items, and originally I designed a map of records where one of the fields was the average damage. But so many of the items do highly variable damage based on whatever formula.

I'm wondering if there is a way to evaluate strings as math, i.e. take a string containing a formula, such as "(x/1.5)+2", replace x with the current value of x, and then solve it. to_int() merely strips out the numbers and puts them together, and I don't see anything else in ashref that looks promising.

Or, is there some other way of accessing this data than building a map?
 
This is the best thing I have so far, ASH using the CLI to make more ASH, with a mafia property as the means of getting the return value. But it's ugly as sin. I'd like to avoid this craziness if possible:

Code:
string formula = "(familiar_weight(my_familiar())/1.5)+2";
cli_execute("ash set_property(\"tempint\",to_string("+formula+")));
return to_int(get_property("tempint"));
 
There we go. Ha, I was unknowingly ninja'd by Jason. If that ugly hack is also the only natively working option he could suggest, then I feel better about posting such as my best solution thus far.

The parsers presented in that thread are much more lovely (props to Cake and Cow), but also a bit too bulky for inclusion in ZLib. Seems like an ugly but oh-so-useful new function

float solve(string formula, float[string] values)

will be making an appearance in ZLib. Being able to store formulae in data files has a vast multitude of uses, particularly for anything involving familiars (weight-dependent) or anything stat-sensitive.
 
Could you explain this to me?

Code:
cli_execute("ash set_property(\"tempint\",to_string("+formula+")));
return to_int(get_property("tempint"));
 
Say I am making a data file with familiar attack rates (actually, I just did this...). I want to have a map of float[familiar], with the float being the rate that the familiar will successfully attack. Many are weight-sensitive, and familiars may have different formulae, so they are stored in the source map as strings, e.g.

ceil((familiar_weight($familiar[angry goat])+weightmod)/6)*0.1

as the attack rate for an Angry Goat. In ASH, assuming I had a variable named weightmod, this would be evaluated just fine. However, it's a string and will not be executed as if it were ASH. The only way to get an arbitrary string to process like ASH from within ASH is by using the CLI "ash" command. And the only way to then get a pertinent return value from cli_execute() is by setting a property (or writing a map) and then reading the property/datafile afterwards. That's what's happening in those two lines.

My final function looks like this:

PHP:
// evaluates formula as ASH after substituting values, returns solution
string to_ash(string formula, string[string] values) {
   if (count(values) > 0) foreach key,val in values
      formula = replace_string(formula,key,to_string(val)).to_string();
   cli_execute("ash set_property(\"temp\",to_string("+formula+"))");
   return get_property("temp");
}


So using the above I could do this:

Code:
float[familiar] famattackrates;

string[string] values;
values["weightmod"] = numeric_modifier("Familiar Weight");

string atkrate = "ceil((familiar_weight($familiar[angry goat])+weightmod)/6)*0.1"

famattackrates[$familiar[angry goat]] = to_float(to_ash(atkrate,values));

Not super useful when it's hard-coded like that, but if the formulas are fields of records in data files, then it's super useful. Also note that the script would want to be smarter about how it calculated weightmod beforehand.

This will appear in ZLib whenever I get my physically-resistant monster CCS working. Today I got familiars and combat items figured out and data-filed. Next: attack skills. Ugh, it's so much Wiki-ing.

Just barely related: anyone know attack / damage formulas for the wereturtle??
 
Last edited:
In ASH, assuming I had a variable named weightmod, this would be evaluated just fine. However, it's a string and will not be executed as if it were ASH. The only way to get an arbitrary string to process like ASH from within ASH is by using the CLI "ash" command. And the only way to then get a pertinent return value from cli_execute() is by setting a property (or writing a map) and then reading the property/datafile afterwards. That's what's happening in those two lines.

That's a really clever work-around. Really twisted. It is interesting seeing how you can get a return value from cli_execute.
 
And a very few others such as the cheapest of an item.

cheapest hi mein; set temp = it

I'm actually thinking about changing this function to write to a map called temp.txt, just because the CLI output gets so ugly when I'm calculating using formulas. Lots of

temp => 11.5
Returned: void

Every time this function runs. Annoying.
 
Last edited:
Edit: There was a silly question here. I realized the answer shortly after posting it. Please ignore.
 
Last edited:
Assuming that the ultimate goal here is to have the combat item data be available via your Map Manager... you DO realize, I hope, that you're opening a can of worms so large that ASH has no datatype capable of representing its size? "drink(20,$item[Imp Ale])" would be the very least of the mischief that could be done if arbitrary ASH expressions became publicly editable.

There is actually another expression evaluator in mafia, that could perhaps be exposed for script use: the one that handles varying modifiers. It's guaranteed side-effect-free, and supports min(), max(), ceil(), floor(), sqrt(), and all the basic math operators. It deals with floats only, and has a strict limit to how complex of an expression it can handle, but the real problem is that it doesn't support arbitrary variables - and the built-in variables lack many that you would need (such as the player's current stats). That seemed like a complete show-stopper, which is why I didn't mention this possibility before, but on further thought that's easily fixable - just run a regex over the expression to replace all variables with their values, then evaluate the result.

Interested? Here's the current documentation for this evaluator:

# No spaces are allowed within the expression, except as part of a zone/location name.
# + - * / ( ) have their usual mathematical meaning and precedence.
# ^ is exponentiation, with the highest precedence.
# Functions available:
# ceil(x) floor(x) sqrt(x) min(x,y) max(x,y)
# Location functions: loc(text) zone(text)
# These have a value of 1 if the current adventure location or zone contains the
# specified text, 0 elsewhere.
# Familiar function: fam(text)
# This has a value of 1 if the player's familiar type contains the text, else 0.
# Preferences function: pref(text)
# There can be at most one of each text function in any single expression.
# Upper-case letters are varying values:
# B - Blood of Wereseal effect
# D - drunkenness
# F - fullness
# G - Grimace darkness (0..5)
# H - Hobo Power
# J - 1 on Festival of Jarlsberg, 0 otherwise
# L - player level
# M - total moonlight (0..9)
# S - spleenness
# T - turns remaining of this effect
# U - telescope upgrades
# W - familiar weight
# X - gender (-1=male, 1=female)
(The variables I've grayed out wouldn't be usable, since they might currently be set to a speculative value by the Modifier Maximizer or whatif. Also, pref() wouldn't be safe to use in general, since it reformats the preference value as a float.)
 
The can of worms is not far from being opened! You may have stepped in in the nick of time.

This sounds like exactly what we need to store formulas in data files. If you look at the above to_ash(), you'll see that I was already replacing variable names with values. Doing it using this method would involve more variables -- I couldn't continue using things like my_buffedstat() in the formula -- but it would be preferable to the unlikely but possible mischief that misanthropic data-file editors could inflict upon users.

This scares me:

There can be at most one of each text function in any single expression.

I'm assuming this does not mean math functions like ceil() and min()...

Summary: definitely interested. I'd also vote for keeping pref().
 
To show you how close I was to releasing the dangerous to_ash(), here's some output from my current test script:

======== ITEMS ==========
temp => 100
Returned: void
fetid feathers (84) deal 100.0 stench damage. Worth 145 meat (1.45 MPD).
Returned: void
flaming feathers (84) deal 100.0 hot damage. Worth 195 meat (1.95 MPD).
Returned: void
flirtatious feathers (84) deal 100.0 sleaze damage. Worth 190 meat (1.9 MPD).
temp => 110
Returned: void
piles of floorboard cruft (35) deal 110.0 stench damage. Worth 500 meat (4.5454545 MPD).
temp => 100
Returned: void
frightful feather (86) deal 100.0 spooky damage. Worth 130 meat (1.3 MPD).
temp => 22
Returned: void
handfuls of frigid ninja stars (78) deal 22.0 cold damage. Worth 134 meat (6.090909 MPD).
temp => 100
Returned: void
frozen feathers (86) deal 100.0 cold damage. Worth 139 meat (1.39 MPD).
temp => 42
Returned: void
grouchy restless spirits (18) deal 42.0 spooky damage. Worth 345 meat (8.214286 MPD).
temp => 4
Returned: void
cans of hair spray (72) deal 4.0 hot damage. Worth 24 meat (6.0 MPD).
temp => 403
Returned: void
love songs of icy revenge (20) deal 403.0 cold damage. Worth 800 meat (1.9851117 MPD).
Returned: void
love songs of naughty innuendo (25) deal 403.0 sleaze damage. Worth 175 meat (0.43424317 MPD).
temp => 343
Returned: void
love songs of smoldering passion (47) deal 343.0 hot damage. Worth 175 meat (0.5102041 MPD).
temp => 72
Returned: void
molotov cocktail cocktails (72) deal 72.0 hot damage. Worth 300 meat (4.1666665 MPD).
temp => 12
Returned: void
handfuls of onion shurikens (84) deal 12.0 sleaze damage. Worth 100 meat (8.333333 MPD).
temp => 72
Returned: void
patchouli oil bombs (29) deal 72.0 stench damage. Worth 300 meat (4.1666665 MPD).
temp => 100
Returned: void
sausage bombs (59) deal 100.0 sleaze damage. Worth 500 meat (5.0 MPD).
temp => 5
Returned: void
spectres' scepters (7) deal 5.0 hot damage. Worth 0 meat (0.0 MPD).
======== FAMILIARS ==========
Passive familiar bonus: +10 lbs.
temp => 0.2
Returned: void
temp => 6.3333335
Returned: void
Your 16-lb. Angry Goat will attack 20.0% of the time for 6.3333335 stench damage (1.2666668 DPR)
temp => 0.33
Returned: void
temp => 15.333333
Returned: void
Your 23-lb. Frozen Gravy Fairy will attack 33.0% of the time for 15.333333 cold damage (5.06 DPR)
temp => 0.4
Returned: void
temp => 15.833333
Returned: void
Your 14-lb. Grue will attack 40.0% of the time for 15.833333 spooky damage (6.3333335 DPR)
temp => 0.15
Returned: void
temp => 9.333333
Returned: void
Your 14-lb. MagiMechTech MicroMechaMech will attack 15.000001% of the time for 9.333333 hot damage (1.4 DPR)
temp => 0.46200004
Returned: void
temp => 16.166668
Returned: void
Your 16-lb. Misshapen Animal Skeleton will attack 46.200005% of the time for 16.166668 spooky damage (7.4690013 DPR)
temp => 0.2
Returned: void
temp => 10.666667
Returned: void
Your 16-lb. Ninja Snowflake will attack 20.0% of the time for 10.666667 cold damage (2.1333334 DPR)
temp => 0.13200001
Returned: void
temp => 11.333333
Returned: void
Your 14-lb. Scary Death Orb will attack 13.200002% of the time for 11.333333 spooky damage (1.4960002 DPR)
temp => 0.33
Returned: void
temp => 23.333334
Returned: void
Your 35-lb. Stinky Gravy Fairy will attack 33.0% of the time for 23.333334 stench damage (7.7000003 DPR)
temp => 0.5
Returned: void
temp => 2
Returned: void
Your 14-lb. Wereturtle will attack 50.0% of the time for 2.0 spooky damage (1.0 DPR)
======== PHYSICAL ==========
Your elemental attack: 1.0

:D
 
By "text function", I mean zone(), loc(), fam(), and pref(). The language doesn't really have strings, so each of these stores its parameter in a particular field of the parsed expression object. If you used any one of these functions twice, both uses would therefore use the same string parameter. That's not at all a problem in the intended uses of the language.

The problem with pref(): if you used it to retrieve a preference with the value "10", the internal representation would be changed to be a float. The string version of the preference, as written to disk or accessed via get_property(), would then be "10.0". Treating that as an integer would discard all non-digit characters, resulting in a value of 100 - probably not what you intended. Basically, to use integer-valued prefs in an expression, you'd need to copy them into a variable (or perhaps the wrapper function that substitutes variable values into the expression could have some syntax for specifying prefs directly).
 
Added in r7754. Here's my test script, which adds support for user-defined variables:
Code:
# Evaluates expressions in the format used by variable modifiers:

# No spaces are allowed within the expression, except as part of a zone/location name.
# + - * / ( ) have their usual mathematical meaning and precedence.
# ^ is exponentiation, with the highest precedence.
# Functions available:
#	ceil(x) floor(x) sqrt(x) min(x,y) max(x,y)
# Location functions: loc(text) zone(text)
#	These have a value of 1 if the current adventure location or zone contains the
#	specified text, 0 elsewhere.
# Familiar function: fam(text)
#	This has a value of 1 if the player's familiar type contains the text, else 0.
# Preferences function: pref(text)
#	This must be used on preferences with a float value ONLY - merely retrieving
#	an integer pref will corrupt it!
# There can be at most one of each text function in an expression.
# All upper-case letters are reserved for internally-used variables.
# The ones likely to be of use in user code are:
#	D - drunkenness
#	F - fullness
#	G - Grimace darkness (0..5)
#	L - player level
#	M - total moonlight (0..9)
#	S - spleenness
#	X - gender (-1=male, 1=female)

# This wrapper allows user-defined variables to be used as well, which must
# have names starting with a lower-case letter (or underscore) to distinguish
# them from built-in variables.  Variables are supplied as a float[string] map.

# Note that modifier_eval NEVER generates errors - a malformed expression will
# just print a message, and return an arbitrary value.

float eval(string expr, float[string] vars)
{
	buffer b;
	matcher m = create_matcher( "\\b[a-z_][a-zA-Z0-9_]*\\b", expr );
	while (m.find()) {
		string var = m.group(0);
		if (vars contains var) {
			m.append_replacement(b, vars[var].to_string());
		}
		// could implement functions, pref access, etc. here
	}
	m.append_tail(b);
	return modifier_eval(b.to_string());
}

# TESTING:

float[string] v;
v["pi"] = 3.14159265;
v["ten"] = 10;
print(eval("2+3", v));
print(eval("max(pi^ten,ten^pi)", v));
print(eval("sqrt(pi)*L", v));
print(eval("undefined/2", v));
 
Brilliant, this will do nicely!

I'd just finished adding spell damage formulas, too! Now I've got three formula-containing data files to convert to this new, clearly superior format.

Actually, I don't know that anyone would prefer modifier_eval() over eval()... might be nicer to have your eval() in mafia instead of modifier_eval(). Otherwise, I'll just pop eval() into ZLib and forget that modifier_eval() exists. :) Either way, thanks!
 
Having problems getting this to work for calculating spell damage:

> ash modifier_eval("(1+0.0)*(40+0.35*162.0+10.0+0.0)")

Modifier bytecode invalid at 1: [C@f93d85
Modifier bytecode invalid at 2: [C@f93d85
Unexpected error, debug log printed.
Returned: void

This came from my data file testing script, which shows this:

=== Awesome Balls of Fire ===
damage: '(1+spelldmgpercent)*(40+(0.35*buffedmys)+spelldmg+elembonus)'
types: 'hot'
elembonus: 0.0
Evaluating '(1+0.0)*(40+(0.35*162.0)+10.0+0.0)'...
Modifier bytecode invalid at 1: [C@28842f
Modifier bytecode invalid at 2: [C@28842f
Unexpected error, debug log printed.

Note: the space was inserted by the forum. You'll see that variables were substituted and it was reduced just fine in the eval() function.

The debug log shows an array-out-of-bounds error.

java.lang.ArrayIndexOutOfBoundsException: 10
at net.sourceforge.kolmafia.Modifiers$Expression.eval(Modifiers.java:2382)


Am I doing something wrong?
 
Last edited:
After some tests, it seems the evaluator has problems with zeros, but I haven't been able yet to figure out why.
 
Last edited:
Back
Top