Algebra in ASH?

cakyrespa

Member
While messing with some ideas, I encountered something I couldn't figure out how to do from an ASH script. In general, I would like to be able to take an arbitrary string, such as "2 * 10" and have it calculate the results.

That is a simplification. In general, I'd like to be able to take something like "mainStat * 2" or even "max(mainStat * 2, 200)" from a map file, substitute mainStat with something useful, and then figure out the results.

And, then after some discussions... we seem to come up with this:

RPN Calculator 4

It isn't algebra per se, but the RPN Calculator implements a Reverse Polish Notation calculator. This is a stack-based calculator that will perform arbitrary math from a given string and return the resulting floating point results.

At the most simple usage, the calculator takes a list of whitespace separated values and splits them into tokens. Numbers are pushed into a stack as are variable substitutions. Operators pop one or more of those numbers from the stack, performs some calculation, and pushes the results. When the tokens are done being processed, the resulting number is returned.

For example:

Code:
1 2 +

Adds 1 and 2 and returns 3.

Code:
1 2 + 4 *

Adds 1 and 2 to get 3, then multiples that by 4 to return 12.

The following operations are supported:

  • X Y + => Z: Adds X and Y and pushes the results.
  • X Y - => Z: Subtracts X from Y and pushes the results.
  • X Y * => Z: Multiplies X and Y and pushes the results.
  • X Y / => Z: Divides X by Y and pushes the results.
  • X Y ^ => Z: Raises X to the Y power and pushes the results.
  • X Y min => Z: Pushes the lower of X or Y.
  • X Y max => Z: Pushes the higher of X or Y.
  • X Y C if => Z: If C is 0, pushes Y, otherwise pushes X.
  • C not => Y: If C is 0, pushes 1, otherwise pushes 0.

There are two files with these scripts. The first rpn.ash implements the basic calculator. rpn_example.ash shows how to use the script and also runs through a series of calculations to make sure the values are supported.

The original implement was written by the_great_cow_guru and expanded on cakyrespa and Catch-22.
 

Attachments

Last edited:
ASH has no such built-in ability, and generally speaking, it's not the kind of language that can efficiently support such an ability.

Quick & ugly hack: build a string containing a CLI "ash" command, and execute it. You'd have to store the result in a preference so that you can access it from your script:
Code:
cli_execute("ash set_property(\"temp\", 2 * 10)");
print(get_property("temp"));

You may be able to construct your map file such that it IS a valid ASH script as well, and execute it via 'import'. For example, if your various expressions would otherwise be identified by an integer key, instead use a string key of the form "result[num]=".

ASH is powerful enough that you could write an expression parser in it, although that would be a non-trivial project (especially if your expressions will ever use more than one datatype).
 
ASH is powerful enough that you could write an expression parser in it, although that would be a non-trivial project (especially if your expressions will ever use more than one datatype).

Yeah, here's a sample "multiply" function I wrote which could be used as part of a larger "expression parser" project. It would be pretty complicated in the end, especially if you wanted to handle parentheses and other fancy stuff like that.

This example currently handles string based expression with multiplication only.

Code:
int multiply(string expression_string) {
    string[int] expression_map = split_string(expression_string, "\\*");
    int result = 1;
    foreach i in expression_map {
        result = result * to_int(expression_map[i]);
    }
    return result;
}

Nothing fancy.. It's a lot of troublesome work to implement useful expression parsing in ASH. In the end you'd probably end up better off incorporating somethning like JEPLite or Jexel into your own fork of KoLmafia.

Oh and to see this script in action, add this to the end:

Code:
void main() {
    print(multiply("2*5*8"));
}

You'll get the expected result of 80 instead of to_int()'s result of 285 (just strips the string). I should also mention that it's currently all integer based, but it's easy enough to change them all to floats.
 
Last edited:
Well I'm not sure how useful a feature like this is, but I do love a good programming challenge so here's what I came up with:

It is possible if you use a different method of representing the equations. The problem with the traditional way is that it's ambiguous (as in you need parentheses to tell you how to evaluate the expression, and this is hard to write a script for) However if you use something like reverse polish notation (RPN) it can be done.

