BatBrain -- a central nervous system for consult scripts

Theraze

Active member
The tutorial page on their source code site is rather useful, but most of their data bits aren't sinking into my head too well. :)

Edit: Meh, maybe I'm completely off here. That does make the links clickable, but it's supposed to import and convert your current table properly. It just isn't working well to figure out the width, etc...

Edit2: Still mucking about. Seems that the header/column problem is a known issue, but not one that anyone has posted any ideas to yet though...
http://code.google.com/p/flexigrid/issues/detail?id=105
In the case of your override, it APPEARS that the header row is losing about 50 pixels or so off the left edge. Not sure why...

Edit3: Removed my garbage above, but have made some minor progress in the Flexigrid bits... namely that adding a title does actually make it so you can compress the table, and singleSelect makes more sense in terms of highlighting options when you'll only ever get to use one. So this:
Code:
   "$('.battable').flexigrid({height:'280', singleSelect:true, showTableToggleBtn:true, title: 'BatBrain Combat Options', striped:false }); }); </script>\n"+
is my current (for who knows how long) flexigrid line. Interestingly, the useRp property displays that apparently, your data rows are never being considered as data, because even though it defaults to showing 15 items per page, it still shows everything and only has a single page...

Edit4: One more tiny thing. Above, when you initialize the table, it displays briefly and then refreshes to become a Flexigrid table. If you set its initial style to not display, it immediately shows as Flexigrid instead of needing to refresh. Like this:
Code:
   acttable.append("<table class='battable' style='display:none'>"+
Anyways, I think I've hit my semi-useful point for today, but... oh yeah, last bit. Trying to hardcode useful column widths, to try to get them working properly. If I can ever get it to realize there's data, that would help, but... Currently using these as my column headers. Changed the graphics for HP/MP to just use letters in case that was part of the problem. Wasn't, but... eh.
Code:
     "<thead><tr><th width=240>Action</th><th width=210>Damage</th><th width=100>Delevel</th><th width=55>Stun</th><th width=45>HP</th>"+
     "<th width=45>MP</th><th width=80>Profit</th></tr></thead><tbody>");
Widths seem roughly right to me, but some of the longer entries that should wrap or have resizable columns aren't fully visible.
 
Last edited:

zarqon

Well-known member
You're welcome Bale!

I just discovered something confusing about ASH which due to my confusion is the source of the empty macros occasionally being submitted by SS since the update.

Previously, I wrote code based on the fact that accessing a nonexistant map index would create that index. Then ASH was changed to return empty values without actually creating the index in the map:

> ash boolean[int] somemap; if (somemap[0] || somemap[-1]) print("Blah blah"); count(somemap)

Returned: 0

Excellent. That ought to help scripters avoid confusing errors. I edited my code, and all was well.

Now, I was checking queue[0].id in SmartStasis, assuming this was safe given the above, but this does create the index in the map, pointing to a blank record. This is the source of the empty macro submissions already being reported here, since this unintentional method of adding something to the queue completely sidesteps all of BB's enqueue() checks. It took me forever to track down because I just knew that couldn't be right!

Evidently, if the map is a map of records, and you attempt to access any of the fields of that record, the key does get created:

> ash record somerec { string a; boolean b; int c; }; somerec[int] test; if (test[1].b) print("blah"); count(test)

Returned: 1

Simply accessing the record itself, however, does not:

> ash record somerec { string a; boolean b; int c; }; somerec[int] test; somerec recvar; recvar = test[1]; count(test)

Returned: 0

Either way, if we check count(queue) > 0 before accessing queue[0].id we'll be fine, but I'd like to understand why it works this way. Can anyone explain? And, is this the way it should work? I found it confusing, but maybe it's just me.

Will submit an update to SS fixing this bug shortly.
 

xKiv

Active member
I believe it's for the same reason as autovivification in perl - so that things like
Code:
somehash[a][b][c][d] = 1;
don't need to become
Code:
if (! exists somehash[a]) {create somehash[a];}
if (! exists somehash[a][b]) {create somehash[a][b];}
if (! exists somehash[a][b][c]) {create somehash[a][b][c];}
somehash[a][b][c][d] =1;
(and it should work for all(?) nested data structures)
 

