Example script: acquire_familiars

I was writing a script to obtain any mall-purchasable familiars I was missing, and realized that while it was fairly short and simple it was also using a fairly wide array of functions and would make a great example script. I noticed a couple people here lately who seem new to coding/scripting in general, so I added some comments.

Below, and attached, is acquire_familiars.ash. It will purchase and grow any familiars you don't have that cost no more than your autoBuyPriceLimit, and will display any other familiars that are under 10M for you to consider manually purchasing. It is heavily commented with the intent of being a learning tool for those new to ASH.

Code:
// acquire_familiars.ash
// Notepad++ with the 'ash' user defined language HIGHLY recommended!
// Get Notepad++ from https://notepad-plus-plus.org/.
// Get the ASH user-defined language mod from Bale's post at: http://kolmafia.us/showthread.php?3164-How-do-you-write-an-ASH-file

// Automaticaly obtain any purchasable familiars that are less expensive than your autoBuyPriceLimit setting.
void acquire_familiars() {
	// can_interact() returns TRUE if we can interact with other players and the mall.
	if( !can_interact() ) {
		abort("This script is useless in-run!");
	}
	// Note this script does not actually check for mall access - it will fail for new accounts under a certain level.
	// We don't worry about it because some edge cases really just aren't worth accounting for.
	
	boolean[familiar] expensives;
	boolean[familiar] moderates;
	// get_property(prop) always returns a string. It needs to be converted to the appropriate data type.
	// Note that to_int( get_property("autoBuyPriceLimit") ) would also work. I generally prefer dot notation for type casting.
	int buy_limit = get_property("autoBuyPriceLimit").to_int();
	
	// Loop through all existing familiars. 'fam' will reference each one in turn.
	foreach fam in $familiars[] {
	
		// have_familiar( familiar ) is a method that returns TRUE if you already own the referenced familiar.
		// So, "!have_familiar(fam)" does the opposite; TRUE only if you DON'T own the familiar.
		// It could also be written as "!fam.have_familiar()".
		// fam.hatchling is a property that returns the item the familiar is obtained from.
		// item.tradeable returns TRUE if it can be purchased from the mall.
		if( !have_familiar(fam) && fam.hatchling != $item[none] && fam.hatchling.tradeable ) {
			// historical_price( item ) returns Mafia's stored price of an item, and will never check the mall.
			// mall_price( item ) checks the mall for the price, but no more than once per day.
			// Here we use the historical price if it's available, and check the mall only if it's not (returns 0).
			int p = fam.hatchling.historical_price();
			if( p <= 0 ) p = fam.hatchling.mall_price();
			if( p <= 0 ) continue; // If the price is still 0, it isn't available from the mall for some reason.
			
			if( p <= buy_limit ) {
				// We use retrieve_item first in case we have one somewhere grabbable, i.e. hagnk's or a clan stash.
				// retrieve_item will also purchase the item from the mall IF autoSatisfyWithMall is on.
				// It will abort the script if it fails UNLESS we grab its return value, so we store it
				// in 'nothing' because we don't actually need it.
				boolean nothing = retrieve_item(1, fam.hatchling);
				// If we didn't get the item and autoSatisfyWithMall is off, it's because we didn't even try to mallbuy.
				if( item_amount(fam.hatchling) < 1 && ! get_property("autoSatisfyWithMall").to_boolean() ) {
					buy(1, fam.hatchling);
				}
				// If we still didn't get the item, we probably don't have any meat left. Abort.
				// Note that our 'if' doesn't need curly braces {} if we're only executing one statement.
				if( item_amount(fam.hatchling) < 1 )
					abort("Failed to acquire " + fam.hatchling.to_string() + " - do you have enough meat?");
				
				// When I first wrote this, I used 'use', but it turns out there are hatchlings that have a different
				// function when you use them. So instead I have to visit the 'grow' URL directly.
				// I had to find this URL by looking at the [grow] link next to the item on the inventory page.
				// Turned out it calls inv_familiar.php with the property "which" set to "3", and "whichitem" to the
				// ID of the item in question. You can obtain an item's id by running it through to_int().
				// Note that it returns the entire page, but we do NOT catch it this time; this is because we don't
				// need it and would rather the script go ahead and abort if it fails for any reason.
				visit_url("inv_familiar.php?pwd="+my_hash()+"&which=3&whichitem="+fam.hatchling.to_int().to_string());
				//use( 1, fam.hatchling ); 
			
			// Below we deal with any familiars we aren't purchasing.
			
			// Prices above over 10M indicate Mr. Store or other expensive fams. We don't track those.
			} else if( p > 10000000 ) {
				continue;
			
			// If the price is between 2M and 10M, we'll display it as 'Expensive' for possible manual purchase.
			} else if( p > 2000000 ) {
				expensives[fam] = true;
			
			// If the price is below 2M but above autoBuyPriceLimit, we'll display it as 'Moderate' for possible manual purch.
			} else {
				moderates[fam] = true;
			}
		}
	}
	
	// Because we're visiting the URLs directly Mafia may not always know what all we've grown, so let's 
	// refresh the terrarium, just in case.
	cli_execute("refresh familiars");
	
	// If we tracked any expensive familiars, display them.
	if( expensives.count() >= 1 ) {
		print("");
		print("--- Expensive Familiars ----");
		foreach fam in expensives {
			// Print the familiar, its hatchling, and the hatchling's price.
			// The to_string function can accept a Java formatting argument. The example below adds separating commas to an integer.
			// More examples of Java string formatting can be found at: https://dzone.com/articles/java-string-format-examples
			print( fam.to_string() + ", " + fam.hatchling.to_string() + ": ~" + fam.hatchling.historical_price().to_string("%,d") + " meat" );
		}
	}
	
	// If we tracked any moderate familiars, display those too.
	if( moderates.count() >= 1 ) {
		print("");
		print("--- Moderate Familiars ----");
		foreach fam in moderates {
			print( fam.to_string() + ", " + fam.hatchling.to_string() + ": ~" + fam.hatchling.historical_price().to_string("%,d") + " meat" );
		}
	}
}

