BatBrain -- a central nervous system for consult scripts

That's why BatBrain contains kill_rounds() and die_rounds() -- or it will. Although they haven't made their way into the "official" release yet, they are in the placeholder I posted in the SS thread. The first of those accepts either a spread or an advevent.

if (kill_rounds(get_action("attack")) < die_rounds()) print("Your regular attack will kill this monster!");
 
Not bad. So if kill_rounds(get_action("attack")) < die_rounds() && m_dpr(0,0)*die_rounds()*meatperhp - to_profit(get_action("attack")) < 5*meatpermp then it is worth using attack, even if you can hit only with a critical.

I just pulled the number 5 from somewhere. In practice it would be the mp required to kill the monster with combat skills.
 
My monster_stat-function currently looks like this in order to not have my consult script break on b-monsters while in b-core:

Code:
// adjusted monster stat (unknown_ml and current or projected +/-ML)
float monster_stat(string which)
{
	int bees = 0;
	if(my_path() == "Bees Hate You")
	{
		for i from 0 to length(to_string(m)) -1
		{
			if(char_at(to_string(m),i) == "b")
				bees = bees + 1;
		}
	}
	
	switch (which)
	{
		case "att":	return adj.att + (m == last_monster() ? monster_attack() : monster_attack(m)) + bees * 15;
		case "def":	return adj.def + (m == last_monster() ? monster_defense() : monster_defense(m)) + bees * 15;
		case "hp":	return adj.dmg[$element[none]] + max(1.0, (m == last_monster() ? monster_hp() : monster_hp(m))) + bees * 15;
	}
	return 0;
}

15 is probably not the correct number but it is at least closer to the reality than not having it there :)
 
I've been using Bale's FinalAttack and SpellKill posted back and I'm just, "wow". I was watching it yesterday while I was questing and noticed it was hitting with attacks that kept missing, until the monster died from a crit or familiar attack, instead of shieldbutting. While I was disturbed at first, when I finally figured out that it knew I couldn't die no matter how many times opp hit me and was doing the absolute cheapest way of killing said monster. Bale, dude, you collecting anything new?
 
LoL!

That was actually zarqon's brilliance, not mine. SS knows that will happen and simply does that. Only if you can't kill the monster with crits & familiar actions while taking negligible damage will FinalAttack take over. It's wonderful so please send batbits to zarqon.
 
As stated in the SS thread regarding the experimental new BatBrain and SS there appears to be some oddity going on. My script calculates and enqueues skills for me and tehn executes a macro. Today I noticed that it wouldn't saucesplash as it should and realised that this was due to the macro aborting after 1 cast in the 2-cast-chain with the Danger-message. A check with verbosity 9 indicated a wonderful "hpbelow 304" even though my maxhp is currently 100.

In a test I then put in print outs of m_dpr(0,0) after and before every enquement of a skill with the following result:
Code:
SpamAttack: We're actually NOT going to one-shot (with Wave of Sauce) because noodles and then two shotting would be cheaper.
Expected damage per round (before Saucestorm: 33.229385).
Expected damage per round (after Saucestorm: 132.91754).
Expected damage per round (before Salsaball: 132.91754).
Expected damage per round (after Salsaball: 6.58).
SpamAttack: We are going to 2-shot with Saucestorm and Salsaball.

I have jaba and jala saucespheres active but surely they should not give that sort of effect?

Edit: Further digging indicates m_hit_chance() to part of the problem as it returns interesting values such as 6.58 which sounds a bit high considering it should range from 0 to 1...

Edit edit: Even further investigations show that m_hit_chance() get above 1 because stunmod gets to below 0. Changing m_hit_chance() to:
Code:
float m_hit_chance()
{
	float stunmod = minmax(merge(baseround(),adj).stun, 0.0, 1.0);
	if (contains_text(m,"Gremlin"))
		return 1.0 - stunmod;
	return (1.0 - stunmod)*minmax(0.55 + (max(monster_stat("att"),0.0) - my_buffedstat($stat[moxie]))*0.055,my_location() == $location[slime tube] ? 0.8 : 0.06,0.94);
}
removes this problems. I'v eno idea if it causes any other though, and also I'm not sure where the -3.0 in adj.stun comes from when enqueueing saucespells...
 
Last edited:
I've reverted the above change in my local copy since I've figured out the problem (or at least I think I have):