Bale

Minion
I seem to be developing DAM for Boriscore. Can you help make sense of this error, please?

Code:
Evaluator syntax error: can't understand aob
[COLOR="#FF0000"]Group 2 requested, but pattern only has 1 groups (BatBrain.ash, line 187)[/COLOR]

Also, finished() is not always recognizing that I've killed my monster. To cope with this I'm checking finished() || page.contains_text("<!--WINWINWIN-->") although it seems to me that either you figure out why it that fails to update round to maxround +1 or you should add that check to finished().
 

fronobulax

Developer
Staff member
Code:
Evaluator syntax error: can't understand aob
[COLOR="#FF0000"]Group 2 requested, but pattern only has 1 groups (BatBrain.ash, line 187)[/COLOR]

I'm out on a limb here but that is a typical error message from doing a match or split when you don't get as many tokens as you expect.

Looking at BatBrain, I'm guessing that to_spread has the problem and one way or another it was passed data it wasn't expecting. Beyond that you're on your own but hopefully I'm pointing in the right direction.
 

zarqon

Well-known member
The string "aob" does not exist in batfactors, BatBrain, or SmartStasis. Does it exist in DAM? Also, my line 187 is not the same as yours, so the line number didn't give me a clue. Perhaps the next update will fix it, or least you can tell me the new line number and I might have a better clue. Also, some context might help.

For your finished() problem, you're aware this function is not meant to be used predictively, right? Asking just in case. And I really don't want to check WINWINWIN. There has got to be a single check we can make rather than checking for victory, loss, and running away separately. I'm presently checking for the start of the KoL action options table, which will not exist when the fight is over, regardless of how the fight ended. Which monsters were not correctly detected? Again, CLI output would be appreciated if you have it.

What does mafia check to know the fight is over? We could also check my_turncount(), but -- like WINWINWIN -- that's also only partially effective.

The next update will be a refinement of all the heavy-handed reworking that happened in the last one. I have been mostly happy with auto-funk, but not happy with it happening during stasis. You don't want to hurry up your prolonging of combat! So I've made BB skip auto-funk when the items are both the same item and the monster can't deal more than profitforstasis damage to you. That should solve most of those cases, without losing any desired functionality. Of course, auto-funk doesn't work at all in the currently released version so you won't know the difference, but it's nice, trust me. Hehe.

I've also been running into issues caused by trying to better incorporate Bale's lovely idea for reducing server hits into my combat script. A DB with a hobo monkey easily illustrates the problem. If we want to save server hits, it makes sense to enqueue the custom actions, then the combos, then call stasis, which will definitely submit a macro with lots of repeat conditions. If stasis doesn't submit anything, we want to move on to our attack script and queue our finishing moves up, and then submit the whole combat as one macro. Yay fewer server hits and faster automation!

However, implementing this has had undesirable consequences. We've already discussed that the results of your pickpocketing may determine whether you want to cast Rave Steal or either of the Concentrations, so if custom actions contains pickpocketing it has to be submitted first before building your combos. A hobo monkey causes another problem: until the monkey is detected as having stolen, the profit calculations will be based on the assumption that the monkey provides 75 meat every round. If we enqueue our combos and then call stasis(), the stasis action selected will be totally off, because the monkey will probably steal during the combos. What's worse, the stasis conditions won't match the monkey theft message for some reason so the action will continue even if by that point it is no longer profitable to stasis! Aaaaggh. I even tried adding a monkey theft abort to batround but that meant that combos got cut off before being performed.