I made an RPN calculator in ash as an example. Basically you pass it an expression string and a map of variables and it will evaluate the expression with those variables. If you give it the string: "1 blah + 4 * 5 + x -" and you variables look like this: vars["x"] = 3; vars["blah"] = 2; it will spit out 14, the correct answer.

Have a look at my code if you like, and if anyone has a need for something like this, feel free to use it in your scripts.
 

Attachments

Sure, and there are probably better ways of thinking about it. The original idea I had was for the level up script I'm messing with (because it is fun). Figuring out expected stat gain from a combat is fairly trivial and I already got that done. I'm working on non-combats now. I want to be able to figure out the expected stat gains for non-combat adventures. And, I'm really not fond of large if/else if/else statements or switches of location types for something that could be put into a map file. I'll use them if I have to, but they don't seem as elegant as a single formula in a file.

It seems like for many of the noncombat adventures, you have four basic numerical gains: muscle, moxie, myst, and meat (4M). Each one of those can be either a numeric range or a formula like "max(primeStat * 1.5, 300)" Each formula can be broken into 3 fields (stat, multiplier, cap), but with 4 fields, that would be 12 fields to represent the 4M gains for a given non-combat adventure. It also assumes rather simple formulas, whereas I would be evil and put in something like "max(drunk * 2 + primestat, 200)". Having an arbitrary formula in a map file would make that easier in case Jick decides to be evil.

http://kol.coldfront.net/thekolwiki/index.php/A_Test_of_Testarrrsterone

If you wanted to get complicated, the "Cheat" option of A Test of Testarrrsterone would use an if() construct in a formula. Since, you have two different values, depending on the class of the player. To break that out accurately, I'd have to break those 12 fields into 6 classes (72 fields). Or hard-code it, which is would I'd have to do, but I'd like to avoid that if possible. :)

Some items also use a formula to figure out gains or length of effects (such as the Crimbo stuff lat year with the gene splicing). Call it guilty of my profession, but I deal a lot with parsed formulas in programs, so naturally I think of those first.

Finally, having the ability to get into a map file the effects of familiars would be nice, but those requires powers (I seem to recall there is a "X ** 1.2" in there) to really calculate to decide of a vollyball or sombreo familiar would be a better choice.

If I can't figure this out in ASH, I'll see if I can figure it out in Java, then submit a patch. :)

Side note: Finding a way that both KoLmafia, the CLI, and ASH could use a common formula would be a neat idea. I'd love to see noncombats represented in the front end (in the location details actually). I wrote a wiki parser to generate a "noncombats.txt" file which is what I'm basing off my work. If formulas work, then KoL would be able to just use a map file for stat gain (like the experience for the location now) instead of having to hard-code it.

I won't fork KoLmafia. I've started a few projects that someone forked for some trivial or minor change which I would have just as easily merged into the code with maybe a few minor changes. It is a frustrating and ego-kicking experience. But, once forked, you now have two people maintaining two projects instead of combining effort and getting a single product that is better. That is one reason I requested the flag to not use user_confirm() in zlib instead of just making the change for my personal version. And also why I'm trying to post my canadv changes instead of just having one for my auto-level script. Call it a philosophy.
 
Slightly modified version. I also renamed it to rpn.ash since that seemed a bit more accurate than algebra.

I added the following:

* Included zlib.ash because I'm using min and max and I'm lazy.
* Added the "^" to do powers (X Y ^)
* Added the "min" and "max" operators (though, I really meant min() for most of my examples).
* Added an "if" which takes "A B C if => X". If C == 0, push B, otherwise push A.
* Added a "X not" operator. If x is is 0, then push 1, otherwise push 0.
* Added a eval_rpn_character() which adds a few common fields that I would think most RPN calculations would use.
* Created an override that doesn't take variables, for simple math.
* Added a bunch of other tests to show it working with fancier features.

I also added a few comments. Not sure if that would work for others, but I figured it gets the minimum that I expect to need for my own code.

Code:
> call rpn.ash