The fucntion "adjust(advevent a)" adapts the script environment when I enqueue something. In its current version it looks like this:
PHP:
void adjust(advevent a)
{
	a = merge(a,baseround());							// add base round
	if (monster_stat("hp")-dmg_dealt(a.dmg) > 0)		// monster is alive; add monster attack
	{
		a.hp -= m_dpr(0,0);
		if (my_stat("hp") < 1)							// you died
			a.meat -= beatenup;
		else											// you didn't die; add retaliation damage
			a = merge(a,factor(retal,m_hit_chance()));
	}
	if (monster_stat("hp")-dmg_dealt(a.dmg) < 1)		// monster died
		a.meat += monstervalue();
	a.dmg = to_spread(-dmg_dealt(a.dmg),"physical");	// monster hp is stored in dmg[none]
	round += count(split_string(a.id,";"));				// a round or several go by
	adj.stun -= max(adj.stun, count(split_string(a.id,";")));
	adj = merge(adj,a);
}

The calculation of the stun-parameter seems just wrong to me. It leads to negative stuns and ever increasing m_dpr(0,0). My local copy now has the following instead:
PHP:
// adjusts script to predicted post-action state; stacks (only reset when you hit the server using act())
void adjust(advevent a)
{
	a = merge(a,baseround());					// add base round
	if (a.id == "attack")
		a = merge(a,onhit);					// add onhit event for regular attack (others?)
	if (monster_stat("hp")-dmg_dealt(a.dmg) > 0)			// monster is alive; add monster attack
	{
		a.hp -= m_dpr(0,0);
		if (my_stat("hp") < 1)
			a.meat -= beatenup;				// you died
		else
			a = merge(a,factor(retal,m_hit_chance()));	// you didn't die; add retaliation damage
	}
	if (monster_stat("hp")-dmg_dealt(a.dmg) < 1)
		a.meat += monstervalue();				// monster died
	a.dmg = to_spread(-dmg_dealt(a.dmg),"physical");		// monster hp is stored in dmg[none]
	round += count(split_string(a.id,";"));				// a round or several go by
	vprint("adj.stun (1345): " + adj.stun + ", a.stun (1345): " + a.stun + ", round: " + round + ", a.id: " + a.id, "red", 9);
	adj = merge(adj,a);
	adj.stun = max(adj.stun - 1, 0);
}

Which seems to work much better accordign to a little test I did today:
Code:
Expected damage per round (before noodles: 9.188834).
Expected damage per round (before wave of sauce: 0.0).
Expected damage per round (before salsaball: 0.0).
Expected damage per round (before saucegeyser1: 9.188834).
Expected damage per round (after everything: 9.188834).

We can see that the m_dpr(0,0) is at ~9 at the start, then goes to 0 as I enqueue noodles, and when the script expects it to have run out it goes back up to ~9 again.

Compare this with the result of the original code and the same type of queue:
Code:
Expected damage per round (before noodles: 9.188834).
Expected damage per round (before wave of sauce: 0.0).
Expected damage per round (before salsaball: 9.188834).
Expected damage per round (before saucegeyser1: 36.755337).
Expected damage per round (after everything: 64.32184).

Also I am not sure why the Purse rat gets added to each action I do since I do not have a crown of thrones, it's just my familiar. Since this affects the round-counter the round increases by way more than it should and will reach maxrounds a lot faster (I've enqueued 4 skills and the round number is 13...)
 
Congrats on locating the bug, Winterbay. That should be a min() rather than a max(). I'll probably just minmax() it as an extra safeguard.

Fun how just two characters being off can totally mess up the whole script.

I've actually expanded advevents locally to include an integer "rounds", containing the number of rounds something takes. That ought to solve these complications based on round numbers and merging events. It also makes it easy to tell whether something is an action or just supplementary event information (or a free action in certain cases).

Other than Salve and Mistletoe, are there any other "quick" actions?
 
No. Let's say adj.stun is 2; we have enqueued an action that stuns for two rounds. Now we enqueue another action, which does not stun.

adj.stun -= min(adj.stun, a.rounds); // note the assignment operator

adj.stun is 2, and the action takes 1 round, meaning that stun will be decremented by one. Since only one round has gone by, this is exactly correct!

Or suppose that instead we'd enqueued a 3-round DB combo as our second action.

adj.stun (2) < rounds (3), so stun will be decremented by 2, thus making it 0 again. Correct!

