BatBrain -- a central nervous system for consult scripts

Yeah, I've changed all (or almost all) instances of monster_stat() functions with
Code:
(my_monster != last_monster()) ? monster_stat(my_monster) : monster_stat()
and defined my_monster as last_monster(), so IF I want to test something at least I only need to change one thing in the script :)
 
Well, yes. But with monster_stat I meant all instances of monster_hp, monster_defense and monster_attack as well as monster_element which are used on various places in the script. The script also uses last_monster() for certain checks in the script and I changed those as well.
 
My finishing attack script has evolved some more in a direction I find troubling. Quite frankly BatBrain's macro building commands aren't as flexible as I'd like so I find myself calling macro(string mac) directly, like this:

PHP:
string dangermacro() {
	string act = batround();
	if(get_action($skill[entangling noodles]).id != "")
		act += get_action($skill[entangling noodles]).id+"; call batround; ";
	act += "sub main; "+dangeraction().id+"; call batround; endsub; call main; repeat";
	return macro(act);
}

That does exactly what I want which the other macro() commands just cannot do. I often want to execute a sequence of commands (or perhaps just entangling noodles) followed by a repeating finisher.

I was happy to figure out that hitchance(1) will tell me if "attack" is likely to hit. I've used that to trigger my danger action. I love BatBrain's build in functions!
 
It looks like conditions like this one are often checked:
PHP:
if(get_action($skill[entangling noodles]).id != "")
Maybe something like this would save some typing ?
PHP:
boolean can_use( string whichid) { return (get_action(whichid).id != ""); }
boolean can_use( item it ) { return (get_action(it).id != ""); }
boolean can_use( skill sk ) { return (get_action(sk).id != ""); }

I'm probably completely off though, I haven't delved into batbrain enough yet.
 
Actually, that sounds like a good idea to me slyz. I use that a lot. Here's another section I just wrote for my FinalAttack:

PHP:
string mac = batround();
	case $location[8-Bit Realm]:
		int dam = m_regular() + (item_amount($item[blue shell]) > 0? 25: 0);
		if(get_action($skill[entangling noodles]).id != "" && dam > my_hp()) {
			mac += get_action($skill[entangling noodles]).id+"; call batround; ";
			dam -= m_regular();
		}
		if(get_action($item[blue shell]).id != "" && my_hp() > dam)
			mac += get_action($item[blue shell]).id+"; call batround; ";
		mac += "sub main; "+get_action("attack").id+"; call batround; endsub; call main; repeat";
		return macro(mac);
 
Last edited:
Bale, how is what you're doing different from queueing the actions, and adding the repeating finisher as the postact parameter of macro(string, string)? You're doing unnecessary work again. :)

@slyz: The main idea behind opts is that it is a sortable map of all your combat actions. You should be able to sort it by the qualities you want in an action, and perform the top action in the list. Need to restore X mp? Sort opts by -profit and then by -min(X,value.mp). I didn't want to go in a piecewise direction with it. I'll hold off on those functions for a bit. As far as I'm concerned, BatBrain is not ready for public use and this thread is premature.
 
Last edited:
Bale, how is what you're doing different from queueing the actions, and adding the repeating finisher as the postact parameter of macro(string, string)? You're doing unnecessary work again. :)

LOL! I'm an idiot! How's this look?

PHP:
string dangermacro() {
	if(get_action($skill[entangling noodles]).id != "")
		queue[count(queue)] = get_action($skill[entangling noodles]);
	return macro("", "sub main; "+dangeraction().id+"; call batround; endsub; call main; repeat");
}



And this?

PHP:
	case $location[8-Bit Realm]:
		int dam = m_regular() + (item_amount($item[blue shell]) > 0? 25: 0);
		if(get_action($skill[entangling noodles]).id != "" && dam > my_hp()) {
			queue[count(queue)] = get_action($skill[entangling noodles]);
			dam -= m_regular();
		}
		if(get_action($item[blue shell]).id != "" && my_hp() > dam)
			queue[count(queue)] = get_action($item[blue shell]);
		return macro("", "sub main; "+get_action("attack").id+"; call batround; endsub; call main; repeat");