1 blah + 4 * 5 + x - => 14.0
10 12 min => 10.0
10 12 max => 12.0
100 200 0 if => 200.0
100 200 1 if => 100.0
prime 1.5 * 300 min => 289.0
prime 1.5 * 300 min prime 200 min isMoxieClass if => 193.0
prime 1.5 * 300 min prime 200 min isMoxieClass not if => 289.0
prime 1.5 * 300 max => 300.0
prime 1.5 * 300 max prime 200 max isMoxieClass if => 200.0
prime 1.5 * 300 max prime 200 max isMoxieClass not if => 300.0
10 2 ^ => 100.0
10 1.2 ^ => 15.848933
 

Attachments

Nice, I'm happy to see you were able to use my code as a nice starting point. I really like the eval_rpn_character function you added. I could see how this could actually be quite useful in some cases. I will definitely keep a copy of your improved version around in case I ever need it ;)
 
I won't fork KoLmafia.

Hehe, was never really a serious suggestion. Just saying that it would be less code to integrate something that's already done than write one from scratch.

Must say, impressive work on the RPN. Quite interesting stuff and a good implementation of a stack in ASH.
 
Oh yeah before I forget, the is_numeric function (currently looks like this)
Code:
boolean is_numeric(string test) {
	if (test == "-") return false;
	string valid_numeric = "0123456789.-";
	string [int] test_chars = split_string(test, "");
	foreach i in test_chars {
		if (!contains_text(valid_numeric, test_chars[i])) {
			return false;
		}
	}
	return true;
}

should really have an extra check for the string "." just to be safe. I was being lazy when I wrote it since I didn't see a reason for anyone to just type a "." but just in case here's the code that makes sure it doesn't get accepted as numeric

Code:
boolean is_numeric(string test) {
	if (test == "-" || test == ".") return false;
	string valid_numeric = "0123456789.-";
	string [int] test_chars = split_string(test, "");
	foreach i in test_chars {
		if (!contains_text(valid_numeric, test_chars[i])) {
			return false;
		}
	}
	return true;
}
 
Oh yeah before I forget, the is_numeric function (currently looks like this)
should really have an extra check for the string "." just to be safe. I was being lazy when I wrote it since I didn't see a reason for anyone to just type a "." but just in case here's the code that makes sure it doesn't get accepted as numeric

Code:
boolean is_numeric(string test) {
	if (test == "-" || test == ".") return false;
	string valid_numeric = "0123456789.-";
	string [int] test_chars = split_string(test, "");
	foreach i in test_chars {
		if (!contains_text(valid_numeric, test_chars[i])) {
			return false;
		}
	}
	return true;
}

So now .- and ... are considered a valid number. A regular expression would work nicely for this:
Code:
^(-?[0-9]+)((\.?[0-9]+)?)$

Edit: If you're using this in a matcher in ASH, make sure the RegExp is properly escaped so that Java can handle the string.
Code:
"^(-?[0-9]+)((\\.?[0-9]+)?)$"
 
Last edited:
True, that would be a much better way to do it. I didn't know it was possible to do regex matching in ash, could you give an example of how to do that? thanks

Here's the suggested is_numeric() function:

Code:
boolean is_numeric(string test) {
    matcher isnum = create_matcher("^(-?[0-9]+)((\\.?[0-9]+)?)$" , test);
    if(isnum.find()) {
        return true;
    }
    return false;
}

You can test it for me :)

I was thinking today though; to be strictly RPN, shouldn't the numbers be unsigned? You would push a 0 on the stack then do a subtraction.

So if you wanted the value -5, this is how it should be written.

0 5 -

The result would represent the negative number.

The regular expression would need to be changed so that unsigned numbers were enforced, the new one would look like this: "^([0-9]+)((\\.?[0-9]+)?)$"
 
The regex looks good to me. I had no idea there was a whole matcher class. I'm sure that'll come in handy when I'm writing other scripts too.

Not that it's a big deal, but could you simplify the function like this, to save space?

Code:
boolean is_numeric(string test) {
    return create_matcher("^(-?[0-9]+)((\\.?[0-9]+)?)$" , test).find();
}

As for the negative numbers, true you could represent them like that. I wasn't aware of any restriction that said you couldn't use negative numbers in RPN though, and even if that is the case, it can't hurt to have a nice shortcut for the users, right? ;)

I'll let caky update the script in the interest of not having a billion copies floating around this thread...
 
Back
Top