Although this does make me notice that simulation is off for multi-round advevents. Some of that is unavoidable, since one advevent should always technically be one action, but it could be handled better for cases of "event squash". For instance, a 3-round DB combo should include 3 times the monster's attack rather than one, with stun calculated sequentially. Now that events include the number of rounds, that's possible.
 
Ahh yes, the problem in the current version was that it decremented by 3 since a.id contained "skill 3004; base; Purse Rat" which meant that we got 2 - 3 = -1 and problems :)
 
Finally had a chance to tie up some of the loose ends and spin a proper release. w00t

In this update:

  • Substats are now integrated into advevents (they had been in the record declaration but not actually used).
  • A new field "rounds" now describes the number of rounds an advevent takes. This not only makes clearing the queue possible (below), but it eliminates guesswork when enqueueing actions, which may have an arbitrary number of commands separated by semicolons, and not all of them actions.
  • Track usage of Stealth Mistletoe.
  • As requested, if you have a Juju Mojo Mask equipped but don't have one of the gazes active, it will attempt to attract the appropriate gaze for your mainstat. It does this while enqueueing -- if the action you queued will attract the correct gaze, it does nothing further. Otherwise, it slips your class's trivial skill in front of the action it's about to enqueue.
  • Don't use m_dpr() for the macro abort -- use m_regular(), the monster's full attack. We want to abort if we are in danger of being one-shotted -- any number smaller than that is meaningless.
  • I have no idea wtf was going on when verbosity was set to 9 or higher, but mafia very much disliked something about the to_html(advevent) business in stasis_action(). Changed that to a simple print command -- less information, but seriously that was just off da chain insane.

And some new useful functions:

void reset_queue() -- clears the queue and correspondingly un-adjusts the script environment. The only known issue with this is that it resets stun to 0 -- which may not be true if you have a multi-round stun currently active. MORAL: enqueue enough things to use up your stunners. MOREL: a kind of mushroom.

int kill_rounds( spread )
int kill_rounds( advevent )
-- the number of rounds it would take a given action or damage spread to kill the monster. This accounts for retaliation damage, familiar actions -- everything that BB knows about.

int die_rounds() -- the number of rounds it will take the monster to kill you.

Enjoy, everyone. Sorry it took me a while. Still using the little coding-unfriendly netbook.

SS update coming tomorrow, I think -- with Mayfly support!
 
Coolio.

On the subject of batbrain. Can anyone explain why the following code doesn't work?
PHP:
import <batbrain.ash>
void main(int initround, monster foe, string page)
{
	page = act(page);

	for i from 1 to 4
	{
		if(get_action($skill[salsaball]).id != "")
		{
			vprint("i = " + i + " Expected damage per round (before salsaball: " + m_dpr(0,0) + ").", "purple", 9);
			vprint("my_stat(hp): " + my_stat("hp") + ", my_stat(mp): " + my_stat("mp"), 9);
			vprint("monster_stat(hp): " + monster_stat("hp"), 9);
			enqueue(get_action($skill[salsaball]));
		}
		else
			vprint("Say what?",-9);
	}
	//page = macro(); - this done to abort the script before the activation of the macro
}

The output at verbosity 9 against a cursed pirate is the following:
Code:
Building options...
Options built! (61 actions)
i = 1 Expected damage per round (before salsaball: 0.17324999).
my_stat(hp): 136.0, my_stat(mp): 339.0
monster_stat(hp): 90.0
Say what?
Say what?
Say what?
You're on your own, partner.

Meaning that after the first enqueue of salsaball it cannot be enqueued again. Why not?
 
Did you try that with the old or new BatBrain? The vprints in the new BatBrain should have helped with debugging...

As requested, if you have a Juju Mojo Mask equipped but don't have one of the gazes active, it will attempt to attract the appropriate gaze for your mainstat. It does this while enqueueing -- if the action you queued will attract the correct gaze, it does nothing further. Otherwise, it slips your class's trivial skill in front of the action it's about to enqueue.

Those function names and comments leave something to be desired.

Other than that, nice update! (I really like the comment on QUEUEING.) Thanks, but I worry that this script is becoming somewhat risqué.
 
Last edited:
It was the new version, which makes me confused because there are a lot of things missing in that output, such as the meatperhp and mp readouts which should be there at verbosity 9.
 