Since BALLS has far less information available to it than BB does, despite being "closer" to KoL, it seems that at each major point in our combat scripts (custom, combos, stasis, smackdown) we need to check for the possible existence of scenario-changing events in what we have so far, and if they may exist, we need to submit the queue as a macro before continuing so that BB may properly assess the changed scenario rather than choosing actions based on old information. This means about as many server hits as before the change for certain people (DB's with hobo monkeys for instance), but it still does mean fewer server hits for most players. We'll just have to iron out what constitutes a scenario-changing event!

I'm sometimes jealous of people who actually finish their scripts.
 

Bale

Minion
Good luck with that. I hope you work out your struggles with BALLS.

The string "aob" does not exist in batfactors, BatBrain, or SmartStasis. Does it exist in DAM? Also, my line 187 is not the same as yours, so the line number didn't give me a clue. Perhaps the next update will fix it, or least you can tell me the new line number and I might have a better clue. Also, some context might help.

aob is not in DAM. DAM doesn't even use eval() or modifier_eval(). Here's the function containing line 187. I've highlighted 187 in red

Code:
spread to_spread(string dmg, float factor) {
   spread res;
   string[int] pld = split_string(dmg,"\\|");
   foreach i,bit in pld {
      matcher bittles = create_matcher("(\\S+?) ((?:(?:none|hot|cold|stench|sleaze|spooky|slime),?)+)",bit);
      if (!bittles.find()) { if (bit != "") res[$element[none]] += eval(bit,fvars); continue; }
      float thisd = eval(bittles.group(1),fvars);
[COLOR="#FF0000"]      string[int] dmgtypes = split_string(bittles.group(2),",");[/COLOR]
      foreach n,tid in dmgtypes res[to_element(tid)] += thisd / max(count(dmgtypes),1);
   }
   return factor(res,factor);
}


I'm sometimes jealous of people who actually finish their scripts.

People actually finish scripts?
 

fronobulax

Developer
Staff member
Unless someone is dealing with Baobab sap I have no idea where "aob" comes from either.

I would, however, ask what is being passed to to_spread as dmg since I suspect the problem is with the data and not the script. That is much more likely than finding a bug in ash parsing and even if we did, the first thing people would want was the input that generated the error.
 

Bale

Minion
Another feature request please! zarqon, I would be happy if I could choose to add min(5%,+5) to HP, attack and defense to account for monster level variance on non-boss monsters so that I can adventure in slightly greater safety. Those extra 5 HP are a small difference, but sometimes signficant.

(Yay! Another use for the new .boss proxy field since they don't have ML variance.)

Question now! If I have two skills with the same mp cost and both will overkill the monster, how can I chose the one that does more damage? As I understand it, BatBrain caps damage on those skills at a maximum of the monster's remaining HP so I don't know how to chose the more powerful one.
 
Last edited:

Winterbay

Active member
Wouldn't one of them have a higher profit due to the higher damage/mp? Or is that capped as well?

Edit: I can see one problem with non-funkslinging two of the same item and that is with the RAM. But on the other hand Mafia won't let you script that fight anyway so...
 

slyz

Developer
What does mafia check to know the fight is over? We could also check my_turncount(), but -- like WINWINWIN -- that's also only partially effective.
Here is the relevant code in FightRequest.updateRoundData()

PHP:
boolean won = responseText.indexOf( "<!--WINWINWIN-->" ) != -1;

if ( won )
{
	KoLCharacter.getFamiliar().addCombatExperience( responseText );
}

// If we won, the fight is over for sure. It might be over
// anyway. We can detect this in one of two ways: if you have
// the CAB enabled, there will be no link to the old combat
// form. Otherwise, a link to fight.php indicates that the
// fight is continuing

if ( !won &&
	responseText.indexOf( Preferences.getBoolean( "serverAddsCustomCombat" ) ?
		"(show old combat form)" :
		"fight.php" ) != -1 )
{
	return;
}
So Mafia:

1) checks for "<!--WINWINWIN-->"
2) checks for either "(show old combat form)" or "fight.php", depending on whether the user has the CAB enabled.

The serverAddsCustomCombat is set when account.php is visited (at sartup I guess).

I guess BatBrain doesn't need to do 1), since it doesn't have any special actions to perform when the player wins. That would mean that the check could be:
PHP:
finished = !responseText.contains_text( "(show old combat form)" ) && !responseText.contains_text( "fight.php" )
 

slyz

Developer
However, implementing this has had undesirable consequences. We've already discussed that the results of your pickpocketing may determine whether you want to cast Rave Steal or either of the Concentrations, so if custom actions contains pickpocketing it has to be submitted first before building your combos. A hobo monkey causes another problem: until the monkey is detected as having stolen, the profit calculations will be based on the assumption that the monkey provides 75 meat every round.
Just to illustrate this point, and even adding a check for olfacting goth giants in the mix, here is my rave-hobo monkey castle farming BALLS macro:
Code:
[ global prefix ]
sub olf
    if !haseffect on the trail
        while mpbelow 40
            call mafiamp
        endwhile
        skill transcendent olfaction
    endif
