Improving the auto resolve system

Users who are viewing this thread

The wounded ratio seemed a bit high to me. And the battle were a bit slow to me. I doubled the parties strength before the battle, which doubled the speed of the fight (I think), and I'll do with the high wounded ratio. Now I'm just curious about how this damned game works in autoresolve. Why this single guy survive everytime, when his fellows in the stack all died? I mean, all stacks are hit, which means that they are all affected by the inflict casualties. And in other stacks, every soldiers died, which means that the range of the condition is ok. This makes no sense to me. I think that this ****er is the reason why the battle never ends, the party never triggers the 0 strength condition.

My last request and I promise (or at least I'll try to) I leave this forum in peace.

Could anyone be so kind as to point me to me every instance directly involved in the autoresolving processes ? What I want to do is "simply" to add a template to all archers, and bonuses to their strength in siege. Simply. :lol:

edit: nevermind, I've found them six. The battle calculation is simplified but I find it quite satisfying for my mod. Defender strength is multiplied by 2.5. Except when the player is defending (x2 only, thanks for taking care of us armagan :wink: )

but isn't this a problem:

          (store_div, ":defender_strength", ":defender_strength", 20),
          (val_min, ":defender_strength", 50),
          (val_max, ":defender_strength", 1),
          (store_div, ":attacker_strength", ":attacker_strength", 20),
          (val_min, ":attacker_strength", 50),
          (val_add, ":attacker_strength", 1),

Doesn't this limit strength to 50? 60 huscarls would be enough to reach this cap. Or Am I wrong?

Another problem: AI vs AI on the map, castle defenders only have x1.5 while it's 2.5 when the player party is involved.
 
Twan said:
Wow so it's possible to use a generic system instead of 500 lines of tries to fill the bonuses slots.
Technically it's still a set of 500 lines, just not 500 lines that we have to write ourselves. 
I think a complete storing of units attributes etc... isn't needed anyway as most of the factors are already included in troops level and autoresolve hasn't be so detailed that it makes difference between 2 usefull skills or attribute (I mean there are 2 stats and 6 skills usefull in combat, it's interesting to know if an unit has a bonus in these compared to other units of equal level, not necessary to know which stat/skill exactly is better, or the value of stats having no effect in combat).
For the task at hand, yes, storing all stats and six skills is probably overkill but other than a small investment in time it isn't likely to break anything either.  I also don't doubt that someone along the line will find the above useful, and if I've saved someone the trouble, that's a win in my book.  I do believe that some of the information we're trying to divine at compile time should be available through the module system but it isn't.

More needed is to analyze the items an unit is (or may be) carrying as gear especially armor quality make more difference than skills, and as weapons categories are the main factor to give a relevant bonus for each kind of fight (I guess it's doable with an init giving slots to items first, then for each unit making a calculation based on the values of items it may carry ; the problem here is the fact the item list is a potential so if say an unit may have 5 different weapons/armors the value should be either an average or based on a probability).
Compile time analysis of equipment is on the way.  Either I'll get to it this evening/tomorrow or someone else will, though I wouldn't disregard the influence of skills on unit toughness so readily.  An ideal solution in my eyes for posting this kind of code is to show people what's possible and a sample implementation that they can hack up for their particular needs.
 
the 1.5 and 2.5 type stuff is good to move to constants.py - put that sort of stuff there, and you can tweak your compile to generate the numbers you want - and by using the constants in all six autoresolves, then they become consistent.  Just a thought :wink:
 
but isn't this a problem:

          (store_div, ":defender_strength", ":defender_strength", 20),
          (val_min, ":defender_strength", 50),
          (val_max, ":defender_strength", 1),
          (store_div, ":attacker_strength", ":attacker_strength", 20),
          (val_min, ":attacker_strength", 50),
          (val_add, ":attacker_strength", 1),

Doesn't this limit strength to 50? 60 huscarls would be enough to reach this cap. Or Am I wrong?

Another problem: AI vs AI on the map, castle defenders only have x1.5 while it's 2.5 when the player party is involved.
If you have questions about how the autoresolve does its thing, it's probably worth it to read through the original thread as posted above.  I go through what all the stuff does and even posted some code that tries to fix some of its shortcomings.  Of particular note I go through what the strength calculation is and how it's (mis)used in the solver and how the normalization steps screw up strength calculations for very large and/or skilled armies.

In response to your particular question, yes, the normalization effectively puts a cap on how many troops factor into the strength calc on a level basis.  If you hunt around in the code you'll find lots of places that aren't consistent like your 1.5 and 2.5x.  Some of thes are probably intentional but the authors didn't leave us any notes as to their intent.

Of other note, you might also turn off the thing where autoresolve troops won't fight at night  :mrgreen:
 
kt0 said:
Technically it's still a set of 500 lines, just not 500 lines that we have to write ourselves. 

Was an allusion to the (rather long and uneleguant) code I've used to give terrain bonuses in my mod.

(something like
Code:
(try_for_range, ":troop_no", soldiers_begin, soldiers_end),

(try_begin),
(eq, ":troop_no", "trp_blabla_1"),
(assign, ":bonus", x),
(else_try),
(eq, ":troop_no", "trp_blabla_2"),
(assign, ":bonus", y),

(etc... for all troops)
(try_end),
(troop_set_slot, ":troop_no", slot_bonus, ":bonus")

(was even more complex as I was giving each unit several notes based on weapons used, armor etc... to then calculate a different bonus for each terrain)

For the task at hand, yes, storing all stats and six skills is probably overkill but other than a small investment in time it isn't likely to break anything either.  I also don't doubt that someone along the line will find the above useful, and if I've saved someone the trouble, that's a win in my book.  I do believe that some of the information we're trying to divine at compile time should be available through the module system but it isn't.

You are right. Was just thinking of the immediate use for autoresolve.

Compile time analysis of equipment is on the way. 

Thanks a lot for all this work. You just saved people like me who don't understand python out of the functions used in module system, but are sufficiently crazy to make a code with 462 (else_try) to get this kind of value from deadly headaches. :wink:
 
if you're going to do compile time analysis of skills and equipment for use in a script like this, it might be worth looking at some sort of classing system. That would slim the code down considerably.

if you're going to use a rock/paper/scissors type model for the auto-resolve, you could define the base types for use in that at compile time and then when you're actually going to resolve, you can just read one slot to get the type, rather than having to go through try blocks.

Similarly this could be something to be used for improved formation scripts (which is something that's on my to-do list)

so 'spearman', 'swordsman', 'archer', etc. Or branch it out a bit more, would have to think for a reasonable system that's not too convoluted but covers the basics.
 
Compile time analysis of equipment is on the way. 
Rubik has done some of that in his Autoloot enhancement kit.

I have taken that a bit further in SoD, including multiple data points for imods.  I'll try to publish a kit later today based on my current Autoloot, which will give you all the item data you could possibly want :wink:

Also, its easy to extend these things and add additional tables of information that have nothing to do with the MS code - purely for your own autoresolve - perhaps specifying a "classification" for each troop type, as Martin is essentially suggesting.
 
Mordachai said:
Compile time analysis of equipment is on the way. 
Rubik has done some of that in his Autoloot enhancement kit.
Yep, I've been all through rubik's stuff to see what he was doing.  I was talking more about applying such thoughts to troop calculations.  This stuff generally isn't really that hard.  I just wish I had more time to futz with it :smile:

My general thoughts were to chug through all the item data and build an aggregate value for gear.  From there, do more maths and write an offense and defense value to specific slots and use that in the autoresolvers.  Trying to divine the class of a troop from skills and equipment is an interesting problem which I might put some thought into for other purposes, but I'm not sure I'd use more than very basic info during autoresolve either way ("is always mounted" for instance).  That should suit my purposes well enough.  I'm less interested in making it fun for the computer than I am making it at least somewhat more fun for the player  :mrgreen:
 
BLAM! (Part deux)
In this installment of "kt0 uses his least favorite scripting language next to Perl", I've parsed and mungified the item data for each item on a given troop into two values for Offense and Defense and have stuffed them into appropriate slots.  I've made other modifications as well like not filling out the slots that aren't important to the calculation.  Tomorrow (the fates willing) I'll get to the strength calculation and autoresolve changes though astute readers should be able to fill in the blanks appropriately.

I've made a lot of assumptions and fudges here not limited to:
  • assuming how the item picker works esp. if tf_guarantee flags aren't specified
  • increasing blunt damage by 25%
  • increasing pierce damage by 50%
  • giving weird weights to lots of stuff like horse charge and armor

If you try to use this code, you will find bugs.  As always, let me know where they are.  Other than that, feel free to modify to your particular needs. 

This stuff goes at the top of module_scripts.py:
import string
from process_common import *
from module_troops import *
from module_items import *

# pulls the gigantor values out of the skill blob and returns a 3-tuple
# containing power draw, power strike, and power draw skill values.
def kt_get_power_skills( flags ):
pdraw = 0
pstrk = 0
pthrw = 0
pdraw_top = knows_power_draw_10 + knows_power_draw_5
pstrk_top = knows_power_strike_10 + knows_power_strike_5
pthrw_top = knows_power_throw_10 + knows_power_throw_5

if ( flags & pdraw_top ) > 0:
pdraw = flags & pdraw_top
pdraw /= knows_power_draw_1
if ( flags & pstrk_top ) > 0:
pstrk = flags & pstrk_top
pstrk /= knows_power_strike_1
if ( flags & pthrw_top ) > 0:
pthrw = flags & pthrw_top
pthrw /= knows_power_throw_1

return (pdraw, pstrk, pthrw)

# pulls the gigantor values out of the skill blob and returns a 3-tuple
# containing shield, athletics, and ironflesh skill values.
def kt_get_melee_skills( flags ):
shld = 0
athl = 0
irfl = 0
shld_top = knows_shield_10 + knows_shield_5
athl_top = knows_athletics_10 + knows_athletics_5
irfl_top = knows_ironflesh_10 + knows_ironflesh_5

if ( flags & shld_top ) > 0:
shld = flags & shld_top
shld /= knows_shield_1
if ( flags & athl_top ) > 0:
athl = flags & athl_top
athl /= knows_athletics_1
if ( flags & irfl_top ) > 0:
irfl = flags & irfl_top
irfl /= knows_ironflesh_1

return (shld, athl, irfl)

# parse troop items and return a tuple containing average item values. 
# we make assumptions on the flags and average gear of the same type
# to get aggregate values.  note that the weights given to items in a
# list that aren't guaranteed with a tf_ flag are a guess.  i'm counting
# no flag as a 0 value in the average which might not be correct.
def kt_parse_troop_items( item_list, flags, ohprof, thprof, poleprof, bowprof, xbowprof, throwprof, pstrike, pdraw, pthrow ):
mw_value = 0 # melee weapon damage of the greater if multiple
mw_count = 0 # never seen a guy without a weapon O_O
rw_value = 0 # ranged weapon damage
rw_count = 1
ha_value = 0 # head armor
ha_count = 1
ba_value = 0 # body armor
ba_count = 1
fa_value = 0 # foot armor
fa_count = 1
na_value = 0 # hand armor
na_count = 1
sh_value = 0 # shield percentage 0-100
sh_count = 1
ho_value = 0 # horse aggregate charge and armor value
ho_count = 1

# parse guarantee flags
if ( flags & tf_guarantee_boots ) > 0:
fa_count = 0
if ( flags & tf_guarantee_armor ) > 0:
ba_count = 0
if ( flags & tf_guarantee_helmet ) > 0:
ha_count = 0
if ( flags & tf_guarantee_horse ) > 0:
ho_count = 0
if ( flags & tf_guarantee_shield ) > 0:
sh_count = 0
if ( flags & tf_guarantee_ranged ) > 0:
rw_count = 0

# constants
pierce_flag = pierce << iwf_damage_type_bits
blunt_flag = blunt << iwf_damage_type_bits

# parse each item
# once we know the type, we pull the values from the appropriate places.
# if we don't know the type, we ignore the item.  we also ignore ammo
# and books and a handful of other things intentionally.
for item in item_list:
item_type = items[item][3] & 0xFF
if itp_type_horse == item_type:
ho_count += 1
chg = get_thrust_damage( items[item][6] )
arm = get_body_armor( items[item][6] )
ho_value += chg
ho_value += (arm+5)/10
# we only consider the higher of thrust or swing damage
elif item_type in (itp_type_one_handed_wpn, itp_type_two_handed_wpn, itp_type_polearm):
mw_count += 1
swd = get_swing_damage( items[item][6] )
thd = get_thrust_damage( items[item][6] )
speed = get_speed_rating( items[item][6] )
# if the damage is blunt, we increase by 25%
# if the damage is thrust, we increase by 50%
if (swd & pierce_flag) > 0:
swd &= 0xFF
swd *= 3
swd /= 2
elif (swd & blunt_flag) > 0:
swd &= 0xFF
swd *= 5
swd /= 4
if (thd & pierce_flag) > 0:
thd &= 0xFF
thd *= 3
thd /= 2
elif (thd & blunt_flag) > 0:
thd &= 0xFF
thd *= 5
thd /= 4
# also modify by speed rating and proficiency
prof = 100
if item_type == itp_type_one_handed_wpn:
prof = ohprof
elif item_type == itp_type_two_handed_wpn:
prof = thprof
elif item_type == itp_type_polearm:
prof = poleprof
swd *= speed
swd *= prof
thd *= speed
thd *= prof
if pstrike > 0:
swd *= (100 + pstrike * :cool:
swd /= 100
thd *= (100 + pstrike * :cool:
thd /= 100
swd /= 10000
thd /= 10000
if swd > thd:
mw_value += swd
else:
mw_value += thd
elif item_type in (itp_type_bow, itp_type_crossbow, itp_type_thrown):
rw_count += 1
rdam = get_thrust_damage( items[item][6] )
rdam &= 0xFF # no adjustment for type
# adjust for speed and accuracy
acc = get_leg_armor( items[item][6] )
spd = get_speed_rating( items[item][6] )
if acc == 0:
acc = 100
rdam *= acc
rdam *= spd
# adjust for proficiency
if item_type == itp_type_bow:
rdam *= bowprof
if pdraw > 0:
pdraw_amt = get_difficulty( items[item][6] )
pdraw_amt += 4
if pdraw < pdraw_amt:
pdraw_amt = pdraw
rdam *= (100 + pdraw_amt*14)
rdam /= 100
elif item_type == itp_type_crossbow:
rdam *= xbowprof
elif item_type == itp_type_thrown:
rdam *= throwprof
if pthrow > 0:
rdam *= (100 + pthrow*10)
rdam /= 100
rdam /= 1000000
rw_value += rdam
elif itp_type_shield == item_type:
sh_count += 1
sh_value += 100
elif item_type in (itp_type_head_armor, itp_type_body_armor, itp_type_foot_armor, itp_type_hand_armor):
if itp_type_head_armor == item_type:
ha_count += 1
elif itp_type_body_armor == item_type:
ba_count += 1
elif itp_type_foot_armor == item_type:
fa_count += 1
elif itp_type_hand_armor == item_type:
na_count += 1
na_value += get_body_armor( items[item][6] )
else:
print "ERROR:  item ", items[item][0], " is unknown armor type!" # shouldn't ever get this
ba_value += get_body_armor( items[item][6] )
fa_value += get_leg_armor( items[item][6] )
ha_value += get_head_armor( items[item][6] )

# do the averaging; values will be rough
if ba_count > 0: # nb:  this doesn't catch no body armor + gloves case
ba_value -= na_value
ba_value /= ba_count
if na_count > 0:
na_value /= na_count
ba_value += na_value
if ha_count > 0:
ha_value /= ha_count
if fa_count > 0:
fa_value /= fa_count
if mw_count > 0:
mw_value /= mw_count
if rw_count > 0:
rw_value /= rw_count
if sh_count > 0:
sh_value /= sh_count
if ho_count > 0:
ho_value /= ho_count

return (mw_value, rw_value, ha_value, ba_value, fa_value, sh_value, ho_value)

# generates code tuples for setting slots based on values accessible
# during compile.  this gets inserted into the scripts array and parsed
# like any other module code. 
def kt_python_init_troop_slots():
module_code = []

# figure out our bounds
underscore_pos = string.find( soldiers_begin, "_" )
id_str = soldiers_begin[ underscore_pos+1:len(soldiers_begin) ]
begin_troop = find_troop( troops, id_str )
underscore_pos = string.find( soldiers_begin, "_" )
id_str = soldiers_end[ underscore_pos+1 : len(soldiers_end) ]
end_troop = find_troop( troops, id_str )

# process for each troop
for i_troop in range(begin_troop, end_troop+1):
oneh_prof = (troops[i_troop][9] >> one_handed_bits) & 0x3FF
twoh_prof = (troops[i_troop][9] >> two_handed_bits) & 0x3FF
pole_prof = (troops[i_troop][9] >> polearm_bits) & 0x3FF
arch_prof = (troops[i_troop][9] >> archery_bits) & 0x3FF
xbow_prof = (troops[i_troop][9] >> crossbow_bits) & 0x3FF
thrw_prof = (troops[i_troop][9] >> throwing_bits) & 0x3FF
att_str = (troops[i_troop][8] & 0xFF)
att_agi = (troops[i_troop][8] & 0xFF00) >> 8
att_int = (troops[i_troop][8] & 0xFF0000) >> 16
att_cha = (troops[i_troop][8] & 0xFF000000) >> 24
# setup special skills (add whatever you care about here as well)
(skill_pdraw, skill_pstrike, skill_pthrow) = kt_get_power_skills( troops[i_troop][10] )
(skill_shld, skill_athl, skill_irfl) = kt_get_melee_skills( troops[i_troop][10] )

mw_value = 0
rw_value = 0
ha_value = 0
ba_value = 0
fa_value = 0
sh_value = 0
ho_value = 0
(mw_value, rw_value, ha_value, ba_value, fa_value, sh_value, ho_value) = kt_parse_troop_items( troops[i_troop][7], troops[i_troop][3], oneh_prof, twoh_prof, pole_prof, arch_prof, xbow_prof, thrw_prof, skill_pstrike, skill_pdraw, skill_pthrow )
d_val = ha_value + ba_value + fa_value + sh_value
d_val /= 5
d_val += skill_irfl*2
d_val += att_str
o_val = mw_value + rw_value + ho_value
module_code.append( (troop_set_slot, "trp_"+troops[i_troop][0], kt_slot_troop_o_val, o_val) )
module_code.append( (troop_set_slot, "trp_"+troops[i_troop][0], kt_slot_troop_d_val, d_val) )

return module_code[:]

Stick this at the bottom of your module_scripts.py which is the same as last time:
  # the body of this script is generated dynamically.
( "kt_init_troop_slots", kt_python_init_troop_slots() ),

New slots in module_constants.py but not all of them used now:
kt_slot_troop_1hprof = 200
kt_slot_troop_2hprof = 201
kt_slot_troop_poleprof = 202
kt_slot_troop_archprof = 203
kt_slot_troop_xbowprof = 204
kt_slot_troop_thrwprof = 205
kt_slot_troop_str = 206
kt_slot_troop_agi = 207
kt_slot_troop_int = 208
kt_slot_troop_cha = 209
kt_slot_troop_pstrike = 210
kt_slot_troop_pdraw = 211
kt_slot_troop_pthrow = 212
kt_slot_troop_shield = 213
kt_slot_troop_atheltics = 214
kt_slot_troop_ironflesh = 215
kt_slot_troop_o_val = 230
kt_slot_troop_d_val = 231

Data generated from Native troops for your perusal (name, o_val, d_val, old strength value):
        farmer 16 11 2
        townsman 18 12 2
        watchman 33 36 4
        caravan_guard 38 40 6
        mercenary_swordsman 48 51 10
        hired_blade 67 57 13
        mercenary_crossbowman 49 29 9
        mercenary_horseman 59 50 10
        mercenary_cavalry 73 55 13
        mercenaries_end 0 7 2
        swadian_recruit 17 22 2
        swadian_militia 29 34 4
        swadian_footman 34 41 6
        swadian_infantry 39 50 10
        swadian_sergeant 69 59 13
        swadian_skirmisher 35 28 6
        swadian_crossbowman 46 28 9
        swadian_sharpshooter 57 45 12
        swadian_man_at_arms 70 51 10
        swadian_knight 92 64 13
        swadian_messenger 77 16 13
        swadian_deserter 35 30 6
        swadian_prison_guard 63 61 13
        swadian_castle_guard 63 61 13
        vaegir_recruit 16 26 2
        vaegir_footman 25 35 4
        vaegir_skirmisher 33 27 6
        vaegir_archer 48 30 9
        vaegir_marksman 68 35 12
        vaegir_veteran 34 42 6
        vaegir_infantry 39 52 9
        vaegir_guard 58 59 12
        vaegir_horseman 54 51 9
        vaegir_knight 82 59 12
        vaegir_messenger 99 16 13
        vaegir_deserter 42 27 6
        vaegir_prison_guard 48 56 12
        vaegir_castle_guard 51 56 12
        khergit_tribesman 26 17 2
        khergit_skirmisher 53 27 4
        khergit_horseman 71 34 6
        khergit_horse_archer 65 39 6
        khergit_veteran_horse_archer 101 46 10
        khergit_lancer 85 51 12
        khergit_messenger 95 16 13
        khergit_deserter 42 27 6
        khergit_prison_guard 40 54 12
        khergit_castle_guard 42 54 12
        nord_recruit 17 24 3
        nord_footman 50 39 4
        nord_trained_footman 40 51 6
        nord_warrior 76 54 9
        nord_veteran 95 57 12
        nord_champion 143 63 16
        nord_huntsman 25 20 5
        nord_archer 43 26 7
        nord_veteran_archer 61 32 9
        nord_messenger 100 16 13
        nord_deserter 42 27 6
        nord_prison_guard 49 55 12
        nord_castle_guard 48 58 12
        rhodok_tribesman 14 23 2
        rhodok_spearman 35 38 4
        rhodok_trained_spearman 47 46 6
        rhodok_veteran_spearman 57 51 9
        rhodok_sergeant 67 67 12
        rhodok_crossbowman 47 31 4
        rhodok_trained_crossbowman 57 45 7
        rhodok_veteran_crossbowman 68 48 10
        rhodok_sharpshooter 83 54 13
        rhodok_messenger 105 16 13
        rhodok_deserter 39 27 6
        rhodok_prison_guard 48 57 12
        rhodok_castle_guard 44 57 12
        looter 7 11 2
        bandit 30 23 4
        brigand 49 29 7
        mountain_bandit 51 31 5
        forest_bandit 44 17 5
        sea_raider 76 46 7
        steppe_bandit 72 29 5
        black_khergit_horseman 64 32 10
        manhunter 15 22 4
        slave_driver 25 22 6
        slave_hunter 25 22 9
        slave_crusher 51 22 11
        slaver_chief 75 29 14
        follower_woman 30 24 2
        hunter_woman 36 24 4
        fighter_woman 42 31 7
        sword_sister 76 55 11
        refugee 11 10 1
        peasant_woman 10 9 1
        caravan_master 47 20 4
        kidnapped_girl 0 9 1
        town_walker_1 0 13 2

On a somewhat related note, why the crap does the code tag kill spacing?!  Especially bad for python  :???:
 
nice work.

Just some feedback from a quick look at your numbers there. Obviously I don't know exactly how you're going to do the calculations in the script so I might be speaking out of turn.

Seems to be atm that mounted troops are not getting much in the way of an offensive (or defensive) bonus. Again, perhaps you are going to fix this in the scripts in which case you may disregard this, but for example vaegir knight vs nord champion. Similar defenses but the champion has about 1,5 times the offense rating.

in MNB especially, mounted units have a huge advantage in missions due (in part) to the poor combat system. So this is not really a realistic representation of that.

This list also explain why rhodoks = lose :smile:

 
MartinF said:
Just some feedback from a quick look at your numbers there. Obviously I don't know exactly how you're going to do the calculations in the script so I might be speaking out of turn.
This is exactly the kind of feedback I'm looking for.  Skepticism isn't a bad thing :wink: 
As a simple first cut, I'll take the D val and average it across all troops.  From this I'll take a % soak based on the armor calculations that were discussed recently.  Mul the O val of the opponent by the soak val of this party to get the adjusted damage output which will then be normalized and weighted for advantage as per my last bit of code.  This will then get fed into the inflict casualties handler and off we go.  Should be relatively simple but I won't know until I see the numbers.

Seems to be atm that mounted troops are not getting much in the way of an offensive (or defensive) bonus. Again, perhaps you are going to fix this in the scripts in which case you may disregard this, but for example vaegir knight vs nord champion. Similar defenses but the champion has about 1,5 times the offense rating.
Huscarls are scary.  I don't have the numbers with me but they have among the highest if not THE highest weapon proficiences and skill levels in Native.  Some tweaking of the maths is probably necessary, but I'm not surprised that Huscarls seem mean :smile:

EDIT:  Ha, yeah.  There's a bug there.  Huscarls are scary but not that scary.  This is what I get for posting without really thinking through the code.  Any troop that has both ranged and melee weapons got full values for both.  Huscarls carry lots of throwables and have power throw 5 which inflates their ranged value even further.  Thanks for pointing that out. 

Mounted troops get a bonus to offense equal to their armor/10 and their charge value and no bonus to defense.  It turns out that the top end horses don't show up on normal troops (same with top end armors).  And yes, you've got my idea pegged:  I won't add in a bigger offense/defense until strength calculation time (it'll be a % bonus).  If I were more on the ball last night, I would have stuffed the horse contribution to offense in its own slot so I wouldn't count it for sieges and such.  I'll fix that for round 3. 

Something I noticed last evening was that mounted archers get an offense bonus for their horses even though they shouldn't be charging into the fray (though sadly, my Veteran Horse Archers always do  :???:).  I didn't get to divining class disposition based on items last night but that's what I'd need to properly adjust the maths.  There are probably more situations like that which are broken. WIP :smile:
 
BLAM! the return of BLAM!

This post adds almost the last piece to the puzzle.  There are still some loose ends which I'll point out, but this should be enough to get folks going.  There are undoubtedly bugs.  Let me know when you find them.  There's more discussion at the bottom of this post.  If you use this stuff in your mod and release it publically, please add my name to the credits.  That would be swell.

Changes from last time:
  • Horse attack values split out to its own slot
  • Troop type divined from tf_guarantee flags
  • Tweaks to attack values based on troop type (thanks MartinF!)
  • Shield values use shield size rather than 100
  • Strength calculation changed to use the new values
  • 3/6 resolvers re-worked to use the new data (others not forthcoming)
  • Various and sundry tweaks I've forgotten

As it turned out, divining the troop type from the tf_guaranteed flags wasn't too hard and the results look passable.  The strength calculation was also fairly straightforward.  The defense calculation worked out fairly well and I was able to remove the "we're totally killing those guys" weighting I put in with the modified oldstyle strength calculation based on level.  Finally, I've updated only 3 of the 6 resolvers because the other 3 require coordination with an AI ally which I don't have the ability to test easily.  Astute readers should be able to fill in the blanks easily enough.

As added bonuses, this includes remaining fit troop counts and fixes the not-counting-attachments bug in castle_attack_walls_simulate.


Stick this at the top of your module_scripts.py:
import string
from process_common import *
from module_troops import *
from module_items import *

# pulls the gigantor values out of the skill blob and returns a 3-tuple
# containing power draw, power strike, and power draw skill values.
def kt_get_power_skills( flags ):
pdraw = 0
pstrk = 0
pthrw = 0
pdraw_top = knows_power_draw_10 + knows_power_draw_5
pstrk_top = knows_power_strike_10 + knows_power_strike_5
pthrw_top = knows_power_throw_10 + knows_power_throw_5

if ( flags & pdraw_top ) > 0:
pdraw = flags & pdraw_top
pdraw /= knows_power_draw_1
if ( flags & pstrk_top ) > 0:
pstrk = flags & pstrk_top
pstrk /= knows_power_strike_1
if ( flags & pthrw_top ) > 0:
pthrw = flags & pthrw_top
pthrw /= knows_power_throw_1

return (pdraw, pstrk, pthrw)

# pulls the gigantor values out of the skill blob and returns a 3-tuple
# containing shield, athletics, and ironflesh skill values.
def kt_get_melee_skills( flags ):
shld = 0
athl = 0
irfl = 0
shld_top = knows_shield_10 + knows_shield_5
athl_top = knows_athletics_10 + knows_athletics_5
irfl_top = knows_ironflesh_10 + knows_ironflesh_5

if ( flags & shld_top ) > 0:
shld = flags & shld_top
shld /= knows_shield_1
if ( flags & athl_top ) > 0:
athl = flags & athl_top
athl /= knows_athletics_1
if ( flags & irfl_top ) > 0:
irfl = flags & irfl_top
irfl /= knows_ironflesh_1

return (shld, athl, irfl)

# parse troop items and return a tuple containing average item values. 
# we make assumptions on the flags and average gear of the same type
# to get aggregate values.  note that the weights given to items in a
# list that aren't guaranteed with a tf_ flag are a guess.  i'm counting
# no flag as a 0 value in the average which might not be correct.
def kt_parse_troop_items( item_list, flags, ohprof, thprof, poleprof, bowprof, xbowprof, throwprof, pstrike, pdraw, pthrow ):
mw_value = 0 # melee weapon damage of the greater if multiple
mw_count = 0 # never seen a guy without a weapon O_O
rw_value = 0 # ranged weapon damage
rw_count = 1
ha_value = 0 # head armor
ha_count = 1
ba_value = 0 # body armor
ba_count = 1
fa_value = 0 # foot armor
fa_count = 1
na_value = 0 # hand armor
na_count = 1
sh_value = 0 # shield percentage 0-100
sh_count = 1
ho_value = 0 # horse aggregate charge and armor value
ho_count = 1

guarantee_horse = 0
guarantee_ranged = 0
troop_type = kt_troop_type_footsoldier

# parse guarantee flags
if ( flags & tf_guarantee_boots ) > 0:
fa_count = 0
if ( flags & tf_guarantee_armor ) > 0:
ba_count = 0
if ( flags & tf_guarantee_helmet ) > 0:
ha_count = 0
if ( flags & tf_guarantee_horse ) > 0:
ho_count = 0
guarantee_horse = 1
if ( flags & tf_guarantee_shield ) > 0:
sh_count = 0
if ( flags & tf_guarantee_ranged ) > 0:
rw_count = 0
guarantee_ranged = 1

# divine the troop type from the guarantee flags.  this isn't 100%.
if guarantee_horse and guarantee_ranged:
troop_type = kt_troop_type_mtdarcher
elif guarantee_horse and not guarantee_ranged:
troop_type = kt_troop_type_cavalry
elif not guarantee_horse and guarantee_ranged:
troop_type = kt_troop_type_archer
else:
troop_type = kt_troop_type_footsoldier

# constants
pierce_flag = pierce << iwf_damage_type_bits
blunt_flag = blunt << iwf_damage_type_bits

# parse each item
# once we know the type, we pull the values from the appropriate places.
# if we don't know the type, we ignore the item.  we also ignore ammo
# and books and a handful of other things intentionally.
for item in item_list:
item_type = items[item][3] & 0xFF
if itp_type_horse == item_type:
ho_count += 1
chg = get_thrust_damage( items[item][6] )
arm = get_body_armor( items[item][6] )
ho_value += chg
ho_value += (arm+5)/10
# we only consider the higher of thrust or swing damage
elif item_type in (itp_type_one_handed_wpn, itp_type_two_handed_wpn, itp_type_polearm):
mw_count += 1
swd = get_swing_damage( items[item][6] )
thd = get_thrust_damage( items[item][6] )
speed = get_speed_rating( items[item][6] )
if (swd & pierce_flag) > 0:
swd &= 0xFF
swd *= 3
swd /= 2
elif (swd & blunt_flag) > 0:
swd &= 0xFF
swd *= 5
swd /= 4
if (thd & pierce_flag) > 0:
thd &= 0xFF
thd *= 3
thd /= 2
elif (thd & blunt_flag) > 0:
thd &= 0xFF
thd *= 5
thd /= 4
# also modify by speed rating and proficiency
prof = 100
if item_type == itp_type_one_handed_wpn:
prof = ohprof
elif item_type == itp_type_two_handed_wpn:
prof = thprof
elif item_type == itp_type_polearm:
prof = poleprof
swd *= speed
swd *= prof
thd *= speed
thd *= prof
if pstrike > 0:
swd *= (100 + pstrike * :cool:
swd /= 100
thd *= (100 + pstrike * :cool:
thd /= 100
swd /= 10000
thd /= 10000
if swd > thd:
mw_value += swd
else:
mw_value += thd
elif item_type in (itp_type_bow, itp_type_crossbow, itp_type_thrown):
rw_count += 1
rdam = get_thrust_damage( items[item][6] )
# adjust for type
if (rdam & pierce_flag) > 0:
rdam &= 0xFF
rdam *= 3
rdam /= 2
elif (rdam & blunt_flag) > 0:
rdam &= 0xFF
rdam *= 5
rdam /= 4
# adjust for speed and accuracy
acc = get_leg_armor( items[item][6] )
spd = get_speed_rating( items[item][6] )
if acc == 0:
acc = 100
rdam *= acc
rdam *= spd
# adjust for proficiency
if item_type == itp_type_bow:
rdam *= bowprof
if pdraw > 0:
pdraw_amt = get_difficulty( items[item][6] )
pdraw_amt += 4
if pdraw < pdraw_amt:
pdraw_amt = pdraw
rdam *= (100 + pdraw_amt*14)
rdam /= 100
elif item_type == itp_type_crossbow:
rdam *= xbowprof
elif item_type == itp_type_thrown:
rdam *= throwprof
if pthrow > 0:
rdam *= (100 + pthrow*10)
rdam /= 100
rdam /= 1000000
rw_value += rdam
elif itp_type_shield == item_type:
sh_count += 1
sh_value += get_weapon_length( items[item][6] )
elif item_type in (itp_type_head_armor, itp_type_body_armor, itp_type_foot_armor, itp_type_hand_armor):
if itp_type_head_armor == item_type:
ha_count += 1
elif itp_type_body_armor == item_type:
ba_count += 1
elif itp_type_foot_armor == item_type:
fa_count += 1
elif itp_type_hand_armor == item_type:
na_count += 1
na_value += get_body_armor( items[item][6] )
else:
print "ERROR:  item ", items[item][0], " is unknown armor type!" # shouldn't ever get this
ba_value += get_body_armor( items[item][6] )
fa_value += get_leg_armor( items[item][6] )
ha_value += get_head_armor( items[item][6] )

# do the averaging; values will be rough
if ba_count > 0: # nb:  this doesn't catch no body armor + gloves case
ba_value -= na_value
ba_value /= ba_count
if na_count > 0:
na_value /= na_count
ba_value += na_value
if ha_count > 0:
ha_value /= ha_count
if fa_count > 0:
fa_value /= fa_count
if mw_count > 0:
mw_value /= mw_count
if rw_count > 0:
rw_value /= rw_count
if sh_count > 0:
sh_value /= sh_count
if ho_count > 0:
ho_value /= ho_count

return (mw_value, rw_value, ha_value, ba_value, fa_value, sh_value, ho_value, troop_type)

# generates code tuples for setting slots based on values accessible
# during compile.  this gets inserted into the scripts array and parsed
# like any other module code. 
def kt_python_init_troop_slots():
module_code = []

# figure out our bounds
underscore_pos = string.find( soldiers_begin, "_" )
id_str = soldiers_begin[ underscore_pos+1:len(soldiers_begin) ]
begin_troop = find_troop( troops, id_str )
underscore_pos = string.find( soldiers_begin, "_" )
id_str = soldiers_end[ underscore_pos+1 : len(soldiers_end) ]
end_troop = find_troop( troops, id_str )

# process for each troop
for i_troop in range(begin_troop, end_troop+1):
oneh_prof = (troops[i_troop][9] >> one_handed_bits) & 0x3FF
twoh_prof = (troops[i_troop][9] >> two_handed_bits) & 0x3FF
pole_prof = (troops[i_troop][9] >> polearm_bits) & 0x3FF
arch_prof = (troops[i_troop][9] >> archery_bits) & 0x3FF
xbow_prof = (troops[i_troop][9] >> crossbow_bits) & 0x3FF
thrw_prof = (troops[i_troop][9] >> throwing_bits) & 0x3FF
att_str = (troops[i_troop][8] & 0xFF)
att_agi = (troops[i_troop][8] & 0xFF00) >> 8
att_int = (troops[i_troop][8] & 0xFF0000) >> 16
att_cha = (troops[i_troop][8] & 0xFF000000) >> 24
# setup special skills (add whatever you care about here as well)
(skill_pdraw, skill_pstrike, skill_pthrow) = kt_get_power_skills( troops[i_troop][10] )
(skill_shld, skill_athl, skill_irfl) = kt_get_melee_skills( troops[i_troop][10] )
mw_value = 0
rw_value = 0
ha_value = 0
ba_value = 0
fa_value = 0
sh_value = 0
ho_value = 0
troop_type = 0
(mw_value, rw_value, ha_value, ba_value, fa_value, sh_value, ho_value, troop_type) = kt_parse_troop_items( troops[i_troop][7], troops[i_troop][3], oneh_prof, twoh_prof, pole_prof, arch_prof, xbow_prof, thrw_prof, skill_pstrike, skill_pdraw, skill_pthrow )
d_val = ha_value + ba_value + fa_value + sh_value
d_val /= 5
d_val += skill_irfl*2
d_val += att_str
if troop_type in (kt_troop_type_mtdarcher, kt_troop_type_archer):
o_val = mw_value / 3 + rw_value
if troop_type in (kt_troop_type_footsoldier, kt_troop_type_cavalry):
o_val = mw_value + rw_value / 4
h_val = ho_value
module_code.append( (troop_set_slot, "trp_"+troops[i_troop][0], kt_slot_troop_o_val, o_val) )
module_code.append( (troop_set_slot, "trp_"+troops[i_troop][0], kt_slot_troop_d_val, d_val) )
module_code.append( (troop_set_slot, "trp_"+troops[i_troop][0], kt_slot_troop_h_val, h_val) )

old_val = troops[i_troop][8]
old_val >>= level_bits
old_val &= level_mask
old_val += 12
old_val *= old_val
old_val /= 100
troop_string = "footsoldier"
if troop_type == kt_troop_type_cavalry:
troop_string = "cavalry"
if troop_type == kt_troop_type_archer:
troop_string = "archer"
if troop_type == kt_troop_type_mtdarcher:
troop_string = "mtdarcher"

return module_code[:]

Stick this stuff at the bottom of your module_scripts.py.  This includes four new functions kt_party_calculate_strength, kt_party_calculate_strength_with_attachments, kt_count_viable_troops, and kt_count_viable_troops_with_attachments:
( "kt_init_troop_slots", kt_python_init_troop_slots() ),

# kt0:  new strength calculation
# this script makes use of new slots that are filled out at init time with
# script code that was generated at compile time.  the range of the values
# coming out of this script are much larger (about 100x) than the original
# range.  furthermore, we add a defense calculation and troop count to the
# returns.
# INPUT: 
# arg1:  party_id
# arg2:  exclude leader
# arg3:  is siege
# OUTPUT:
# reg0:  offense value
# reg1:  defense value (damage redux in percent)
# reg2:  troop count
( "kt_party_calculate_strength",
[
# remember our params
(store_script_param_1, ":party"), # party id
(store_script_param_2, ":exclude_leader"), # also a party id apparently
(store_script_param, ":is_siege", 3), # so we don't count horses for sieges

# clear out our returns and temps
(assign, reg0, 0),
(assign, reg1, 0),
(assign, reg2, 0),

# figure out which stack to start with and how many we have
(party_get_num_companion_stacks, ":num_stacks", ":party"),
(assign, ":first_stack", 0),
(try_begin),
(neq, ":exclude_leader", 0),
(assign, ":first_stack", 1),
(try_end),

# for each stack that we care about, grab the offense, defense and count
# and stuff the values into our return registers. 
(try_for_range, ":i_stack", ":first_stack", ":num_stacks"),
(party_stack_get_troop_id, ":stack_troop", ":party", ":i_stack"),
(party_stack_get_size, ":stack_size",":party",":i_stack"),
(party_stack_get_num_wounded, ":num_wounded",":party",":i_stack"),
(val_sub, ":stack_size", ":num_wounded"),
(gt, ":stack_size", 0),
(assign, ":eek:_val", 0),
(assign, ":d_val", 0),
(assign, ":h_val", 0),
(assign, ":tr_type", 0),
(try_begin),
# if this is not a hero, just read slots
(neg|troop_is_hero, ":stack_troop"),
(troop_get_slot, ":eek:_val", ":stack_troop", kt_slot_troop_o_val),
(troop_get_slot, ":d_val", ":stack_troop", kt_slot_troop_d_val),
(troop_get_slot, ":h_val", ":stack_troop", kt_slot_troop_h_val),
(troop_get_slot, ":tr_type", ":stack_troop", kt_slot_troop_type),
# zero out horse bonuses for mounted archers.  they aren't
# supposed to be charging into the fray.
(try_begin),
(eq, ":tr_type", kt_troop_type_mtdarcher),
(assign, ":h_val", 0),
(try_end),
# mul by stack size
(val_mul, ":eek:_val", ":stack_size"),
(val_mul, ":d_val", ":stack_size"),
(val_mul, ":h_val", ":stack_size"),
(else_try),
# todo:  heroes have different rules since they don't have troop
# templates.  for now, we'll use 50 + level*3 for o_val and
# 20 + level*2 for d_val.  in the future, this should be replaced
# by a gear lookup.
(store_character_level, ":level", ":stack_troop"),
(store_mul, ":eek:_val", ":level", 3),
(val_add, ":eek:_val", 50),
(store_mul, ":d_val", ":level", 2),
(val_add, ":d_val", 20),
(try_end),

# siege checks
(try_begin),
# if not sieging, mounted guys get a bonus.
(eq, ":is_siege", 0),
(try_begin),
# mounted archers only get 50% more defense
(eq, ":tr_type", kt_troop_type_mtdarcher),
(val_mul, ":d_val", 3),
(val_div, ":d_val", 2),
(else_try),
# cavalry get 50% more attack and defense and add h_val to o_val
(eq, ":tr_type", kt_troop_type_cavalry),
(val_mul, ":eek:_val", 3),
(val_div, ":eek:_val", 2),
(val_add, ":eek:_val", ":h_val"),
(val_mul, ":d_val", 3),
(val_div, ":d_val", 2),
(try_end),
(val_add, ":eek:_val", ":h_val"),
(try_end),

# add stuff up
(val_add, reg0, ":eek:_val"),
(val_add, reg1, ":d_val"),
(val_add, reg2, ":stack_size"),
(try_end),

# calculate damage redux from defense
(val_div, reg1, reg2), # avg defense
(val_clamp, reg1, 0, 90), # values outside this range don't work well
(store_sub, reg1, 100, reg1), # opponent offense should be multiplied by this %
]),

# kt0:  this is a helper that basically calls kt_party_calculate_strength
# for each attachment to the given party. 
# INPUT:
# arg1:  party_id
# arg2:  exclude leader of given stack (not attachments)
# arg3:  is_siege
# OUTPUT:
# reg0:  aggregate strength
# reg1:  number of attached parties
( "kt_party_calculate_strength_with_attachments",
[
# remember our params and set some initial values
(store_script_param_1, ":root_party"),
(store_script_param_2, ":exclude_leader"),
(store_script_param, ":is_siege", 3),

# call the counting script for the given party
(call_script, "script_kt_party_calculate_strength", ":root_party", ":exclude_leader", ":is_siege"),
(assign, ":strength_so_far", reg0),
(assign, ":def_so_far", reg1),
(assign, ":count_so_far", reg2),
(val_mul, ":def_so_far", ":count_so_far"),

# for every attached party, do the same
(party_get_num_attached_parties, ":attached_count", ":root_party"),
(try_for_range, ":rank", 0, ":attached_count"),
(party_get_attached_party_with_rank, ":attached_party", ":root_party", ":rank"),
(call_script, "script_kt_party_calculate_strength", ":attached_party", 0, ":is_siege"),
(val_add, ":strength_so_far", reg0),
(store_mul, ":def_this_party", reg1, reg2),
(val_add, ":def_so_far", ":def_this_party"),
(val_add, ":count_so_far", reg2),
(try_end),

# fill out our returns
(assign, reg0, ":strength_so_far"),
(val_div, ":def_so_far", ":count_so_far"),
(assign, reg1, ":def_so_far"),
(assign, reg2, ":count_so_far"),
]),

# kt0:  there seem to be multiple ways to calculate how many fit troops
# there are in an encounter and they all do something slightly different
# but seem to be used for the same things.  this is a simple consolidation
# attempt that counts guys the same way that we calculate party strengths.
# INPUT: 
# arg1:  party_id
# arg2:  exclude leader
# OUTPUT:
# reg0:  viable troop count
( "kt_count_viable_troops",
[
# remember our params
(store_script_param_1, ":party"), # party id
(store_script_param_2, ":exclude_leader"), # also a party id apparently

# clear out our return
(assign, reg0, 0),

# figure out which stack to start with and how many we have
(party_get_num_companion_stacks, ":num_stacks", ":party"),
(assign, ":first_stack", 0),
(try_begin),
(neq, ":exclude_leader", 0),
(assign, ":first_stack", 1),
(try_end),

(try_for_range, ":i_stack", ":first_stack", ":num_stacks"),
(party_stack_get_troop_id, ":stack_troop", ":party", ":i_stack"),
(party_stack_get_size, ":stack_size",":party",":i_stack"),
(party_stack_get_num_wounded, ":num_wounded",":party",":i_stack"),
(val_sub, ":stack_size", ":num_wounded"),
(try_begin),
(gt, ":stack_size", 0),
(try_begin),
# if this stack is a hero, check health vs. the viable thresh.
(troop_is_hero, ":stack_troop"),
(neg|troop_is_wounded, ":stack_troop"),
(val_add, reg0, 1),
(else_try),
# otherwise just add
(val_add, reg0, ":stack_size"),
(try_end),
(try_end),
(try_end),

# reg0 should have the battle-ready count
]),

# kt0:  this is a helper that basically calls kt_count_viable_troops for
# each attachment to the given party.  if the party has no attachments,
# it just returns the given party's count.
# INPUT:
# arg1:  party_id
# arg2:  exclude leader of given stack (not attachments)
# OUTPUT:
# reg0:  viable troop count
# reg1:  number of attached parties
( "kt_count_viable_troops_with_attachments",
[
# remember our params and set some initial values
(store_script_param_1, ":root_party"),
(store_script_param_2, ":exclude_leader"),

# call the counting script for the given party
(call_script, "script_kt_count_viable_troops", ":root_party", ":exclude_leader"),
(assign, ":count_so_far", reg0),

# for every attached party, do the same
(party_get_num_attached_parties, ":attached_count", ":root_party"),
(try_for_range, ":rank", 0, ":attached_count"),
(party_get_attached_party_with_rank, ":attached_party", ":root_party", ":rank"),
(call_script, "script_kt_count_viable_troops", ":attached_party", 0),
(val_add, ":count_so_far", reg0),
(try_end),

# fill out our returns
(assign, reg0, ":count_so_far"),
(assign, reg1, ":attached_count"),
]),

New slots in module_constants.py again, not all of them used:
kt_slot_troop_1hprof = 200
kt_slot_troop_2hprof = 201
kt_slot_troop_poleprof = 202
kt_slot_troop_archprof = 203
kt_slot_troop_xbowprof = 204
kt_slot_troop_thrwprof = 205
kt_slot_troop_str = 206
kt_slot_troop_agi = 207
kt_slot_troop_int = 208
kt_slot_troop_cha = 209
kt_slot_troop_pstrike = 210
kt_slot_troop_pdraw = 211
kt_slot_troop_pthrow = 212
kt_slot_troop_shield = 213
kt_slot_troop_atheltics = 214
kt_slot_troop_ironflesh = 215
kt_slot_troop_o_val = 230
kt_slot_troop_d_val = 231
kt_slot_troop_h_val = 232
kt_slot_troop_type = 233

# kt_slot_troop_type values
kt_troop_type_footsoldier = 0 # !tf_guarantee_horse AND !tf_guarantee_ranged
kt_troop_type_cavalry = 1 # !tf_guarantee_ranged AND tf_guarantee_horse
kt_troop_type_archer = 2 # tf_guarantee_ranged AND !tf_guarnatee_horse
kt_troop_type_mtdarcher = 3 # tf_guarantee_ranged AND tf_guarantee_horse

This is code for game_event_simulate_battle.  Insert this stuff after the two party_collect_attachments_to_party calls that fill out p_collective_ally and p_collective_enemy.  It replaces the code until you hit a try_begin followed by (this_or_next|eq, ":new_attacker_strength", 0).
          (assign, ":is_siege", 0),
          (try_begin),           
            (this_or_next|party_slot_eq, ":root_defender_party", slot_party_type, spt_castle),
            (party_slot_eq, ":root_defender_party", slot_party_type, spt_town),
            (assign, ":is_siege", 1),
          (try_end),
         
          (call_script, "script_kt_party_calculate_strength", "p_collective_ally", 0, ":is_siege"),
          (assign, ":defender_strength", reg0),
          (assign, ":defender_defense", reg1),
          (call_script, "script_kt_party_calculate_strength", "p_collective_enemy", 0, ":is_siege"),
          (assign, ":attacker_strength", reg0),
          (assign, ":attacker_defense", reg1),
                               
          # For sieges increase attacker casualties and reduce defender casualties.
          (try_begin),           
            (this_or_next|party_slot_eq, ":root_defender_party", slot_party_type, spt_castle),
            (party_slot_eq, ":root_defender_party", slot_party_type, spt_town),
            (val_mul, ":defender_strength", 3),
            (val_div, ":defender_strength", 2),
            (val_div, ":attacker_strength", 2),
          (try_end),
                     
          # calculate damage values given average defense
          (store_mul, ":defender_adjusted_damage", ":attacker_defense", ":defender_strength"),
          (store_mul, ":attacker_adjusted_damage", ":defender_defense", ":attacker_strength"),
          (val_div, ":defender_adjusted_damage", 100),
          (val_div, ":attacker_adjusted_damage", 100),
         
          # normalize values to make battles go more slowly
          # a normal party with ~100 guys typically generates an attack
          # value around 5000.  this should be twice what Native was
          # with the added bonus that we don't cap the upper bound.
          (val_div, ":attacker_adjusted_damage", 50),
          (val_div, ":defender_adjusted_damage", 50),
          (val_max, ":attacker_adjusted_damage", 1),
          (val_max, ":defender_adjusted_damage", 1),
         
          (try_begin),
            (inflict_casualties_to_party_group, ":root_attacker_party", ":defender_adjusted_damage", "p_temp_casualties"),
            (party_collect_attachments_to_party, ":root_attacker_party", "p_collective_enemy"),
          (try_end),
          (call_script, "script_party_count_fit_for_battle", "p_collective_enemy", 0),
          (assign, ":new_attacker_strength", reg0),

          (try_begin),
            (gt, ":new_attacker_strength", 0),
            (inflict_casualties_to_party_group, ":root_defender_party", ":attacker_adjusted_damage", "p_temp_casualties"),
            (party_collect_attachments_to_party, ":root_defender_party", "p_collective_ally"),
          (try_end),
          (call_script, "script_party_count_fit_for_battle", "p_collective_ally", 0),
          (assign, ":new_defender_strength", reg0),

This code replaces the entirety of castle_attack_walls_simulate in module_game_menus.py.  It includes the bugfix for attachments and a handy display for number of remaining fit troops.
#
# mnu_castle_attack_walls_simulate
# kt0:  heavily modified for correctness (now considers attachments).
#
(
"castle_attack_walls_simulate",
mnf_scale_picture | mnf_disable_all_keys,
"{s4}^^Your casualties:{s8}^^Enemy casualties were: {s9}^^Remaining allies: {reg10}^Remaining enemies: {reg11}",
"none",
[
(troop_get_type, ":is_female", "trp_player"),
(try_begin),
(eq, ":is_female", 1),
(set_background_mesh, "mesh_pic_siege_sighted_fem"),
(else_try),
(set_background_mesh, "mesh_pic_siege_sighted"),
(try_end),

# grab party strengths and weight for attackers and defenders
(call_script, "script_kt_party_calculate_strength_with_attachments", "p_main_party", 1, 1), # skip player and is_siege
(assign, ":p_str", reg0),
(assign, ":p_def", reg1),
(val_mul, ":p_str", 3),
(val_div, ":p_str", 4), # attacker strength penalty

(call_script, "script_kt_party_calculate_strength_with_attachments", "$g_encountered_party", 0, 1),
(assign, ":e_str", reg0),
(assign, ":e_def", reg1),
(val_mul, ":e_str", 3),
(val_div, ":e_str", 2),

# adjust for defense values
(val_mul, ":e_str", ":p_def"),
(val_mul, ":p_str", ":e_def"),
(val_div, ":e_str", 100),
(val_div, ":p_str", 100),

# slow the battle down so the player can make choices
# attacking a castle goes faster than overland battles.
(val_div, ":e_str", 10),
(val_div, ":p_str", 10),

# debughax
(assign, reg0, ":e_str"),
(assign, reg1, ":p_str"),
(display_message, "@going to solver:  e_str:  {reg0}, p_str:  {reg1}", 0xFFFFFF00),

# hurt both sides
(inflict_casualties_to_party_group, "p_main_party", ":e_str", "p_temp_casualties"),
(call_script, "script_print_casualties_to_s0", "p_temp_casualties", 0),
(str_store_string_reg, s8, s0),

(inflict_casualties_to_party_group, "$g_encountered_party", ":p_str", "p_temp_casualties"),
(call_script, "script_print_casualties_to_s0", "p_temp_casualties", 0),
(str_store_string_reg, s9, s0),

# fill out remaining troops
(call_script, "script_kt_count_viable_troops_with_attachments", "p_main_party", 1), # don't count the player
(assign, ":allies_left", reg0),
(assign, reg10, reg0),
(call_script, "script_kt_count_viable_troops_with_attachments", "$g_encountered_party", 0),
(assign, ":enemies_left", reg0),
(assign, reg11, reg0),

# determine if we're still fighting or what for the next menu
(assign, "$no_soldiers_left", 0),
(try_begin),
(le, ":allies_left", 0),
(assign, "$no_soldiers_left", 1),
(str_store_string, s4, "str_attack_walls_failure"),
(else_try),
(le, ":enemies_left", 0),
(assign, "$no_soldiers_left", 1),
(assign, "$g_battle_result", 1),
(str_store_string, s4, "str_attack_walls_success"),
(else_try),
(str_store_string, s4, "str_attack_walls_continue"),
(try_end),
],
[
("continue",[],"Continue...",[(jump_to_menu,"mnu_castle_besiege")]),
]),

This code replaces the entirety of order_attack_2 in module_game_menus.py.  It also includes a display for the number of remaining troops.
  (
"order_attack_2",mnf_disable_all_keys,
"{s4}^^Your casualties: {s8}^^Enemy casualties: {s9}^^Allies remaining: {reg10}^Enemies remaining: {reg11}",
"none",
[
# kt0:  heavily modified to use the new strength calculation stuff.
(call_script, "script_kt_party_calculate_strength", "p_main_party", 1, 0), # no player, not a siege
(assign, ":player_party_strength", reg0),
(assign, ":player_party_defense", reg1),

(call_script, "script_kt_party_calculate_strength", "p_collective_enemy", 0, 0), # leader leading, not a siege
(assign, ":enemy_party_strength", reg0),
(assign, ":enemy_party_defense", reg1),

# normalize strengths for defense
(val_mul, ":player_party_strength", ":enemy_party_defense"),
(val_mul, ":enemy_party_strength", ":player_party_defense"),
(val_div, ":player_party_strength", 100),
(val_div, ":enemy_party_strength", 100),

# slow down the fight so the player can make choices between each
# round.  note that player fights go faster than fights between AI
# parties.  this is intentional:  it gives the player time to
# become involved.
(val_div, ":player_party_strength", 25),
(val_div, ":enemy_party_strength", 25),

(inflict_casualties_to_party_group, "p_main_party", ":enemy_party_strength", "p_temp_casualties"),
(call_script, "script_print_casualties_to_s0", "p_temp_casualties", 0),
(str_store_string_reg, s8, s0),

(inflict_casualties_to_party_group, "$g_encountered_party", ":player_party_strength", "p_temp_casualties"),
(call_script, "script_print_casualties_to_s0", "p_temp_casualties", 0),
(str_store_string_reg, s9, s0),

(party_collect_attachments_to_party, "$g_encountered_party", "p_collective_enemy"),

# calculate aftermath so we can display stuff
(call_script, "script_party_count_members_with_full_health","p_main_party"),
(assign, reg10, reg0),
(call_script, "script_party_count_members_with_full_health","p_collective_enemy"),
(assign, reg11, reg0),

(assign, "$no_soldiers_left", 0),
(try_begin),
(le, reg10, 0),
(assign, "$no_soldiers_left", 1),
(str_store_string, s4, "str_order_attack_failure"),
(else_try),
(le, reg11, 0),
(assign, ":continue", 0),
(party_get_num_companion_stacks, ":party_num_stacks", "p_collective_enemy"),
(try_begin),
(eq, ":party_num_stacks", 0),
(assign, ":continue", 1),
(else_try),
(party_stack_get_troop_id, ":party_leader", "p_collective_enemy", 0),
(try_begin),
(neg|troop_is_hero, ":party_leader"),
(assign, ":continue", 1),
(else_try),
(troop_is_wounded, ":party_leader"),
(assign, ":continue", 1),
(try_end),
(try_end),
(eq, ":continue", 1),
(assign, "$g_battle_result", 1),
(assign, "$no_soldiers_left", 1),
(str_store_string, s4, "str_order_attack_success"),
(else_try),
(str_store_string, s4, "str_order_attack_continue"),
(try_end),
],
    [
      ("order_attack_continue",[(eq, "$no_soldiers_left", 0)],"Order your soldiers to continue the attack.",[
          (jump_to_menu,"mnu_order_attack_2"),
          ]),
      ("order_retreat",[(eq, "$no_soldiers_left", 0)],"Call your soldiers back.",[
          (jump_to_menu,"mnu_simple_encounter"),
          ]),
      ("continue",[(eq, "$no_soldiers_left", 1)],"Continue...",[
          (jump_to_menu,"mnu_simple_encounter"),
          ]),
    ]
  ),

Updated offense and defense values now with divined type:
        farmer 17 11 2 footsoldier
        townsman 19 12 2 footsoldier
        watchman 32 36 4 footsoldier
        caravan_guard 30 40 6 cavalry
        mercenary_swordsman 44 51 10 cavalry
        hired_blade 68 57 13 cavalry
        mercenary_crossbowman 51 29 9 archer
        mercenary_horseman 44 50 10 cavalry
        mercenary_cavalry 55 55 13 cavalry
        mercenaries_end 0 7 2 footsoldier
        swadian_recruit 16 22 2 footsoldier
        swadian_militia 30 34 4 footsoldier
        swadian_footman 32 41 6 footsoldier
        swadian_infantry 37 50 10 footsoldier
        swadian_sergeant 69 59 13 footsoldier
        swadian_skirmisher 34 28 6 archer
        swadian_crossbowman 47 28 9 archer
        swadian_sharpshooter 57 45 12 archer
        swadian_man_at_arms 48 51 10 cavalry
        swadian_knight 67 64 13 cavalry
        swadian_messenger 64 16 13 mtdarcher
        swadian_deserter 36 30 6 archer
        swadian_prison_guard 61 61 13 footsoldier
        swadian_castle_guard 63 61 13 footsoldier
        vaegir_recruit 15 26 2 footsoldier
        vaegir_footman 26 35 4 footsoldier
        vaegir_skirmisher 32 27 6 archer
        vaegir_archer 46 30 9 archer
        vaegir_marksman 70 35 12 archer
        vaegir_veteran 30 42 6 footsoldier
        vaegir_infantry 42 52 9 footsoldier
        vaegir_guard 55 59 12 footsoldier
        vaegir_horseman 38 51 9 cavalry
        vaegir_knight 56 59 12 cavalry
        vaegir_messenger 89 16 13 mtdarcher
        vaegir_deserter 43 27 6 archer
        vaegir_prison_guard 49 56 12 footsoldier
        vaegir_castle_guard 52 56 12 footsoldier
        khergit_tribesman 20 17 2 footsoldier
        khergit_skirmisher 49 27 4 mtdarcher
        khergit_horseman 53 34 6 mtdarcher
        khergit_horse_archer 54 39 6 mtdarcher
        khergit_veteran_horse_archer 89 46 10 mtdarcher
        khergit_lancer 76 51 12 cavalry
        khergit_messenger 81 16 13 mtdarcher
        khergit_deserter 43 27 6 archer
        khergit_prison_guard 41 54 12 footsoldier
        khergit_castle_guard 42 54 12 footsoldier
        nord_recruit 16 24 3 footsoldier
        nord_footman 50 39 4 footsoldier
        nord_trained_footman 43 51 6 footsoldier
        nord_warrior 75 54 9 footsoldier
        nord_veteran 103 57 12 footsoldier
        nord_champion 144 63 16 footsoldier
        nord_huntsman 25 20 5 archer
        nord_archer 45 26 7 archer
        nord_veteran_archer 59 32 9 archer
        nord_messenger 86 16 13 mtdarcher
        nord_deserter 42 27 6 archer
        nord_prison_guard 46 55 12 footsoldier
        nord_castle_guard 46 58 12 footsoldier
        rhodok_tribesman 14 23 2 footsoldier
        rhodok_spearman 37 38 4 footsoldier
        rhodok_trained_spearman 46 46 6 footsoldier
        rhodok_veteran_spearman 57 51 9 footsoldier
        rhodok_sergeant 72 67 12 footsoldier
        rhodok_crossbowman 50 31 4 archer
        rhodok_trained_crossbowman 56 45 7 archer
        rhodok_veteran_crossbowman 68 48 10 archer
        rhodok_sharpshooter 80 54 13 archer
        rhodok_messenger 85 16 13 mtdarcher
        rhodok_deserter 42 27 6 archer
        rhodok_prison_guard 45 57 12 footsoldier
        rhodok_castle_guard 47 57 12 footsoldier
        looter 7 11 2 footsoldier
        bandit 22 23 4 footsoldier
        brigand 38 29 7 cavalry
        mountain_bandit 46 31 5 footsoldier
        forest_bandit 49 17 5 archer
        sea_raider 73 46 7 footsoldier
        steppe_bandit 62 29 5 mtdarcher
        black_khergit_horseman 59 32 10 cavalry
        manhunter 12 22 4 footsoldier
        slave_driver 20 22 6 footsoldier
        slave_hunter 22 22 9 footsoldier
        slave_crusher 43 22 11 cavalry
        slaver_chief 51 29 14 cavalry
        follower_woman 28 24 2 footsoldier
        hunter_woman 32 24 4 footsoldier
        fighter_woman 39 31 7 footsoldier
        sword_sister 59 55 11 cavalry
        refugee 11 10 1 footsoldier
        peasant_woman 8 9 1 footsoldier
        caravan_master 37 20 4 cavalry
        kidnapped_girl 0 9 1 footsoldier
        town_walker_1 0 13 2 footsoldier

So!  Now that we've got all of that out of the way, I've done a bare minimum of testing with this stuff so it's very likely that there are bugs (really, goes without saying).  I also haven't replaced the last three autoresolve functions because I can't easily test them.  Those are:  siege_join_defense, join_order_attack, castle_attack_walls_with_allies_simulate.  Furthermore, I haven't done the work of storing item values on item slots yet so I wasn't able to do anything smart with heroes in the strength calc (I hacked it with level).  This is left as an exercise for the reader.

Other things of note:
  • I'm guessing on how equipment is chosen from the list.  It isn't 100% clear how it works from what I've seen.  With more concrete data, it shoudl be relatively easy to make the o_val and d_val calculation better.
  • Each of the three autoresolve mods posted above resolves at a different rate.  This was done intentionally but it's a matter of taste, really.  Overland AI battles as solved with game_event_simulate_battle are the slowest to give the player time to join if they want.  Overland battles for the player are the next slowest to allow the player time to get while the gettin's good.  Autoresolve fights against a structure are the fastest because it felt more correct that way.  I figured that disengagement was harder in that situation.
  • I was most worried about the adjustments made to defender strength when autoresolving a castle.  So in a completely non-scientific test, I sent the same group while being led by me hiding in a corner.  It was roughly 140 huscarls vs. 160 Vaegir defenders at Nelag castle.  I won every time with about 90 guys left give or take a few both with autoresolving and leading my troops from a shadey corner.  I'll call that a result.
  • I haven't done a ton of testing with cavalry overland.  It probably needs tweaking.
 
party_collect_attachments_to_party - what those "attachments" are and what does this operation do with them?
 
As I understand it indirectly since we don't have the source for that function, party_collect_attachments_to_party grabs all the friendly parties in the area inclined to pitch in to a battle and stuffs all the stacks into a new party, typically p_collective_ally, p_collective_enemy, and p_collective_friends.  This manifests itself nicely in the bug I fixed in castle_attack_walls_simulate where it would only consider the guys guarding the castle directly and not the attached lord parties resting at the castle.  In fact, I only by chance stumbled upon it by trying to add the numbers display and trying to puzzle through why they never came out correctly. 

Confusingly, inflict_casualties_to_party_group does traverse attachments hence why it's conceivable that the original implementors thought it was working correctly since all the parties at the center take damage.  I suspect that there are other bugs of this nature lurking in the code that we haven't yet stumbled upon. 

You can find a short discussion we had on the topic here:  http://forums.taleworlds.com/index.php/topic,59757.msg1551602.html#msg1551602
 
Trying to use these scripts (just restarted to work on SoD after a long break) I get the following error :

Invalid object _val . Variables should start with $ sign (...)
(I've not modified the scripts for the moment, I was just testing a copy paste of the last version in SoD code ; I don't think I've misplaced the code)

I didn't find any object with a name starting with _val (but a lot of d_val, o_val, etc... in the true python code in the beginning).
 
I'm stupid, it was just my error copying-pasting stuff from the forum with smileys activated.  :oops:
 
Back
Top Bottom