Crap. I may have found the problem... Check me on this: (look at the lines in red)

Code:
boolean enqueue(advevent a, string pre, string post) {
   if (a.id == "") return vprint("Unable to enqueue empty action.",-8);  // allows if(enqueue(get_action()))
   if (round + a.rounds >= maxround) return vprint("Can't enqueue '"+a.id+"': combat too long.",-8);
   if (my_stat("mp")+a.mp < 0) return vprint("Unable to enqueue '"+a.id+"': insufficient MP.",-7);  // everybody to the limit
  // attract gays
   boolean have_gays() { foreach i,ev in queue if (which_gays(ev.id) != $stat[none]) return true; return false; }
   if (have_equipped($item[juju mojo mask]) && have_effect($effect[gaze of the volcano god]) + have_effect($effect[Gaze of the Lightning God]) +
       have_effect($effect[gaze of the trickster god]) == 0 && !have_gays() && which_gays(a.id) != my_primestat())
      enqueue(get_action(to_skill(1000*(my_class().to_int()+1))),"","");
   adjust(a);
   [COLOR="#ff0000"]a.id.replace_string("; ","; call batround; ");
   a.id = (pre.length() > 0 ? pre+"; " : "")+a.id+"; call batround"+(post.length() > 0 ? "; "+post : "")+"; ";[/COLOR]
   queue[count(queue)] = a;
   return true;
}

That alters a.id. A.di is actually get_action(skill.id) which passes back a value of opts, so aren't you passing an element of a map by reference, not value? That means you're changing the original element in opts.

If so, then it would explain the problem because get_action() would no longer be able to match salasaball to skill 5000.

Does that sound correct?
 
Last edited:
Oh dear. That looks right to me. Old habits die hard -- I'm used to languages where passing by reference must be specified explicitly, so I usually operate on the parameter (copy) in a function, to avoid needlessly declaring another variable. It looks like adjust() also has this same issue. I'll hasten to post a fix posthaste.
 
Hmm..

Code:
1 MP costs 4.7916665μ.
1 HP costs 0.88917524μ.
chef's hat (35.0 @ +57.0): 220μ * 54.95% = 120.89
Knob Goblin tongs (10.0 @ +57.0): 25μ * 15.7% = 3.925
Knob Goblin pants (5.0 @ +57.0): 30μ * 7.85% = 2.355
Value of stat gain: 935μ
Building options...
Evaluating 'loc(vibrato)'...
Evaluating 'loc(vibrato)'...
Evaluating 'loc(vibrato)'...
Evaluating 'loc(vibrato)'...
Evaluating 'min(50,ceil(5*sqrt(min(785.0,71.0))))'...
Evaluating '1.0*(12+min(0.15*785.0,20)+min(20.0,40)+0.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Evaluating '0.0*min(0.9,0.015*32.0)'...
Options built! (98 actions)
[COLOR="red"]Unexpected error, debug log printed.[/COLOR]
 
Yeah, I was just about to say the same thing. Also, Mafia does not like to display the nice µ symbol for some reason. It shows up as μ instead...

Same code as in #93 gives the following output:
Code:
1 MP costs 9.090909μ.
1 HP costs 2.7272727μ.
enchanted bean (50.0 @ +40.0): 700μ * 70.0% = 490.0
sonar-in-a-biscuit (10.0 @ +40.0): 11μ * 14.0% = 1.54
Value of stat gain: 35μ
Building options...
Options built! (18 actions)
i = 1 Expected damage per round according to m_dpr(0,0), (before salsaball): 6.2116895, and with mregular(): 8.058739
my_stat(hp): 24.0, my_stat(mp): 54.0
monster_stat(hp): 18.0
Unexpected error, debug log printed.
1 MP costs 9.090909μ.
1 HP costs 2.7272727μ.
enchanted bean (50.0 @ +40.0): 700μ * 70.0% = 490.0
sonar-in-a-biscuit (10.0 @ +40.0): 11μ * 14.0% = 1.54
Value of stat gain: 35μ
Building options...
Options built! (18 actions)
i = 1 Expected damage per round according to m_dpr(0,0), (before salsaball): 6.2116895, and with mregular(): 8.058739
my_stat(hp): 24.0, my_stat(mp): 54.0
monster_stat(hp): 18.0
Unexpected error, debug log printed.
You're on your own, partner.

at verbosity = 9.
 
Back
Top