Please note that all Advanced Script Handler (ASH) scripts MUST have the ".ash" extension (in other words, they must all end with ".ash"). Failure to conform to this file naming convention will result in really bizarre runtime errors. Other than that, ASH scripts are handled exactly the same as standard command-line interface (CLI) scripts: you may select them from the scripts menu and "call" them from any command-line interface. You should not, however, "call" other ASH scripts from within an ASH script, as this yields unpredictable results.
KoLmafia's ASH supports five primitive types:
void
,boolean
,int
,float
, andstring
. In addition to the five primitive types, there are nine special types:item
,effect
,class
,stat
,skill
,familiar
,slot
,location
, andzodiac
. In addition to these, maps may be constructed from these primitive and special types. Maps are also referred to as "aggregates" and are equivalent to the boolean algebra concept of a "map," where elements from a domain are mapped to elements in a range. For more details, please read the maps tutorial.
item
This can be any item found in the Kingdom of Loathing universe, such as a cog and a Mr. Accessory.
effect
This is any status effect with which you can be afflicted, such as Empathy or Poisoned.
class
Any of the six Kingdom of Loathing classes, such as Accordion Thief or Sauceror.
stat
These are the three main stats - Muscle, Moxie or Mysticality.
skill
This is any skill you can learn in the Kingdom. Examples include Moxious Madrigal and Amphibian Sympathy.
familiar
This is any familiar you can find in the Kingdom, such as a Pet Rock or Cocoabo.
slot
This is an equipment slot, including hat, weapon, off-hand and pants. Accessories are denoted with by acc1, acc2, and acc3, which correspond to the first, second and third accessory slots, respectively.
location
This is anywhere where you can go adventuring. Like The Spooky Forest or The Haunted Pantry.
zodiac
This is an ascension sign, such as Vole or Opossum. One extra possibility is none, which is the sign given to newly-created characters which have never ascended.
While the special-typed variables are declared the same as primitive types (ie:
item i
), special-typed static values are declared very differently from static values. To specify that a value is of a special type,$typename[value]
must be used, with a string in square brackets designating the intended value (ie:$item[cog]
).
if ( <boolean expression> ) <block or statement>if ( <boolean expression> ) <block or statement> else <block>while ( <boolean expression> ) <block or statement>for <var> from <int expression> upto <int expression> [ by <int expression> ] <block or statement>for <var> from <int expression> downto <int expression> [ by <int expression> ] <block or statement>foreach <key> in <aggregate> <block or statement>repeat <block or statement> until ( <boolean expression> );
The KoLmafia User Scripts repository has begun a community-effort construction of an ASH Function Reference Manual which can be consulted for a listing of functions currently provided by the ASH interpreter.
This is a little tutorial to help you get started with advanced scripting in KoLmafia. We're going to build a healing script. Let's start with a simple one. Say you wish to heal using a Doc Galaktik's Ailment Ointment.
There is a nifty function available as seen in the reference: use( [int], [item] ). That would probably be quite suitable for using a Doc Galaktik's Ailment Ointment! But as also seen in the reference, this function requires two arguments. You're going to have to tell the function how many items you wish to use, and which item you wish to use. So let's have the function use 1 Ailment Ointment.
This leaves us with the following script:
use( 1, $item[Doc Galaktik's Ailment Ointment] );
That silly line in blue is a script. If you wish to use it, you're going to have to somehow get KoLmafia to read it. So look inside your KoLmafia folder. There might be a scripts folder in there, that's where your scripts go. If you don't have one, no matter. Just create it. Inside this folder, create a file called heal.ash and put that silly line in there. If you start up KoLmafia now, you can call the script. Go into the Top Menu, picking Scripts, and then heal.ash. The script will be executed. Alternately, you can call the script through the Graphical CLI, which allows you to follow it's activity.
I thought this healing script would serve me until the end of eternity, but I ran into some problems with it: it doesn't seem to work if I'm out of Ailment Ointments. Now I don't wish to purchase 10000 Ailment Ointments at the start of each run to avert this problem, so it might be handy to have the script buy an Ailment Ointment before healing. A second function can be found in the reference, buy( [int], [item] ). It takes the exact same arguments as use but purchases the requested items rather than using them. Thus, version 2 of the script is born:
buy( 1, $item[Doc Galaktik's Ailment Ointment] ); use( 1, $item[Doc Galaktik's Ailment Ointment] );
I don't know about you guys, but I'm cheap. I don't like it if this fancy script here decides to buy things I don't need. Those Ailment Ointments cost money, and if I still have some left, I want to use those. I don't collect Ailment Ointments. It's possible to create an if statement, which will buy an Ailment Ointment only if I don't already have some. Another function from the reference is required: item_amount( [item] ). This function doesn't do anything, but it returns the amount of [item] you currently have. That means that if I call item_amount( [item] ) for Ailment Ointments, I'll be able to use this value to determine whether I need to buy any. Let's test this function first. We will define a variable, put the item amount in there, and then return the variable. Like so:
int ailment_count = item_amount( $item[Doc Galaktik's Ailment Ointment] ); return ailment_count;
Of course you don't specifically need to use the variable. The following is an equivalent script:
return item_amount( $item[Doc Galaktik's Ailment Ointment] );
Now that we have sufficiently ensured that this function does in fact count items in your inventory. We were going to update our healing script with a condition. If I wish to buy an extra Ailment Ointment only if I don't already have one, I will have to use an if statement. There are two ways to use an if statement, you can use curly braces to define the set of actions you wish to be performed, or you can omit the curly braces, and only the next action will be skipped if the condition isn't met. The following two scripts are equivalent:
Script 1:
if ( item_amount( $item[Doc Galaktik's Ailment Ointment] ) < 1 ) buy( 1, $item[Doc Galaktik's Ailment Ointment] ); use( 1, $item[Doc Galaktik's Ailment Ointment] );
Script 2:
if ( item_amount( $item[Doc Galaktik's Ailment Ointment] ) < 1 ) { buy( 1, $item[Doc Galaktik's Ailment Ointment] ); } use( 1, $item[Doc Galaktik's Ailment Ointment] );
If you are not sure how the two different methods of defining an if statement work, it's probably best to always use curly braces. Defining an if statement without curly braces doesn't have any special merits - you can always create the same if statement by putting curly braces around the single action.
What if I want to use more than one Ailment Ointment? Using them one by one can be pretty annoying. It is possible to pass an argument to the script itself, but when doing so, you can no longer just put down a list of actions. You will have to define a function main. You can define main to take any amount of arguments of any type you like. You can also define it to return any type you like. But for the healing script, return type void and a single argument of the type int would probably work best. If you define a function main, don't put any actions outside of the functions - they will be ignored. Let's start by putting the healing script we just made into a function main without any alterations. This is an equivalent script again, but this time with a function main.
void main() { if ( item_amount( $item[Doc Galaktik's Ailment Ointment] ) < 1 ) { buy( 1, $item[Doc Galaktik's Ailment Ointment] ); } use( 1, $item[Doc Galaktik's Ailment Ointment] ); }
Back to why we made the function main in the first place - passing an argument. We want to have our healing script consume a custom amount of Ailment Ointments. We will have main take an integer as an argument and use that value to determine how many Ailment Ointments to use. When the script runs, it will ask you what value you want for that argument. If we have 3 in our inventory and the argument tells us to use 5, we will have to purchase extra Ailment Ointments - but only 2, since we already have 3. So let's calculate the required amount of Ailment Ointments and purchase exactly that many.
void main( int amount ) { if ( item_amount( $item[Doc Galaktik's Ailment Ointment] ) < amount ) { buy( amount - item_amount( $item[Doc Galaktik's Ailment Ointment] ), $item[Doc Galaktik's Ailment Ointment] ); } use( amount, $item[Doc Galaktik's Ailment Ointment] ); }
I don't know about you guys, but I'm lazy. Using multiple Ointments at once is an improvement, but typing in how many Ailment Ointments to use each time can still get pretty annoying. The wiki tells me the Ailment Ointment cures 8-10 HP, maybe I can use that information to calculate the amount of Ointments to use automatically. I'll rename main to use_galaktik, and create a new function main that calculates how many Ointments to use and calls use_galaktik with this amount. Two new functions are required for this: my_hp() and my_maxhp(). Both take no arguments and return an integer containing your current HP and your maximum HP, respectively. Also, I'm introducing the while statement. The while statement is used in the same way as an if statement, but if the condition is evaluated to be true and the while actions are executed, the condition is evaluated again, and the while actions will be executed as long as the conditions are true. Remember that a while loop will not check its condition in the middle of its actions, the condition is only checked between each full execution of the actions.
Example:
int i = 0; while( i < 6) { i = i + 1; i = i + 1; i = i + 1; i = i + 1; } return i;
At the end of the above small script, i will be 8, NOT 6. i will only be checked after each line within the while loop has been executed.
Now, to use my_hp() and my_maxhp() to automatically calculate the correct amount of Ailment Ointments to use:
void use_galaktik( int amount ) { if ( item_amount( $item[Doc Galaktik's Ailment Ointment] ) < amount ) { buy( amount - item_amount( $item[Doc Galaktik's Ailment Ointment] ), $item[Doc Galaktik's Ailment Ointment] ); } use( amount, $item[Doc Galaktik's Ailment Ointment] ); } void main() { while( my_maxhp() > my_hp() ) { int toheal = my_maxhp() - my_hp(); int amount = toheal / 10; if ( amount == 0 ) { amount = 1; } use_galaktik( amount ); } }
The if ( amount == 0 ) check is in there because if you divide a number, the result will always be rounded down. Since we're in the while loop, our hp is still lower than our maxhp, and it is a good idea to use at least one Ailment Ointment, or we'll be stuck inside the loop forever.
Now say you have *plenty* of maximum hp, and you don't mind missing out on just a few points of health. In the case that an Ailment Ointment would potentially heal you more than you need, you feel your hp is so close to its max that you don't need to heal anymore, the script should just end. You can make the while loop stop executing with a break statement. A break statement will stop the execution of a while loop, and continue where it would if the while would fail its condition check. Note that with a while inside a while, the innermost while would be cancelled, but the outer while would continue execution.
void use_galaktik( int amount ) { if ( item_amount( $item[Doc Galaktik's Ailment Ointment] ) < amount ) { buy( amount - item_amount( $item[Doc Galaktik's Ailment Ointment] ), $item[Doc Galaktik's Ailment Ointment] ); } use( amount, $item[Doc Galaktik's Ailment Ointment] ); } void main() { while ( my_maxhp() > my_hp() ) { int toheal = my_maxhp() - my_hp(); int amount = toheal / 10; if ( amount == 0 ) { break; } use_galaktik( amount ); } }
I don't know about you guys, but I'm cheap and lazy. I'm too lazy to change the healing script I use all the time, but I'm too cheap to use Ailment Ointments when I could be using Medicinal Herbs. There is a function that allows me to check my class, namely, my_class(). If I use my_class() to check whether my class is a Muscle class, I can determine whether I am capable of using Medicinal Herbs instead.
void use_galaktik( int amount ) { if ( item_amount( $item[Doc Galaktik's Ailment Ointment] ) < amount ) { buy( amount - item_amount( $item[Doc Galaktik's Ailment Ointment] ), $item[Doc Galaktik's Ailment Ointment] ); } use( amount, $item[Doc Galaktik's Ailment Ointment] ); } void heal_galaktik() { while ( my_maxhp() > my_hp() ) { int toheal = my_maxhp() - my_hp(); int amount = toheal / 10; if ( amount == 0 ) { break; } use_galaktik( amount ); } } void heal_herbs() { if ( item_amount( $item[Medicinal Herb's medicinal herbs] ) < 1 ) { buy( 1, $item[Medicinal Herb's medicinal herbs] ); } use( 1, $item[Medicinal Herb's medicinal herbs] ); } void main() { boolean useHerbs = false; if( my_class() == $class[turtle tamer] ) { useHerbs = true; } if ( my_class() == $class[seal clubber] ) { useHerbs = true; } if( useHerbs ) { heal_herbs(); } else { heal_galaktik(); } }
Nin nin woosh. Now that is one nifty healing script, isn't it? Of course I forgot one final thingy: Accordion Thieves. Those can use Medicinal Herbs as well. Then again, they need to be level 9 to do so. Luckily, the function my_level() will tell us whether our Accordion Thief is level 9 or not. To make this check, I will use the AND condition, which is typed as "&&". This condition is true if and only if both sides evaluate to true. I'll also put the former two checks into an OR condition. The OR condition is typed as "||" and evaluates to true if either of the sides evaluates to true. The final script looks as follows:
void use_galaktik( int amount ) { if ( item_amount( $item[Doc Galaktik's Ailment Ointment] ) < amount ) { buy( amount - item_amount( $item[Doc Galaktik's Ailment Ointment] ), $item[Doc Galaktik's Ailment Ointment] ); } use( amount, $item[Doc Galaktik's Ailment Ointment] ); } void heal_galaktik() { while( my_maxhp() > my_hp() ) { int toheal = my_maxhp() - my_hp(); int amount = toheal / 10; if ( amount == 0 ) { break; } use_galaktik( amount ); } } void heal_herbs() { if ( item_amount( $item[Medicinal Herb's medicinal herbs] ) < 1 ) { buy( 1, $item[Medicinal Herb's medicinal herbs] ); } use( 1, $item[Medicinal Herb's medicinal herbs] ); } void main() { boolean useHerbs = false; if ( my_class() == $class[turtle tamer] || my_class() == $class[seal clubber] ) { useHerbs = true; } if ( my_class() == $class[accordion thief] && my_level() >= 9 ) { useHerbs = true; } if ( useHerbs ) { heal_herbs(); } else { heal_galaktik(); } }
A map is indexed by one data type (the key) and associates that key with another (or the same) data type (the value). The key can be any ASH simple data type: boolean, int, float, string, item, zodiac, location, class, stat, skill, effect, familiar, slot, or monster. The value can be any of those or can be an aggregate: another map. This effectively allows multi-dimensional maps and. In fact, that's how the syntax we provide for multi-dimensional aggregates actually operates: maps of maps of maps ...
You can declare a map any time you can declare a variable: as a top level (global) variable, as a function parameter, or as a local variable in any scope.
You can fetch data from a map any time you can provide a data value: in an expression, as a function parameter, on the right side of an assignment statement, from a "return" statement, as so on. You can pass around entire maps, individual elements, or intermediate maps: "slices".
If you use a map on the left side of an assignment, you set the whole map at once to the new value. If you specify a map and a complete set of indices (of the correct types) on the left side of an assignment statement, you set a single element. If you specify a map and a prefix of indices (of the correct type), you directly set one of the intermediate maps, a "slice".
The syntax for declaring the data type of a map:
<data type> [ <key type>, ... ]The syntax for referencing an element (or slice) of a map:
<variablename>[ <key expression>, ... ]All the key expressions will be evaluated at run time. If you specify all the keys the map expects, you fetch data of the type specified by the map. If you specify fewer keys than the map expects, you get an intermediate map, a "slice".
As an example:
boolean [string, string] props;might be used to hold "properties" associated with names.
props[ "dog", "mammal" ] = true; props[ "dog", "pet" ] = true; props[ "dog", "fun" ] = false; props[ "turtle", "mammal" ] = false; props[ "turtle", "pet" ] = true; props[ "turtle", "fun" ] = false; props[ "aardvark", "mammal" ] = true; props[ "aardvark", "pet" ] = false; props[ "aardvark", "fun" ] = true;references:
props[ "dog", "mammal"] => true boolean [string] animal = props[ "turtle" ]; animal[ "fun" ] => falseYou can test the presence of a key in a map using the "contains" operator:
<aggregate reference expression> contains <key expression>Where <aggregate reference expression> must evaluate at run time to a map or slice, and
must evaluate at run time to a key of the appropriate type. (Note that that is enforced at parse time; ASH can tell the datatype any expression will produce). props contains "dog" => true props contains "elephant" => false props[ "aardvark" ] contains "fun" => true animal contains "pet" => true animal contains "favorite food" => falseYou can remove a key-value association from a map using the "remove" unary operator:
remove <aggregate reference>For clarification, an aggregate reference is "<map name>[ <index 1> ... <index n> ]" where <map name>[ <index 1> ... <index n-1> ] specifies the "slice" and <index n> specifies the "key". Which is just what you expect, if you fully specify the indices; for a single dimensional map, "map[10]" -> "map" is the slice and 10 is the key. The "remove" operator removes the "key" from the "slice". For example:
string [int] map1; map1[5] = "foo"; print( count( map1 ) + " " + map1 contains 5 + " " + map1[5] ); print( "remove: " + remove map1[5] ); print( count( map1 ) + " " + map1 contains 5 + " " + map1[5] ); print( "remove: " + remove map1[5] ); int [string, string] map2; map2["me","you"] = 17; print( count( map2["me"] ) + " " + map2["me"] contains "you" + " " + map2["me","you"] ); print( "remove: " + remove map2["me", "you"] ); print( count( map2["me"] ) + " " + map2["me"] contains "you" + " " + map2["me","you"] ); print( "remove: " + remove map2["me", "you"] ); print( count( map2 ) + " " + map2["me"] ); print( "remove: " + remove map2["me"] ); print( count( map2 ) + " " + map2["me"] );yields:
1 true foo remove: foo 0 false remove: 1 true 17 remove: 17 0 false 0 remove: 0 1 aggregate int [string] remove: aggregate int [string] 0 aggregate int [string]