Actually, there is some greater flexibility to writing this way. Time to refactor some code!
 
Refactoring complete! I have now successfully compressed all my CCSs into three lines.

[ default ]
consult SmartStasis.ash
consult FinalAttack.ash

FinalAttack.ash is not perfect, but it does contain all the information I was previously using in several CCSs. And it is so darn tidy! Check out spamAttack at the top -- it brings consult macro development full circle back to the beginning.

PHP:
import "BatBrain.ash";

string spamAttack(advevent finisher) {
	if(finisher.id == "") abort("Invalid finishing attack!");
	return macro("", "sub main; "+finisher.id+"; call batround; "+"endsub; call main; repeat");
}

advevent dangeraction() {
	// Check shieldbutt, moxious maneuver, saucegeyser, attack
	foreach da in $strings[skill 2005, skill 7008, skill 4012, attack]
		if(get_action(da).id != "") return get_action(da);
	return new advevent;    // you should never reach this
}

string splashSauce() {
	string preact = "mark SauceLoop";
	if(queue[0] == get_action($skill[entangling noodles])) {
		queue.clear();
		preact = get_action($skill[entangling noodles]).id +"; call batround;" + preact;
	}
	queue[count(queue)] = get_action($skill[Wave of Sauce]);
	queue[count(queue)] = get_action($skill[Saucegeyser]);
	return macro(preact, "goto SauceLoop");
}

boolean can_splash() {
	if(my_class() != $class[sauceror] || !have_skill($skill[Wave of Sauce]) || !have_skill($skill[Saucegeyser])
	  || !(have_effect($effect[Jabañero Saucesphere]) >0 || have_effect($effect[Jalapeño Saucesphere]) >0)
	  || dmg_dealt(get_action($skill[Wave of Sauce]).dmg) >= monster_stat("hp"))
		return false;
	switch(normalize_dmgtype("sauce")) {
	case "hot": return numeric_modifier("spell damage") + numeric_modifier("hot spell damage") >= 25;
	case "cold": return numeric_modifier("spell damage") + numeric_modifier("cold spell damage") >= 25;
	}
	return numeric_modifier("spell damage") >= 25;
}