endsub
sub endit
    call RaveCombos
    call DiscoCombos
    attack with weapon
    "repeat"
endsub
sub raveSteal_monkey
    if hasskill 50
        "skill 51"
        if match "climbs up"
            "skill 52"
            "skill 50"
            call endit
            goto exit_raveSteal_monkey
        endif
        "skill 52"
        if match "climbs up"
            "skill 50"
            call endit
            goto exit_raveSteal_monkey
        endif
        "skill 50"
        if match "climbs up"
            call endit
            goto exit_raveSteal_monkey
        endif
        mark exit_raveSteal_monkey
    endif
endsub
sub raveSteal_monkey_goth
    if hasskill 50
        "skill 51"
        if match "climbs up"
            "skill 52"
            "skill 50"
            call olf
            call putty
            call endit
            goto exit_raveSteal_monkey_goth
        endif
        "skill 52"
        if match "climbs up"
            "skill 50"
            call olf
            call putty
            call endit
            goto exit_raveSteal_monkey_goth
        endif
        "skill 50"
        if match "climbs up"
            call olf
            call putty
            call endit
            goto exit_raveSteal_monkey_goth
        endif
        mark exit_raveSteal_monkey_goth
    endif
endsub
sub RaveCombos
    if !hasskill 50
        goto skip
    endif
    "skill 52;skill 51;skill 50;"
    "skill 51;skill 50;skill 52;"
    mark skip
endsub
sub DiscoCombos
    if !hasskill 5008
        goto skip
    endif
    "skill 5005;skill 5008;"
    "skill 5003;skill 5005;skill 5008;"
    mark skip
endsub
sub putty
    if hascombatitem 3665
        "use 3665"
    endif
endsub

[ default ]
try to steal an item
if match "climbs up"
    if match "but you don't find anything"
        call raveSteal_monkey
    endif
    call endit
endif
if match "but you don't find anything"
    call raveSteal_monkey
endif
if match "climbs up"
    call endit
endif
while !match "climbs up and sits"
    "use 2"
endwhile
call endit

[ goth giant ]
try to steal an item
if match "climbs up"
    if match "but you don't find anything"
        call raveSteal_monkey_goth
    endif
    call olf
    call putty
    call endit
endif
if match "but you don't find anything"
    call raveSteal_monkey_goth
endif
if match "climbs up"
    call olf
    call putty
    call endit
endif
call olf
if match "climbs up"
    call putty
    call endit
endif
call putty
if match "climbs up"
    call endit
endif
while !match "climbs up and sits"
    "use 2"
endwhile
call endit
And this waits for the monkey to steal before getting the rave/disco +item and +meat effects.

It's obfuscated, it's hard to update when I ascend and the rave combos change, but it seems to work.

I'd hate try generating this sort of thing from an ASH script :D

This reminds me that RoyalTonberry has a tool which allows user to write a BALLS macro with fancy things like "if...else" and generates the corresponding BALLS macro. A BALLS compiler if you want. Would you be interested in that kind of thing?
 
Last edited:

xKiv

Active member
Code:
spread to_spread(string dmg, float factor) {
   spread res;
   string[int] pld = split_string(dmg,"\\|");
   foreach i,bit in pld {
      matcher bittles = create_matcher("(\\S+?) ((?:(?:none|hot|cold|stench|sleaze|spooky|slime),?)+)",bit);
      if (!bittles.find()) { if (bit != "") res[$element[none]] += eval(bit,fvars); continue; }
      float thisd = eval(bittles.group(1),fvars);
[COLOR="#FF0000"]      string[int] dmgtypes = split_string(bittles.group(2),",");[/COLOR]
      foreach n,tid in dmgtypes res[to_element(tid)] += thisd / max(count(dmgtypes),1);
   }
   return factor(res,factor);
}

Weird. Does it work if you sabe bittles.group(1) and bittles.group(2) first (into variables, and then use those variables instead) bedore the float thisd = eval(..) line?