// When this script is called from the CLI, Mafia always looks for a main() function and runs that.
// If we import this into another script, however, we'll want to be able to call it easily.
// Therefore the primary use of this script is written as a separate function, which is called from main().
void main() {
	acquire_familiars();
}
 

Attachments

  • acquire_familiars.ash
    6 KB · Views: 34
Last edited:

lostcalpolydude

Developer
Staff member
Code:
				// When I first wrote this, I used 'use', but it turns out there are hatchlings that have a different
				// function when you use them. So instead I have to visit the 'grow' URL directly.
				// I had to find this URL by looking at the [grow] link next to the item on the inventory page.
				// Turned out it calls inv_familiar.php with the property "which" set to "3", and "whichitem" to the
				// ID of the item in question. You can obtain an item's id by running it through to_int().
				// Note that it returns the entire page, but we do NOT catch it this time; this is because we don't
				// need it and would rather the script go ahead and abort if it fails for any reason.
				visit_url("inv_familiar.php?pwd="+my_hash()+"&which=3&whichitem="+fam.hatchling.to_int().to_string());
				//use( 1, fam.hatchling );
While the game will always include which=X, that part of the URL is not actually required. You can probably include ajax=1 in the URL to not return the page (which should be slightly more server friendly, though you shouldn't be loading the URL enough to really matter), though that would require testing to make sure it works.

For extra safety,
Code:
buy(1, fam.hatchling);
could be
Code:
buy(1, fam.hatchling, buy_limit);
This would prevent purchasing the hatchling if its price has suddenly gone up a lot since historical_price() was last updated. I suppose that would also mean updating other parts of the script in case this purchase actually fails.

That's just some minor nitpicking of a script that looks pretty good.
 
While the game will always include which=X, that part of the URL is not actually required. You can probably include ajax=1 in the URL to not return the page (which should be slightly more server friendly, though you shouldn't be loading the URL enough to really matter), though that would require testing to make sure it works.

Thanks for this info; I'm not going to bother to update this - it's a teaching script, and I just wanted to let new people know how I got there and why - this is good to understand. I might do that in some other scripts of my own.

Code:
buy(1, fam.hatchling, buy_limit);
This would prevent purchasing the hatchling if its price has suddenly gone up a lot since historical_price() was last updated.

But since buy_limit is literally just the autoBuyPriceLimit setting, doesn't "buy" already default to this? Or is it only retrieve_item() that fails if it can't purchase below the limit?
 

Darzil

Developer
But since buy_limit is literally just the autoBuyPriceLimit setting, doesn't "buy" already default to this? Or is it only retrieve_item() that fails if it can't purchase below the limit?
Buy and autoBuy are different things. autoBuyPriceLimit comes in when using Acquire to get things, but Buy is explicit, and buys a thing regardless of the limit for acquire.
 

Theraze

Active member
Two aliases to do similar with less lines.
buyfamiliars => ashq foreach i in $familiars[] {if (!have_familiar(i) && mall_price(i.hatchling) > 0 && mall_price(i.hatchling) <= %%) { buy(1, i.hatchling, mall_price(i.hatchling)); use(1, i.hatchling); } }
listfamiliars => ashq foreach i in $familiars[] {if (!have_familiar(i) && mall_price(i.hatchling) > 0 && mall_price(i.hatchling) <= %%) print("You don't have "+i+" and it would cost about "+mall_price(i.hatchling)+" to fix that");}
Run listfamiliars 100000 to see all unowned familiars under 100k. Run buyfamiliars 100000 to purchase and grow them. There's also a get version I can post if anyone actually cares.
 

VladYvhuce

Member
Hmm... So... What would I need to trim it down to if I just wanted to check familiar prices, without the buying function?
 

Theraze

Active member
Assuming that Vlad wanted to run the acquire_familiars script to categorize things without actually buying rather than ignoring my post, but... :)
 

VladYvhuce

Member
I'm not sure how to implement those. Everything I've tried results in "Unable to invoke listfamiliars". Whatever I'm doing, Mafia doesn't like it.
 

Pazleysox

Member
I'm not sure how to implement those. Everything I've tried results in "Unable to invoke listfamiliars". Whatever I'm doing, Mafia doesn't like it.

Edit the code, and make this change:
PHP:
			// If the price is between 2M and 10M, we'll display it as 'Expensive' for possible manual purchase.
			} else if( p > 2000000 ) {

Change the 2000000 to 100, like this:

PHP:
			// If the price is between 2M and 10M, we'll display it as 'Expensive' for possible manual purchase.
			} else if( p > 100 ) {

Then the script will display all familiars with a cost between 100 meat, and 10,000,000 meat. If you want to see ALL familiars available, regardless of cost, change this:
PHP:
			// Prices above over 10M indicate Mr. Store or other expensive fams. We don't track those.
			} else if( p > 10000000 ) {

to

PHP:
			// Prices above over 10M indicate Mr. Store or other expensive fams. We don't track those.
			} else if( p > 999999998 ) {

That sets the cost to 999,999,998 which is the most expensive item that can be displayed in a mall search (I believe this was changed from all 9's years ago)

Then just run the script! I tested it myself, and it worked perfect.
 

VladYvhuce

Member
Edit the code, and make this change:
PHP:
			// If the price is between 2M and 10M, we'll display it as 'Expensive' for possible manual purchase.
			} else if( p > 2000000 ) {

Change the 2000000 to 100, like this:

PHP:
			// If the price is between 2M and 10M, we'll display it as 'Expensive' for possible manual purchase.
			} else if( p > 100 ) {

Then the script will display all familiars with a cost between 100 meat, and 10,000,000 meat. If you want to see ALL familiars available, regardless of cost, change this:
PHP:
			// Prices above over 10M indicate Mr. Store or other expensive fams. We don't track those.
			} else if( p > 10000000 ) {

to

PHP:
			// Prices above over 10M indicate Mr. Store or other expensive fams. We don't track those.
			} else if( p > 999999998 ) {

That sets the cost to 999,999,998 which is the most expensive item that can be displayed in a mall search (I believe this was changed from all 9's years ago)

Then just run the script! I tested it myself, and it worked perfect.
Yeah. They changed it from all 9s to make max-priced items not show up, in case you want to save an item for a friend or to make people not be as lazy or something.

Add alias to the front of the lines given to add them as aliases
Thanks.

By combining the knowledge from both responses, now I have a new Daily Deed button that checks for the familiars I don't have.
 

VladYvhuce

Member
I just noticed something. This method is flawed. It doesn't display the lowest mall price, which means that it's useless for me.
 

lostcalpolydude

Developer
Staff member
I just noticed something. This method is flawed. It doesn't display the lowest mall price, which means that it's useless for me.

You already know that your complaint is about KoLmafia, not this script, because you made this thread.

On top of that, this script exists mostly so people can read the code and understand the ideas. Your post comes across as insulting, really.
 

VladYvhuce

Member
Did you think this script could be used as a Mallbot, or some type of pricing bot?
No. I was just not expecting it to tell me that I should buy a max-priced familiar when there's a more reasonably priced one in the mall. As a familiar collector, that gets frustrating.
 

fronobulax

Developer
Staff member
No. I was just not expecting it to tell me that I should buy a max-priced familiar when there's a more reasonably priced one in the mall. As a familiar collector, that gets frustrating.

Actually I don't see a problem with what you are saying. If it displays a price you like and you try to buy it you will spend less that you planned. Perhaps I am mis-remembering mafia's behavior but my recollection is that it will buy the cheapest available regardless of what was displayed as the "five price".
 

Pazleysox

Member
No. I was just not expecting it to tell me that I should buy a max-priced familiar when there's a more reasonably priced one in the mall. As a familiar collector, that gets frustrating.

It's not telling you to buy anything. This script is for information only, or to purchase low cost familiars for people who don't have them. If you are a (familiar) collector, you would have little use for this (or any) script, as you should already have done your homework, know what you are missing, how much it cost, and what you're willing to pay.
 

VladYvhuce

Member
It's not telling you to buy anything. This script is for information only, or to purchase low cost familiars for people who don't have them. If you are a (familiar) collector, you would have little use for this (or any) script, as you should already have done your homework, know what you are missing, how much it cost, and what you're willing to pay.
New famiars tend to come out with various types of content, and sometimes someone who's had one for sale at max price decides "eh, I'll try and slash the price some". The idea of catching these price lowerings through Mafia is a very tempting idea, as opposed to running multiple mall searches and sifting through the entire mall when a new familiar shows up. Or waiting for the wiki to update as people add content data... As the Trade channel shows quite often, collectors are rather lazy. I was just hoping this thing would give me an edge. It's clear that it won't. So, I'm stuck with going back to the old methods.
 

fronobulax

Developer
Staff member
The idea of catching these price lowerings through Mafia is a very tempting idea, as opposed to running multiple mall searches and sifting through the entire mall when a new familiar shows up.

That is as good a definition of a mallbot as any I have read.
 
Top