string take_action(monster foe, string page) {
	float dam = m_regular();
	
	boolean stun;
	void cast_noodles() {
		if(stun || get_action($skill[entangling noodles]).id == "") return;
		queue[count(queue)] = get_action($skill[entangling noodles]);
		stun = true;
	}
	if((dam > my_hp()/ 3 || $monsters[Uppercase Q, Quantum Mechanic] contains foe)
	  && !(foe == $monster[a.m.c. gremlin] && item_amount($item[divine champagne popper]) > 0) )
		cast_noodles();
		
	switch(foe) {
	case $monster[a.m.c. gremlin]:
		foreach da in $items[divine champagne popper, tattered scrap of paper]
			if(get_action(da).id != "")
				return get_action(da);
		break;
	case $monster[mother hellseal]:
		return spamAttack(get_action($skill[lunging thrust-smack]));
	case $monster[naughty sorority nurse]:
		if(my_mp() >= 6+ my_level() * 2 && item_amount($item[frigid ninja stars]) > 0) {
			string [int] rave_stun = split_string(get_property("raveCombo3"), ",");
			foreach key,rave in rave_stun
				queue[count(queue)] = get_action(to_skill(rave));
			queue[count(queue)] = get_action($skill[Moxious Maneuver]);
			queue[count(queue)] = get_action($skill[Moxious Maneuver]);
			queue[count(queue)] = get_action($item[frigid ninja stars]);
			return macro();
		}
		abort();
	case $monster[Candied Yam Golem]:
	case $monster[Malevolent Tofurkey]:
	case $monster[Possessed Can of Cranberry Sauce]:
	case $monster[Stuffing Golem]:
	case $monster[Inebriated Tofurkey]:
	case $monster[Hammered Yam Golem]:
	case $monster[Plastered Can of Cranberry Sauce]:
	case $monster[Soused Stuffing Golem]:
	case $monster[El Novio Cadáver]:
	case $monster[El Padre Cadáver]:
	case $monster[La Novia Cadáver]:
	case $monster[La Persona Inocente Cadáver]:
		if(can_splash())
			return splashSauce();
		return spamAttack(dangeraction());
	// In Fernswarthy's Basement
	case $monster[the ghost of fernswarthy n great-grandfather]:
	case $monster[a n stone golem]:
		for it from 3755 to 3759
			if(item_amount(to_item(it)) > 3) {
				queue[count(queue)] = get_action(to_item(it));
				queue[count(queue)] = get_action(to_item(it));
			}
		return macro("mark lovesongLoop", "goto lovesongLoop");
	case $monster[the beast with n ears]:
	case $monster[the beast with n eyes]:
	case $monster[a n-headed hydra]:
	case $monster[n bottles of beer on a golem]:
	case $monster[a n-dimensional horror]:
		for it from 3118 to 3120
			if(item_amount(to_item(it)) > 3) {
				queue[count(queue)] = get_action(to_item(it));
				queue[count(queue)] = get_action(to_item(it));
				return macro("");
			}
		for it from 3754 to 3759
			if(item_amount(to_item(it)) > 3) {
				queue[count(queue)] = get_action(to_item(it));
				queue[count(queue)] = get_action(to_item(it));
				return macro("");
			}
		return macro("mark divinesongLoop", "goto divinesongLoop");
	}

	switch(my_location()) {
	case $location[8-Bit Realm]:
		if(get_action($item[blue shell]).id != "" && my_hp() > 25) {
			if(dam +25 > my_hp())
				cast_noodles();
			if(my_hp() > (stun? 25: 25 + dam))
				queue[count(queue)] = get_action($item[blue shell]);
		}
		return spamAttack(get_action("attack"));
	case $location[convention hall lobby]:
		cast_noodles();
		queue[count(queue)] = get_action($skill[wave of sauce]);
		queue[count(queue)] = get_action($skill[candyblast]);
		return spamAttack(get_action($item[bottle of Gü-Gone]));
		break;
	}
	if(can_splash())
		return splashSauce();
	else if(hitchance(1) < .2)
		return spamAttack(dangeraction());
	return spamAttack(get_action("attack"));
}

void main(int initround, monster foe, string page) {
	page = act(page);
	page = take_action(foe, page);
}

zarqon: Have I finally managed to successfully use Batbrain ?
 
Last edited:
That looks pretty sweet. Unfortunately your basement section won't work because, like your shadow, mafia doesn't ambiguate those monsters yet. You can match basement monsters in your regular CCS, but not in a consult script -- last_monster() will return $monster[none]. I've already submitted that bug report.

Expect some shiny new functions in the next BatBrain update:

enqueue(advevent)
enqueue(advevent, string repeatcondition)
enqueue(advevent, string pre, string post)

Those should let you make your script simpler still.
 
Huh. It doesn't? Pretty strange. Mafia recognizes those monsters as constants, so I just assumed it would recognize those monsters.

enqueue(advevent)
enqueue(advevent, string repeatcondition)
enqueue(advevent, string pre, string post)

Ooooooooooooooooooooooooooooooooooooooooooooo!

That's just what I've been wanting. The last one will even allow me to instert if conditions, while loops, or the like. I guess I won't need spamAttack() anymore. ;)
 
Last edited:
So, I don't think I'll get any closer to something good without comments now so here it is. My latest version of spamattack that utilises BatBrain for a lot of things. Please give me ideas on what to do and how to make it more efficient.
 