The matcher obviously looks like it has two capturing groups, and bittles.group(1) is apparently "aob" (avatar of boris?), and either both groups should be captured, or neither .... o_O
 

zarqon

Well-known member
I'd also suggest reloading batfactors, just to eliminate that possible source of error. I wrote the new to_spread() to be much stricter about input than the previous, the idea being to refine batfactors.

I would be happy if I could choose to add min(5%,+5) to HP, attack and defense

kill_rounds() already pessimistically accounts for this so if you sorts opts[] properly your top action will correctly take this into account, i.e. even if mafia says the monster has 1 HP, a seal tooth won't be seen as able to kill the monster in one round.

That said, since it seems a lot of people are still using actions by name rather than by quality, for this next update I'll introduce part of a feature I was planning on introducing much later: a pessimism setting. For now, this will only affect ML variance, but (much) later it will also affect damage ranges, hit chances, and familiar action rates.

I don't know how to chose the more powerful one.

The dmg_dealt() function caps the damage on the top end at the monster's HP, however the raw damage dealt exists in every action's dmg field, in spread format. You could thus write your own uncapped_dmg_dealt() function pretty easily.

Here is the relevant code in FightRequest.updateRoundData()

Thanks! We'll go with that and see if that works in all cases. Wouldn't just checking for fight.php work or does the CAB mean there is no longer a link to fight.php?

A BALLS compiler if you want. Would you be interested in that kind of thing?

No, but thanks. If we wanted to get crazy with it, we could start writing BALLS if/then trees using BB to construct multiple queues based from the same point, but with branches for each possible divergence (hitchance < 100, familiar action rates < 100, stun rates < 100, etc). But no way am I touching that yet! I also suspect that presently BALLS simply doesn't provide enough information (or KoL doesn't supply enough useful universal comments in fight text) for that to be feasible.

EDIT: @Theraze: your mucking about inspired me to investigate Flexigrid further. I found a few other people complaining that the table didn't sort when using the DOM as the data source rather than fetching it with AJAX, and the only answer I saw was a guy saying the function for the local data sort doesn't even exist! So I hunted other options; I've just started fooling with DataTables and it rocks! Feature-rich, single dependency (jQuery), incredibly thorough documentation. Plus it does the column width thing dynamically! I'm just now trying to figure out how to customize the sorting -- once I get it reasonably well figured out I'll post my latest fight.ash for you, since going with DataTables it's becoming a very handy tool.
 
Last edited:

xKiv

Active member
The dmg_dealt() function caps the damage on the top end at the monster's HP, however the raw damage dealt exists in every action's dmg field, in spread format. You could thus write your own uncapped_dmg_dealt() function pretty easily.

