User Tools

Site Tools


user:loggy:castingaiexample

Casting AI worked example

This will on several occasions walk through the processes described on this page, it might be useful to keep both open.

The colossus and the werewolf

This was run on 5.54. In 5.57 the cached offensive flag was replaced by a simple switch to an offensive effect number, meaning that this part of the discussion no longer applies

I wrote primarily for myself to better understand this case. In places it won't be structured logically, but rather it represents how I thought about the discrepancy.

Picture the following:

The finer details of these gear setups are not too important. The green clouds are from a popped mossbody (the werewolf broke moss with a smasher, but that's not relevant for this analysis).

The colossus here (after buffs and items) is air 55water 44earth 44astral 44nature 44 with 27 strength, 32 protection, 24 shock res, 12 precision.

The werewolf here (also after buffs and items) has 30 shock res, 31 strength, 25 protection.

If you owned the colossus and could have it cast any spell you wanted, you'd probably choose fists of iron and direct all of its effects into the werewolf, as with these stats it would be extremely likely to just outright delete the werewolf in a single cast and win the battle.

What the casting AI decided to do is instead cast shock wave at the square marked X, with coordinates 22, 8. The numbers in orange denote the coordinates of the other squares containing enemies, which the AI trial-runs its casts at. But more on that later.

Note that this analysis references two debug log lines that do not normally exist: I modified the executable to make it write these

As I am running this in debug level 2, any replay desyncs are made very obvious by messages in the top left of combat. Based on this and no observable differences in the playback of the fight in question (which was a Clockwork multiplayer game, hosted on a normal executable) I am fairly confident that my increased reporting has no adverse effects on gameplay.

Shock Wave, AI calculation

The log:

spellunitscore against no one nr 87264: 120
spellunitscore against Large Spider nr 15: -10
spellunitscore against Large Scorpion nr 18: -10
spellunitscore against Frog nr 20: -10
spellunitscore against Red Ant nr 9: -10
best Shock Wave this far, 20 8 (585 pnts) (midedge 120 60, marg 1, pot 6, hostile 1)
spellunitscore against no one nr 87264: 120
spellunitscore against no one nr 87264: 120
best Shock Wave this far, 22 8 (1105 pnts) (midedge 120 60, marg 1, pot 6, hostile 1)
spellunitscore against Bronze Colossus nr 25006: 0
inferior Shock Wave, 22 9 (221 pnts) (midedge 120 60, marg 1, pot 6, hostile 1)
spellunitscore against no one nr 87264: 120
spellunitscore against no one nr 87264: 120
inferior Shock Wave, 24 10 (1075 pnts) (midedge 120 60, marg 1, pot 6, hostile 1)
spellunitscore against Jotun Werewolf nr 2157: 0
inferior Shock Wave, 22 11 (68 pnts) (midedge 120 60, marg 1, pot 6, hostile 1)
inferior Shock Wave, 23 11 (218 pnts) (midedge 120 60, marg 1, pot 6, hostile 1)
spellscore, Shock Wave score 1089 (boost 99 scorat 0)
Eval: Shock Wave score 1183 (fat 2)
comp_castspell: eval Shock Wave  result 1183
best spell so far  Shock Wave (score1183)

There's a fair amount going on here, both in what is written and what is not.

Importantly there are blocks of scores against individual units, followed by a "best Shock Wave this far" or "inferior Shock Wave" line, which details the lists the coordinates being checked and the final score it got for casting at this position. There's also more numbers in brackets after the score (or "pnts") but this can safely be ignored here.

The other twist is that the game caches scores against squares, that is to say that it will only calculate shock wave once per square it can possibly affect. When reading cached values NOTHING is written to the log, which makes following what is going on a bit harder.

But, in this situation, simulating shock wave will always consider a square spread of 1, IE a 3x3 box around the primary square being targeted. Let's start by running through the chosen outcome, targeting (22, 8).

Here, the simulation sees the imp in the primary square, and the imp in the square between that and the colossus (22, 9). The log tells us that the score for the shock on a single imp is 120 (my hack to get the extra debug line is not perfect and sometimes fails to get the unit being referred to, in this case they are the "no ones").

Rather than simply add the two 120 results together for the two imps, the game takes into account that the middle square is by far the most likely to be hit. Specifically, it does this by multiplying the middle square scores by a larger number (20 * spell aoe) than the outside squares (10 * spell aoe). Once all the scores are added up, the result is then divided by 100.

Shock wave's AoE is 6. Thus the score for the two imps is 120 * 20 * 6 + 120 * 10 * 6 = 21600, or 216 when divided by 100.

This score than has a closed d10 added to it, then has 5 subtracted from it. Due to the small average change, we'll ignore this for now.

As the spell hits enemies and the targeted square is within 10 of the colossus (the actual distance is 2), we multiply it by 10 and then divide by the distance, giving a net multiplication of 5 and a final score of 1080. The log gave a result of 1105, which means that the closed d10 rolled the maximum 10 in this case.

The log later does make this become 1089 when applying the randomness for all spell results later, but that is not important for this analysis.

We can see that, for the analogous square (24,10), the log result is 1075, which is consistent with a d10 result of 4. Needless to say that doesn't beat the maximum d10 roll above, so the choice here can be easily explained.

The mystery is, given that the log clearly states that the scores for the werewolf and colossus are 0 due to their shock resistance, why is the score for (23,11) only 218, about a fifth as large? It would seem logical that it also falls into the 1080 ± 25 result calculated above, but something is altering it.

Despite what the debug log line made (the "inferior Spell"… line was added by me, and I likely either don't understand what hostile 1 means or loaded the wrong value onto the stack for it, this was my first bit of fun writing assembly and I found it hard), the most likely explanation is that the cast at (23,11) is not recognised as hostile due to drawing all its score values from the cache, which is very likely a bug. This means that it skips getting the net multiplication of 5, which would give a final calculated score of 216 ± 5 - which fits with the log result.

The simulation at (22, 11) may also suffer from this, as the hostile flag is only set if the final score is greater than 0. This did run a fresh score, but against the werewolf where the flag would not have been set.

Only target is the imp in the secondary square
120 * 10 * 6 = 7200 / 100 = 72

When given the random ±5 this is consistent with the log reported value of 68.

Fists of Iron, the ignored spell

With the case closed on the shock wave portion of the log, let's examine what it had to say about Fists of Iron.

spellunitscore against no one nr 87264: 70
spellunitscore against no one nr 87264: 70
spellunitscore against Bronze Colossus nr 25006: -70
best Fists of Iron this far, 22 9 (275 pnts) (midedge 80 40, marg 1, pot 3, hostile 1)
spellunitscore against Jotun Werewolf nr 2157: 120
spellunitscore against no one nr 87264: 70
best Fists of Iron this far, 22 11 (460 pnts) (midedge 80 40, marg 1, pot 3, hostile 1)
spellunitscore against no one nr 87264: 70
best Fists of Iron this far, 23 11 (515 pnts) (midedge 80 40, marg 1, pot 3, hostile 1)
spellscore, Fists of Iron score 483 (boost 93 scorat 0)
Eval: Fists of Iron score 497 (fat 7)
comp_castspell: eval Fists of Iron  result 497
loser spell Fists of Iron (score 497)

Fists of iron inflicts 16 damage, +1 per additional caster level, and adds the caster's strength. When used by the colossus at earth 44, it inflicts 19 + the 27 strength = 46 points of magical blunt damage.

Additionally, it has one effect per caster level, so this colossus has a total of four "swings". For reference, the jotun has 25 protection and 53 hp plus 4 undying. Despite this, 4x(46-25) = 84, producing enough expected damage barring limb caps to kill the wolf in a single cast, and yet the casting AI chalks this up to be a bit under half as useful as shock wave.

Upon reading this log, there are a few things to consider:

First, the colossus is considering that it can hit itself. Based on my notes, I think this can technically happen but the risk is very very minimal:

 12 colossus base precision
 +5 fists of iron precison bonus
 -4 per number of effects
 = 13
 /3, 2% chance = 4
 if (4/2)-2 < distance, IE 1, deviation will occur. With these parameters, the deviation will be 1 square
 So there is a 2/(100*9) per swing (IE: 0.22% chance) to hit himself.

Second, the lethality of the spell is lost in these calculations. This is likely the result of both issues in spellunitscore and also due to shock wave's square weights being multiplied by the area of effect.

spellunitscore results and issues in this situation

We see that the spell effect scores 70 vs imps, -70 against the colossus, and 120 against the werewolf. Shock wave on the other hand scores 120 vs imps. But where did these come from?

Imps have 8 hp, 9 strength, and 6 protection. This means that the cap of (max hp + 5) limits damage to 13 against them. The other values come into play later.

Fists of Iron is treated poorly in multiple ways here. First, the fact the caster's strength is added is not respected means the colossus is considered to do 19 damage and not 46. Second, as a single target spell that does not ignore shields, its damage is reduced to 2/3, rounded down.

As such, its calculations proceed as follows:

Imp

19 - Damage after E4 caster level
As it is fully protection respecting we add (1 - target's prot), IE -5
= 14
Apply the max hp +5 limit, reduced to 13
Reduce to 2/3, rounded down = 8
Next, we calculate the strength value, damage + (target's strength) - 10
= 8 + 9 - 10 = 7
The score for damaging effects is 10 * strength value = 70

Jotun Werewolf

19 - Damage after E4 caster level
As it is fully protection respecting we add (1 - target's prot), IE -24
= -5
Max hp+5 limit does not apply
Reduce to 2/3, rounded towards 0 = -3
Next, we calculate the strength value, damage + (target's chassis base strength) - 10
= -3 + 25 - 10 = 12
The score for damaging effects is 10 * strength value = 120

Shock wave vs imps, on the other hand…

12 - Damage after A5 caster level
As it is AN we add 1
= 13
Apply the max hp +5 limit, no effect
Next, we calculate the strength value, damage + (target's strength) - 10
= 13 + 9 - 10 = 12
The score for damaging effects is 10 * strength value = 120

Hypotheticals and Blade Wind

AKA: "Why does the casting AI love to cast Blade Wind?"

A lot of the values that come up in my rather lengthy and dry notes aren't going to be really meaningful, so let's pick a competitor, something like say the fairly humble Falling Fires. Notably, Blade Wind is 4x the fatigue cost, and gets penalised for that, but let's forget about that for now. I'll get back to that later if I remember.

For the sake of this, I'm assuming "base" casting levels (that is fire 33earth 33), which means the spells have the following stats. The casting AI does resolve the scaling correctly for the caster when running its numbers, but that's a needless complication here.

  • Falling fires: aoe 3, nreff 1, 15pts AP fire
  • Blade wind: aoe 0, nreff 35, 14pts mundane slashing

Let's assume (as is the case in most battles) that we're not dealing with things within 5 range of the mage, as that uses different stuff for blade wind. For AoE spreading, both of them fall under…

 If the spell's AoE is 0 or 1, OR the spell's number of effects is 1:
     square spread = 1
     accurate square weight = max(1, aoe) * number of effects * 20
     inaccurate square weight = max(1, aoe) * number of effects * 10

This means mages are looking at 3x3 boxes (1 square deviation in all directions from the square being considered) of units. The weights (more on what these are used for later are):

Falling fires: accurate = 60, inaccurate = 30 Blade wind: accurate = 700, inaccurate = 350

Unit Scores

Now we're onto unit scores, which are a numeric value of how much the AI rates casting the spell on one specific unit - how many are then in the considered area is the basis of the spell score. Let's, for the sake of examples, look at fully packed squares of two unit types. For the sake of an example, I'm picking Barbarian and Wolf. I originally set out to do this analysis with Heavy Infantry vs the Barbarians but ran into a slight issue. I've left the calcs for the heavy infantry below, which themselves might be interesting…

I'll pull all the relevant values together when they're needed later. The calculation method with real unit numbers is here mostly for curiosity's sake.

Barbarian

7 prot, 13 hp, 12 strength.

Falling Fires
  • This is AP, so we add (1- prot/2) rounded down.
  • Final damage = 15 (base) + (1 - 7/2) = 15 + (1 - 3) = 13.
  • This does not exceed target max hp + 5 (18) so doesn't need to be capped to that value
  • Strength value = 12 (str) - 10 (constant) + 13 (final damage) = 15
  • Final score for damaging effects = strength value * friendly bias (this is just 1 for hostiles, and -1 for friends) * 10.
  • = 150
Blade Wind
  • This isn't AP, so we add (1 - 7) = -6
  • Final damage = 14 - 6 = 8
  • Spell is single target and doesn't ignore shields, multiply by 2/3 and round down = 16/3 = 5
  • Strength value = 12 (str) - 10 (constant) + 5 (final damage) = 7
  • Final score = 70

Wolf

8 hp, 9 str, 2 prot.

Falling Fires
  • This is AP, so we add (1- prot/2) rounded down.
  • Final damage = 15 (base) + (1 - 2/2) = 15 + (1 - 1) = 15.
  • This does exceed target max hp + 5 (13) so gets lowered to this
  • Strength value = 9 (str) - 10 (constant) + 13 (final damage) = 12
  • Final score for damaging effects = strength value * friendly bias (this is just 1 for hostiles, and -1 for friends) * 10.
  • = 120
Blade Wind
  • This isn't AP, so we add (1 - 2) = -1
  • Final damage = 14 - 1 = 13
  • Spell is single target and doesn't ignore shields, multiply by 2/3 and round down = 24/3 = 8
  • Strength value = 9 (str) - 10 (constant) + 8 (final damage) = 7
  • Final score = 70

Heavy Infantry

10 hp, 10 str, 14 prot.

Falling Fires
  • This is AP, so we add (1- prot/2) rounded down.
  • Final damage = 15 (base) + (1 - 14/2) = 15 + (1 - 7) = 9.
  • This does not exceed target max hp + 5 (18) so doesn't need to be capped to that value
  • Strength value = 10 (str) - 10 (constant) + 9 (final damage) = 9
  • Final score for damaging effects = strength value * friendly bias (this is just 1 for hostiles, and -1 for friends) * 10.
  • = 90
Blade Wind
  • This isn't AP, so we add (1 - 14) = -13
  • Final damage = 14 - 13 = 1
  • Spell is single target and doesn't ignore shields, multiply by 2/3 and round down = 2/3 = 0
  • Strength value = 10(str) - 10 (constant) + 0 (final damage) = 0
  • Final score = 0
  • Aka: the casting AI won't want to do this at all!

Square totals

Adapted from the notes:

  Check a square of tiles containing units. As per the above, this is a 3x3 box, for 9 squares total.
  
     - Sum the unit scores of the things in that square.
     - If there was more than one thing in the square, and the spell affects only 1 person, divide the summed score by the number of units in the square
     - If the square struck was the originally picked square at the centre of the 3x3 spread...
       - add (this square's score * accurate square weight) to the spell score at this location. 
     - Otherwise, add (this square's score * inaccurate square weight) to the spell score.

Wolf

Falling Fires

Relevant values from above:

  • Spellunitscore = 120
  • accurate weight = 60
  • inaccurate weight = 30

Onto new calculation:

  • A square of wolves has three of them, so total unit score for each square = 360.
  • Falling Fires has AoE > one person, so we're not dividing anything.
  • Because we're dealing with a 3x3 box of all the same unit, we get 8 "secondary" squares that use the inaccurate weight, and 1 "primary" square that uses the accurate weight, and can just multiply up accordingly
  • = 8 * (360*30) + (60*360) = 86400 + 21600 = 108000
Blade Wind

Relevant values from above:

  • Spellunitscore = 70
  • accurate weight = 700
  • inaccurate weight = 350

Onto new calculation:

  • A square of wolves has three of them, so total unit score for each square = 210.
  • Blade Wind is AoE = one person, so we divide by the number of units in the square and go back to 70 again.
  • Same deal, 8 squares with the inaccurate weight, 1 with the accurate weight.
  • = 8 * (70*350) + (70*700) = 196000 + 49000 = 245000

Barbarian

Falling Fires

Relevant values from above:

  • Spellunitscore = 150
  • accurate weight = 60
  • inaccurate weight = 30

Becomes…

  • One square's total unit score = 450
  • = 8 * (450*30) + (60*450) = 108000 + 27000 = 135000
Blade Wind

Relevant values from above:

  • Spellunitscore = 70
  • accurate weight = 700
  • inaccurate weight = 350

These numbers are exactly the same as the wolves. We end up at 245000 here too.

Theoretical results

If you've read the debug log and seen the spell scores before, you'll notice that these are extremely large compared to what you get there: the game divides them by 100 and throws some randomness in there before working with them. If you opened up the game and actually did this, you'd also run into this, but assuming I haven't fallen flat on my face somewhere along the way, the numbers should line up fairly well. (Both of these units are undisciplined, trying this in reality requires getting fully "dense" formations)

There are other things not included, there's an increase to targeting nearby (but as mentioned above, this changes the weights for blade wind if less than 5 squares away), and a penalty for targeting further away, but that's percentage based and ultimately hits both of the spells we're dealing with here in much the same way.

Unit Blade Wind Falling Fires
Wolf 2450 1080
Barbarian 2450 1350

For the sake of completeness, and to maybe deal with the fact I've gone this deep without even opening up Dominions in anything except a disassembler, I thought I should quickly get some real values for comparison.

Practical Testing

The above mentioned precision and distance factors need taking into account. Dominions does not output the scores before they are applied. Due to the fact this needs 3x3 packed squares, I modded off undisciplined (via #clearspec) for these units to make my life a LOT easier. This doesn't affect their stats or anything else that matters, as per the above.

I empowered a Mound King (it was what I had laying around) and foward positioned him against a bunch of the respective unit types on hold and attack.

Barbarians

 best Falling Fires this far, 26 18 (500 pnts) (midedge 60 30, marg 1, pot 27, hostile 1)
 best Falling Fires this far, 27 18 (518 pnts) (midedge 60 30, marg 1, pot 27, hostile 1)
 best Falling Fires this far, 27 19 (519 pnts) (midedge 60 30, marg 1, pot 27, hostile 1)
 

All of these coordinates are 3x3 boxes packed with barbarians. The caster himself is at (53, 20). The first entry is one square further away than the next two. The total distance between the caster and the middle line (27, 18) is sqrt((53-27)^2 + (20-18)^2) = 26 squares, once rounded down. Even unrounded it's only marginally larger than that anyway (Python's float implementations comes up with 26.076…)

Above I predicted a whopping 1350, and got something a bit under half of what I expected. Notably, it falls foul of:

  If the spell's (NOT FINAL) precision is less than 20:
  score = max(1, (score * 10)/max(10, distance from caster))

Putting 1350 into this gives max(1, (1350*10)/26) = 519. A little randomness later, and we're about where we'd expect to be…

 best Blade Wind this far, 24 17 (443 pnts) (midedge 307 153, marg 2, pot 27, hostile 1)
 best Blade Wind this far, 26 17 (713 pnts) (midedge 307 153, marg 2, pot 27, hostile 1)
 best Blade Wind this far, 25 18 (727 pnts) (midedge 307 153, marg 2, pot 27, hostile 1)
 best Blade Wind this far, 26 18 (914 pnts) (midedge 307 153, marg 2, pot 27, hostile 1)

These scores are highly variable. The highest one, (26, 18), is further away than falling fires we looked at, and comes to 27 squares away after Pythagorus - math.sqrt((53-26)**2 + (20-18)**2) = 27.073

The range modification: 2340*10/27 = 866. Definitely the correct ballpark, but the randomness liekly did its thing - it's set to (80 + openended d40)% of the calculated value, and the casting AI output doesn't output lower scores, meaning this last one might have rolled an especially big d40.

The high fatigue penalty is, usefully, applied after these values are written to the log. In any case, this was the first cast of the battle, and when starting at 0 fatigue neither of these spells suffered from it.

But, how much damage is Blade Wind really going to do in reality anyway?

user/loggy/castingaiexample.txt · Last modified: 2022/12/14 04:18 by loggy