I guess this is as good a place as any to ask this: Is there a way to get a consult-script to call itself if the monster isn't dead when it has ran once?
 
Maybe with a cli_execute() ? But that seems really awkward. Why not simply avoid exiting the consult script in the first place?
 
Yes... I guess I could do a while (monster_health > 0) -loop somewhere.

That won't work because of monster ML variance. Sometimes the monsters health goes slightly negative according to mafia. You'd have to check for win text. "WINWINWIN" However, I wouldn't do that since...


Is there a way to get a consult-script to call itself if the monster isn't dead when it has ran once?

Well that isn't really a problem is it? Unless things changed somewhere, the final line of a CCS will be repeated until the monster is dead. That means your script will be called until mafia detects death or it fails to execute an action.
 
If you're using monster_stat("hp"), it's capped on the low end at 1.

But you don't actually need to parse combat completion, just assume the monster is always alive. It's been my experience that mafia won't execute further combat actions once the combat is over. You can check the round variable to see where you are in the combat, and if it's 0 the combat is over.

If you're executing all of your actions using BatBrain functions like macro(), it automatically sets round to maxround if it's 0. So you can just do this:

PHP:
while (round < maxround) {
   // call my consulty goodness
}
 
I went with:
Code:
repeat{ ... }until(contains_text(page, "WINWINWIN") || page == "");