I looked there and was (again?) horrified by the way averages are used ...
min(average_damage, hp_left) is better than just average_damage, but not very inaccurate, since increasing the max damage past monster hp means that the action will hit for the remainig hp more often (and will "not be enough" less often), which is by definition higher average, always, even though the pre-cap damage average is already over the cap. Your formula actually *overestimates* the average, even (it will be hp_left as soon as (mindmg+maxdmg)/2 >= hp_left, but the actual damage done is always between mindmg and hp_left, so the average is also between those two, and lower than hp_left unless mindmg >= hp_left).
More correct would be (but you don't have the mindmg and maxdmg, just their average :( ) :
Code:
if minDmg == maxDmg return min(minDmg, monsterHP);
// else assume minDmg < maxDmg, so that we won't be dividing by zero
capMinDmg = min(minDmg, monsterHP);
capMaxDmg = min(maxDmg, monsterHP);
negcapMinDmg = max(minDmg, monsterHP);
negcapMaxDmg = max(maxDmg, monsterHP);
// split in two parts (both divided by (max-min)) to show that it's a convex combination of a part that's all below the cap and a part that's all over the cap
// cap and negcap make sure that these parts only operate on values in their domain, and, in fact, if the entire range is on one side of monsterHP, the other part simplifies to zero
// (and in this case, the non-zero part simplies either to (minDmg+maxDmg)/2, or monsterHP, respectively)
return ((capMinDmg + capMaxDmg)/2)*(capMaxDmg - capMinDmg)/(maxDmg - minDmg) + monsterHP*(negcapMaxDmg-negcapMinDmg)/(maxDmg-minDmg);
 

zarqon

Well-known member
If the average damage is greater than the monster's hp, on an action with average results, it will kill the monster. The average raw damage dealt by an action is not changed based on the monster's health. An action that deals 1-10 damage always deals an average of 5.5 damage, regardless of whether the monster has only 2 health remaining. The point of dmg_dealt() is to then apply upper and lower caps to that damage -- when you perform this average-damage action, how much damage will it actually do? The HP cap may not even strictly be necessary for the calculations scripts are making.

Fortunately, no one is requiring you to use this horrifying script. :(
 

Bale

Minion
kill_rounds() already pessimistically accounts for this so if you sorts opts[] properly your top action will correctly take this into account, i.e. even if mafia says the monster has 1 HP, a seal tooth won't be seen as able to kill the monster in one round.


The dmg_dealt() function caps the damage on the top end at the monster's HP, however the raw damage dealt exists in every action's dmg field, in spread format. You could thus write your own uncapped_dmg_dealt() function pretty easily.

OH! Thank you! Both those things are really good to know so thank you for pointing them out.


Fortunately, no one is requiring you to use this horrifying script. :(

This script is horrifyingly awesome! My greatest problem is often not being able to squeeze out every erg of work it can save me, either through lack of understanding or time. Ah BatBrain, how you taunt me with features I have not yet had time to incorporate.
 

xKiv

Active member
If the average damage is greater than the monster's hp, on an action with average results, it will kill the monster. The average raw damage dealt by an action is not changed based on the monster's health. An action that deals 1-10 damage always deals an average of 5.5 damage, regardless of whether the monster has only 2 health remaining.
An action that deals 1-10 damage will kill a 2-hp monster more often than an action that dels 1-3 damage.

The point of dmg_dealt() is to then apply upper and lower caps to that damage -- when you perform this average-damage action, how much damage will it actually do? The HP cap may not even strictly be necessary for the calculations scripts are making.
You are going from "an action by itself" to "an action done by this monster", and that's where the average is no longer correct.
The average damage done by a 1-10 action to a 2-hp monster is not 2, it's 1/10+2*9/10=1.9. The average result isn't dead monster, it's schroedinger's 90% dead monster (which is different from monster that's 90% on the way to being dead).
But I am aware that this cannot be blindly implemented into BatBrain as it is, that would require reworking the entire architecture to support the idea of "does at least X damage, does at most Y damage, average is Z" and "monster is guaranteed to have between A and B hp, with C on average" and actually using those number effectively. But that's something I would have automatically done from the beginning, and not just because it would have better predictive power ("the monster is roughly X% likely to be dead at this point" as opposed to "if all your actions up to this point yielded average results, the monster is/isn't (choose one) dead at this point").

Fortunately, no one is requiring you to use this horrifying script. :(
The awesome parts more than outweight the non-awesome parts.




EDIT: on futher reflection, I would like to add another example:
assume a 6 hp monster and 1-10 damage spell (uniformly distributed)
1) your method says "1 hp left" and will want to deal 1 damage in the next step
2) my method says (assuming I have the formula right) "50% of dead (0 hp left), 50% of 3 left on average (1-6) -> altogether 1.5 hp left" and would want to deal 1.5 damage in the next step
3) you can do even better:
- 1-10 damage -> 50% dead
- if you got here, the monster survived, and we have to use conditional probabilities
- that means 3 hp left on average -> need to deal ~3 damage in the next step

... on average, 1) will deal (assuming the follow-up action deals exactly 1 damage, and therefore 1 damage on average) 1.25 damage less than necessary for kill (50% chance of killing in first step, 50%*1/6 chance of killing in the second step, 50%*5/6 chance of the 1 damage not being enough)
2) will deal (assuming uniformly 1-2 follow-up) 0.7833... damage less than necessary for kill
3) will deal (assuming uniformly 1-5 follow-up) 0.0833... damage less than necessary for kill

(and the divergence will be magnified in futher rounds of "average damage failed to kill")

It's nit picking (especially at low ranges like this), but it can be a round of difference, and that round can be profitable enough to matter.
 
Last edited:
Top