That works with "real" fights and testing where I supply the html of a fight page to the script in order to see what would happen (just having the winwinwin there made it never stop since 1 pass through the script emptied "page".

Also, I've had a couple of instances where the script ran once and then the fight aborted and the monster had still ~5HP left due to monster and spell variance.
 
Just to share, this is what has happened to my FinalAttack with the implementation of BatBrain's enqueue() command. It's bloody brilliant how easy it makes writing macros. I think that this makes a great example of how to use BatBrain. It is just complicated enough to show some interesting techniques, but reasonably short and simple enough not to become too overwhelming to a beginning scripter.

PHP:
import "BatBrain.ash";

advevent dangeraction() {
	// Check shieldbutt, moxious maneuver, saucegeyser, attack
	foreach da in $strings[skill 2005, skill 7008, skill 4012, attack] {
		if(get_action(da).id != "" && !(da == "skill 7008" && hitchance(0) < .8))
			return get_action(da);
	}
	return new advevent;    // you should never reach this
}

string splashSauce() {
	enqueue(get_action($skill[Wave of Sauce]), "mark SauceLoop", "");
	enqueue(get_action($skill[Saucegeyser]), "", "goto SauceLoop");
	return macro();
}

boolean can_splash() {
	if(my_class() != $class[sauceror] || !(have_skill($skill[Wave of Sauce]) && have_skill($skill[Saucegeyser]))
	  || !(have_effect($effect[Jabañero Saucesphere]) >0 || have_effect($effect[Jalapeño Saucesphere]) >0)
	  || dmg_dealt(get_action($skill[Wave of Sauce]).dmg) >= monster_stat("hp"))
		return false;
	switch(normalize_dmgtype("sauce")) {
	case "hot": return numeric_modifier("spell damage") + numeric_modifier("hot spell damage") >= 25;
	case "cold": return numeric_modifier("spell damage") + numeric_modifier("cold spell damage") >= 25;
	}
	return numeric_modifier("spell damage") >= 25;
}

string take_action(monster foe, string page) {
#print("FinalAttack macro to kill a "+foe+" in the "+my_location()+". ( "+hitchance(1)+" )", "blue");
	float dam = m_regular();
	
	boolean stun;
	void cast_noodles() {
		if(stun || get_action($skill[entangling noodles]).id == "") return;
		enqueue(get_action($skill[entangling noodles]));
		stun = true;
	}
	if((dam > my_hp()/ 3 || $monsters[Uppercase Q, Quantum Mechanic] contains foe)
	  && !(foe == $monster[a.m.c. gremlin] && item_amount($item[divine champagne popper]) > 0) )
		cast_noodles();
		
	switch(foe) {
	case $monster[A.M.C. gremlin]:
		foreach da in $items[divine champagne popper, tattered scrap of paper]
			if(get_action(da).id != "")
				return macro(get_action(da));
		break;
	case $monster[mother hellseal]:
		enqueue(get_action($skill[lunging thrust-smack]), "");
		return macro();
	// This monster was specific to a certain character of mine, not generally useful
	case $monster[naughty sorority nurse]:
		if(my_mp() >= 6+ my_level() * 2 && item_amount($item[frigid ninja stars]) > 0) {
			string [int] rave_stun = split_string(get_property("raveCombo3"), ",");
			foreach key,rave in rave_stun
				enqueue(get_action(to_skill(rave)));
			enqueue(get_action($skill[Moxious Maneuver]));
			enqueue(get_action($skill[Moxious Maneuver]));
			enqueue(get_action($item[frigid ninja stars]));
			return macro();
		}
		abort();
	case $monster[mine crab]:
		enqueue(get_action($item[Miniborg Destroy-O-Bot]));
		enqueue(get_action($item[Miniborg Destroy-O-Bot]), "");
		return macro();
	case $monster[Candied Yam Golem]:
	case $monster[Malevolent Tofurkey]:
	case $monster[Possessed Can of Cranberry Sauce]:
	case $monster[Stuffing Golem]:
	case $monster[Inebriated Tofurkey]:
	case $monster[Hammered Yam Golem]:
	case $monster[Plastered Can of Cranberry Sauce]:
	case $monster[Soused Stuffing Golem]:
	case $monster[El Novio Cadáver]:
	case $monster[El Padre Cadáver]:
	case $monster[La Novia Cadáver]:
	case $monster[La Persona Inocente Cadáver]:
		if(can_splash())
			return splashSauce();
		enqueue(get_action(dangeraction()), "");
		return macro();
	// In Fernswarthy's Basement
	case $monster[the ghost of fernswarthy n great-grandfather]:
	case $monster[a n stone golem]:
		for it from 3755 to 3759
			if(item_amount(to_item(it)) > 3) {
				enqueue(get_action(to_item(it)));
				enqueue(get_action(to_item(it)));
			}
		return macro("mark lovesongLoop", "goto lovesongLoop");
	case $monster[the beast with n ears]:
	case $monster[the beast with n eyes]:
	case $monster[a n-headed hydra]:
	case $monster[n bottles of beer on a golem]:
	case $monster[a n-dimensional horror]:
		for it from 3118 to 3120
			if(item_amount(to_item(it)) > 3) {
				enqueue(get_action(to_item(it)));
				enqueue(get_action(to_item(it)));
			}
		for it from 3754 to 3759
			if(item_amount(to_item(it)) > 3) {
				enqueue(get_action(to_item(it)));
				enqueue(get_action(to_item(it)), "");
			}
		return macro("mark divinesongLoop", "goto divinesongLoop");
	}

	switch(my_location()) {
	case $location[8-Bit Realm]:
		if(get_action($item[blue shell]).id != "" && my_hp() > 25) {
			if(dam +25 > my_hp())
				cast_noodles();
			if(my_hp() > (stun? 25: 25 + dam))
				enqueue(get_action($item[blue shell]));
		}
		enqueue(get_action("attack"), "");
		return macro();
	// This is set up for a specific character. Needs to be changed for circumstance
	case $location[convention hall lobby]:
		cast_noodles();
		enqueue(get_action($skill[wave of sauce]));
		enqueue(get_action($skill[candyblast]));
		enqueue(get_action($item[bottle of Gü-Gone]), "");
		return macro();
	}
	if(can_splash())
		return splashSauce();
	else if(hitchance(1) < .1)
		enqueue(dangeraction(), "");
	enqueue(get_action("attack"), "");
	return macro();
}

void main(int initround, monster foe, string page) {
	page = act(page);
	page = take_action(foe, page);
}
 
Since Bale made me this thread, and it contains lots of good discussion already, I decided to make this the official BatBrain thread. I think it's ready for release, so I'm calling this update 1.0. It has some really awesome new features.

BatBrain 1.0 updates!

  • Smarter aborting of macros (don't use "match" for poisoned, etc.), and a generally more streamlined batround.
  • Smarter insertion of batround into the macros. Subroutines including batround are still built-in for actions with repeat conditions.
  • New wrapper function for player stats -- my_stat(string which). This is basically identical to the already existing monster_stat(string which), except for the player instead. Presently the only possible values are "hp" and "mp". All references to the ASH functions my_hp() and my_mp() have been replaced with this, and you should do the same if you're writing a script using BatBrain -- for reasons I will soon divulge.

Major changes to the enqueueing process:

  • All versions of enqueue() now return a boolean and reject actions (don't enqueue them) if they are empty or if you lack the MP at that point. It returns true if the action was accepted and added to the queue. Now, if (enqueue(get_action("myaction"))), then myaction is available to your character and was added to the queue.
  • All versions of macro() now clear the queue, after enqueueing any actions specified in parameters. It will clear the queue regardless of whether the action was actually enqueued.

And now for something truly amazing: Predictive Adjustment

I'm quite excited to announce this; the framework for it has been sitting there for a long time but I've finally made it work.

When an action is enqueued, the script environment is adjusted to a post-queue state. In other words, if you enqueue an action such as "attack", then check monster_stat("hp"), it will return an adjusted value -- the monster's HP minus your expected damage. If you enqueue a second attack, it adjusts the value further. Checking my_stat("hp") will also include adjustments for the monster's expected damage during those rounds. These adjustments also include retaliation damage, familiar actions, passive effects, etc. -- and significant meat gain/loss if you are predicted to kill the monster or die trying (monster_value() and beatenup, respectively). You can now easily build entire predictive combats in your script.

Since this is a new feature, there are definitely some special cases and formula variables which are not properly altered when adjusting for an enqueued action, but all the most important ones -- accessible in monster_stat() and my_stat() -- are handled. Additionally, since all the adjustments are kept in a gobal advevent variable called adj, you can check the fields of that variable any time for your estimated progress in any of those areas.

For example, if your Volleyball has an ant pick equipped, that's an average of 5 meat per round and 4.4 cold damage. You're facing a Battlefield monster and you enqueue noodles, followed by a saucestorm, followed by a regular attack. That's three rounds -- so assuming no other factors at play, if you check adj.meat at that point, it will contain 15. If you check adj.mp, it will contain -13 (the cost of both skills). If you check adj.hp, it will contain 0, since Noodles is assumed to stun for 3 rounds. The $element[none] field of adj.dmg is used as the monster's HP adjustment, and it will contain -(your estimated saucestorm damage + your estimated attack damage + 26.4). That last number is 4.4 times 3, and doubled because Battlefield monsters are vulnerable to cold.

At last, a predictive solution to plinking:

PHP:
while (m_hit_chance() > 0.07) enqueue(get_action($skill[suckerpunch]));

Known Issues

  • Predictive adjustment presently doesn't work for actions with repeat conditions -- it will treat them like single actions. I don't know if I'll ever try to make this work -- generally, if you know the repeat conditions, you generally know the situation post-macro, at which point if the combat isn't over your script can recalculate.
  • Predictive adjustment also doesn't know about auto-funkifying yet. If you enqueue two consective item throws, the macro will automatically convert these to a single funkslinging toss -- but this happens at the time of macrofying, not at the time of enqueueing, so it predicts this like two rounds rather than one. This is an issue which will be fixed. Auto-funkifying will be moved into enqueueing -- but for now, it just means extra-conservative combat estimations; not too harmful.

Neat, eh? Have fun!
 
Back
Top