How does a cavalry unit AI decide in evading charges?

Field of Glory II: Medieval

Moderator: rbodleyscott

Post Reply
Atherys
Senior Corporal - Ju 87G
Senior Corporal - Ju 87G
Posts: 85
Joined: Fri Feb 05, 2021 6:56 am

How does a cavalry unit AI decide in evading charges?

Post by Atherys »

I have a few questions regarding cavalry units in deciding if they need to evade or charge:

1. How does the calculation actually work? What's the percentage threshold that decide if a cavalry unit is more beneficial to charge back? Will my cavalry evade if I don't have more than 20% chance to win, for example?
2. Regarding best-equipped cavalries that Mongols have: Sure, they are one of a few lancers that can actually evade charges, but they have a lance. They do count as lancer then, right? Their enemy cavalries (with the exception of knights) will most likely be worse than them in terms of impact, either due to quality, lance POA, armor, or combination of them. Why do they evade when charged by cavalries worse than them?

All these questions assume that my cavalry gets charged frontally instead of from flank/rear and not "light vs non-light".
rbodleyscott
Field of Glory 2
Field of Glory 2
Posts: 28014
Joined: Sun Dec 04, 2005 6:25 pm

Re: How does a cavalry unit AI decide in evading charges?

Post by rbodleyscott »

It is pretty much impossible to distill the decision making code to the simple rule of thumb you would like. It isn't calculated from the % chances anyway, but from raw combat ratings.

Also the chance of evading is also modified by the chance of being caught if they evade.

Also, the unit also tests against other enemy units that might charge them later this turn if they don't evade, so the decision to evade may not be based on the unit that is currently charging them. (A gamey trick in FOG1 was to charge the enemy with a weak unit to trick them into standing, and then charge them with a strong one).

Also, troops who have a better shooting rating than the enemy are more likely to evade, because in the long run it may be better to keep shooting rather than get stuck in melee. (Even if they have a reasonable chance of winning this particular melee eventually - the longer it will take, the more chance of other enemy units intervening).

Also there is a small random element around the cut off point for evading or not.

Here is the actual code: (As you can see, it is not a couple of simple lines)

Code: Select all

	if (evade == 1)
		{
			evadeAP = GetBaseAttrib(me,"AP");
			pursuitAP = GetAttrib(enemy,"AP");

			// See whether unit decides to stand rather than evade      
      
      if ((IsUnitSquadType(me, "Light_Foot") != 1) || (IsLightTroops(enemy) == 1) || (IsRoughOrDifficult(GetUnitX(me),GetUnitY(me)) == 1) || (IsTileEdgeDefendibleObstacle(GetUnitX(me), GetUnitY(me), adjacent_X, adjacent_Y) > -1)) 
      	// v1.5.0 addition. Light foot always evade non-lights unless in rough or difficult or defending obstacle. 
      	// Note that currently they don't automatically evade if alternative chargers could charge them not across an obstacle.
      	{        	
		      if ((IsFlankRearAttack(enemy, me) == 0) || ((IsLightTroops(me) == 0) && (IsLightTroops(enemy) == 1))) // Always evade if charged in flank/rear, unless non-lights being charged by lights
		      // V 1.2.2 change. Note that this does not take into account the guaranteed net +50 POA for a flank charge by lights on non-lights, but as the non-lights are likely to win the melee even
		      // if they disrupt on impact, I don't think this matters.
						{
							// Move enemy to fighting position, calculate combat margin, then move it back to starting position.
							// Note: in the unlikely event that there is a unit there already it will get swapped out then swapped back again.
							current_X = GetUnitX(enemy);
							current_Y = GetUnitY(enemy);
							UnitDeployIfPositionDifferent(enemy, adjacent_X, adjacent_Y, 1);

							Log("Combat margin calc in pos: Chargee x,y, charger x,y", GetUnitX(me), GetUnitY(me), GetUnitX(enemy), GetUnitY(enemy));
							combatMargin = CalculateModifiedCloseCombatRating(me, enemy, enemy, 0, 0) - CalculateModifiedCloseCombatRating(enemy, me, enemy, 0, 0);
							Log("Main charger, combat margin", enemy, combatMargin);
							Log("Pursuit AP", pursuitAP);
							// Don't move the unit back to its start position until after testing other potential chargers, because its combat position may block them from charging

							// Also check if any other nasty enemy could charge them this turn or next (from current position) - if so, and it is nastier than the charger, substitute the combat margin pertaining to it.
							// This is intended to stop players pinning enemy light troops by charging them with something weak enough that they won't evade, and then immediately charging them with something nasty
							// when they are in close combat and can therefore no longer evade.
							// Note, however, does not currently take into account other pursuers that might contact it.
							enemySide = GetEnemySide(me);
							total = GetUnitCount(enemySide);
							for (i = 0; i < total; i++)
							{
								id = GetUnitID(enemySide, i);
								if ((id != -1) && (id != enemy))
									{
										if (GetDistanceBetween(id, me) <= GetBaseAttrib(id, "AP") / 4)
											{
												// Store current values of relevant attributes and set the attribs to start turn condition
												actualAP = GetAttrib(id, "AP");
												SetAttrib(id, "AP", GetBaseAttrib(id, "AP"));
												largeTurn = GetAttrib(id, "MadeLargeTurn");
												SetAttrib(id, "MadeLargeTurn", 0);
												freeTurn = GetAttrib(id, "MadeFreeTurn");
												SetAttrib(id, "MadeFreeTurn", 0);
												shots = GetAttrib(id, "Shots");
												SetAttrib(id, "Shots", GetBaseAttrib(id, "Shots"));

												// Determine whether unit could charge this turn or next turn from current position, and if so whether it is nastier than the original charger.
												if (CallUnitFunctionDirect(id, "CHECK_UNIT_ASSAULT", id, me) >= 0)
													{
														// Move enemy unit to fighting position, calculate combat margin, then move it back to starting position.
														// Note: in the unlikely event that there is a unit there already it will get swapped out then swapped back again.
														unit_X = GetUnitX(me);
														unit_Y = GetUnitY(me);
														potentialCharger_X = GetUnitX(id);
														potentialCharger_Y = GetUnitY(id);
														if (AreTilesAdjacent(potentialCharger_X, potentialCharger_Y, unit_X, unit_Y) == 1)
															{
																adjacent_X = potentialCharger_X;
																adjacent_Y = potentialCharger_Y;
															}
														else
															{
																route = GetRouteCost(id, unit_X, unit_Y, 0, 1);
																length = GetCheckRouteLength();
																adjacent_X = GetCheckRouteX(length-2);
																adjacent_Y = GetCheckRouteY(length-2);
															}

														UnitDeployIfPositionDifferent(id, adjacent_X, adjacent_Y, 1);

														Log("Combat margin calc in pos: Chargee x,y, charger x,y", GetUnitX(me), GetUnitY(me), GetUnitX(id), GetUnitY(id));
														newCombatMargin = CalculateModifiedCloseCombatRating(me, id, id, 0, 0) - CalculateModifiedCloseCombatRating(id, me, id, 0, 0);
														combatMargin = Min(newCombatMargin, combatMargin);
														Log("Unit, new combat margin, worst combat margin", id, newCombatMargin, combatMargin);
														UnitDeployIfPositionDifferent(id, potentialCharger_X, potentialCharger_Y, 1);
														
														// v1.5.8 addition - always evade if effective flank/rear charge from new unit is possible
                            if ((IsFlankRearAttack(id, me) == 1) && ((IsLightTroops(me) == 1) || (IsLightTroops(id) == 0)))
                            	{
                            		definitely_evade = 1;                            		
                            	}													
														// End v1.5.8 addition														
													}

												// Restore previous Attrib values
												SetAttrib(id, "AP", actualAP);
												SetAttrib(id, "MadeLargeTurn", largeTurn);
												SetAttrib(id, "MadeFreeTurn", freeTurn);
												SetAttrib(id, "Shots", shots);
											}
									}
							}

							// Move original charger back to its starting position
							UnitDeployIfPositionDifferent(enemy, current_X, current_Y, 1);

							// v1.5.8 addition
              if (definitely_evade == 0)
              	{
              // End v1.5.8 addition
									desiredMargin = 10 - Rand(0,10); // May need further tweaking
									
									Log("\nShall we evade?: target, charger", me, enemy);

									// Reduce desired margin if light foot being charged by light foot, or light horse by light horse
									if ((IsLightTroops(me) == 1) && (IsLightTroops(enemy) == 1))
										{
											if ((IsMounted(me) == 1) && (IsMounted(enemy) == 1))
												{
													desiredMargin -= 12; // May need tweaking
												}
											if ((IsFoot(me) == 1) && (IsFoot(enemy) == 1))
												{
													desiredMargin -= 12; // May need tweaking
												}
										}
									
									// Reduced desired margin if cavalry, camelry or light chariots	
									if ((IsUnitSquadType(me, "Cavalry") == 1) || (IsUnitSquadType(me, "Camelry") == 1) || (IsUnitSquadType(me, "Light_Chariots") == 1))
										{
				//							desiredMargin -= 12; // may need tweaking.

											Log("Before cavalry anti-evade adjustment: charger, target unit, desiredMargin, combatMargin", me, enemy, desiredMargin, combatMargin);
											
											// v1.3.5 change
											// Don't make anti-evade adjustment if unit has significantly higher shooting rating than charger.
											// (Takes into account current ammunition state, although not how many turns of full ammo left).
				              myShootingRating = CalculateModifiedShootingRating(me, enemy, -1);
				              enemyShootingRating = CalculateModifiedShootingRating(enemy, me, -1);
				              comparator = (myShootingRating * 67) / 100;
				              
				              Log("me (target unit), target shooting rating, charger shooting rating, comparator", me, myShootingRating, enemyShootingRating, comparator);
				              
				              if ((myShootingRating == 0) || (enemyShootingRating > comparator))
				              	{
				              		desiredMargin -= 12; // may need tweaking.
				              		Log("Cavalry Anti-evade boost applied");
				              	}
				              else
				              	{
				              		Log("Cavalry Anti-evade boost NOT applied");
				              	}
											
											// End v1.3.5 change
										}

									Log("charger, target unit, desiredMargin, combatMargin", me, enemy, desiredMargin, combatMargin);

									if (combatMargin >= desiredMargin) // Don't evade because we could win combat.
										{
											evade = 0;
										}
									else
										{
											// Consider not evading anyway if likely to be caught - however, the higher the risk of losing straight combat, the more likely to evade despite risk of being caught.
											// Note that to catch the evaders, the pursuers need to reach a square adjacent to the evaders AND have sufficient AP left to assault the evaders' square.
											safetyMargin =  evadeAP - pursuitAP;
											desiredMargin = 0;

											if (combatMargin > 0) // May need further tweaking
												{
													desiredMargin = 8; // Will get away unless evader goes down on VMD AND charger goes up on VMD (6% chance of being caught)
												}
											else
												{
													if (combatMargin > -20) // May need further tweaking
														{
															desiredMargin = 4; // Will get away unless evader goes down on VMD OR charger goes up on VMD (and other doesn't go other way) (37.5% chance of being caught)
														}
												}

				              Log("Unit, enemy, AP: safetyMargin, desiredMargin", me, enemy, safetyMargin, desiredMargin);
				//              DebugLogX("Unit, enemy, AP: safetyMargin, desiredMargin", me, enemy, safetyMargin, desiredMargin);
				              
											if (safetyMargin < desiredMargin)
												{
													evade = 0;
												}
										}
								} // v1.5.8 addition
						}
				}
		}
From a design point of view, we are happy that this leaves the question of whether a unit will evade or not sometimes somewhat unpredictable. In real life, such things would not be easily predictable for the charging unit or either side's C-in-C.
Richard Bodley Scott

Image
markleslie
Staff Sergeant - Kavallerie
Staff Sergeant - Kavallerie
Posts: 322
Joined: Thu Oct 22, 2020 6:55 am

Re: How does a cavalry unit AI decide in evading charges?

Post by markleslie »

Of course, for the layperson, that can all be distilled down to...

42!
Atherys
Senior Corporal - Ju 87G
Senior Corporal - Ju 87G
Posts: 85
Joined: Fri Feb 05, 2021 6:56 am

Re: How does a cavalry unit AI decide in evading charges?

Post by Atherys »

rbodleyscott wrote: Mon Apr 12, 2021 6:38 am It is pretty much impossible to distill the decision making code to the simple rule of thumb you would like. It isn't calculated from the % chances anyway, but from raw combat ratings.

Also the chance of evading is also modified by the chance of being caught if they evade.

Also, the unit also tests against other enemy units that might charge them later this turn if they don't evade, so the decision to evade may not be based on the unit that is currently charging them. (A gamey trick in FOG1 was to charge them enemy with a weak unit to trick them into standing, and then charge them with a strong one).

Also, troops who have a better shooting rating than the enemy are more likely to evade, because in the long run it may be better to keep shooting rather than get stuck in melee. (Even if they have a reasonable chance of winning this particular melee eventually - the longer it will take, the more chance of other enemy units intervening).

Also there is a small random element around the cut off point for evading or not.

Here is the actual code: (As you can see, it is not a couple of simple lines)

Code: Select all

	if (evade == 1)
		{
			evadeAP = GetBaseAttrib(me,"AP");
			pursuitAP = GetAttrib(enemy,"AP");

			// See whether unit decides to stand rather than evade      
      
      if ((IsUnitSquadType(me, "Light_Foot") != 1) || (IsLightTroops(enemy) == 1) || (IsRoughOrDifficult(GetUnitX(me),GetUnitY(me)) == 1) || (IsTileEdgeDefendibleObstacle(GetUnitX(me), GetUnitY(me), adjacent_X, adjacent_Y) > -1)) 
      	// v1.5.0 addition. Light foot always evade non-lights unless in rough or difficult or defending obstacle. 
      	// Note that currently they don't automatically evade if alternative chargers could charge them not across an obstacle.
      	{        	
		      if ((IsFlankRearAttack(enemy, me) == 0) || ((IsLightTroops(me) == 0) && (IsLightTroops(enemy) == 1))) // Always evade if charged in flank/rear, unless non-lights being charged by lights
		      // V 1.2.2 change. Note that this does not take into account the guaranteed net +50 POA for a flank charge by lights on non-lights, but as the non-lights are likely to win the melee even
		      // if they disrupt on impact, I don't think this matters.
						{
							// Move enemy to fighting position, calculate combat margin, then move it back to starting position.
							// Note: in the unlikely event that there is a unit there already it will get swapped out then swapped back again.
							current_X = GetUnitX(enemy);
							current_Y = GetUnitY(enemy);
							UnitDeployIfPositionDifferent(enemy, adjacent_X, adjacent_Y, 1);

							Log("Combat margin calc in pos: Chargee x,y, charger x,y", GetUnitX(me), GetUnitY(me), GetUnitX(enemy), GetUnitY(enemy));
							combatMargin = CalculateModifiedCloseCombatRating(me, enemy, enemy, 0, 0) - CalculateModifiedCloseCombatRating(enemy, me, enemy, 0, 0);
							Log("Main charger, combat margin", enemy, combatMargin);
							Log("Pursuit AP", pursuitAP);
							// Don't move the unit back to its start position until after testing other potential chargers, because its combat position may block them from charging

							// Also check if any other nasty enemy could charge them this turn or next (from current position) - if so, and it is nastier than the charger, substitute the combat margin pertaining to it.
							// This is intended to stop players pinning enemy light troops by charging them with something weak enough that they won't evade, and then immediately charging them with something nasty
							// when they are in close combat and can therefore no longer evade.
							// Note, however, does not currently take into account other pursuers that might contact it.
							enemySide = GetEnemySide(me);
							total = GetUnitCount(enemySide);
							for (i = 0; i < total; i++)
							{
								id = GetUnitID(enemySide, i);
								if ((id != -1) && (id != enemy))
									{
										if (GetDistanceBetween(id, me) <= GetBaseAttrib(id, "AP") / 4)
											{
												// Store current values of relevant attributes and set the attribs to start turn condition
												actualAP = GetAttrib(id, "AP");
												SetAttrib(id, "AP", GetBaseAttrib(id, "AP"));
												largeTurn = GetAttrib(id, "MadeLargeTurn");
												SetAttrib(id, "MadeLargeTurn", 0);
												freeTurn = GetAttrib(id, "MadeFreeTurn");
												SetAttrib(id, "MadeFreeTurn", 0);
												shots = GetAttrib(id, "Shots");
												SetAttrib(id, "Shots", GetBaseAttrib(id, "Shots"));

												// Determine whether unit could charge this turn or next turn from current position, and if so whether it is nastier than the original charger.
												if (CallUnitFunctionDirect(id, "CHECK_UNIT_ASSAULT", id, me) >= 0)
													{
														// Move enemy unit to fighting position, calculate combat margin, then move it back to starting position.
														// Note: in the unlikely event that there is a unit there already it will get swapped out then swapped back again.
														unit_X = GetUnitX(me);
														unit_Y = GetUnitY(me);
														potentialCharger_X = GetUnitX(id);
														potentialCharger_Y = GetUnitY(id);
														if (AreTilesAdjacent(potentialCharger_X, potentialCharger_Y, unit_X, unit_Y) == 1)
															{
																adjacent_X = potentialCharger_X;
																adjacent_Y = potentialCharger_Y;
															}
														else
															{
																route = GetRouteCost(id, unit_X, unit_Y, 0, 1);
																length = GetCheckRouteLength();
																adjacent_X = GetCheckRouteX(length-2);
																adjacent_Y = GetCheckRouteY(length-2);
															}

														UnitDeployIfPositionDifferent(id, adjacent_X, adjacent_Y, 1);

														Log("Combat margin calc in pos: Chargee x,y, charger x,y", GetUnitX(me), GetUnitY(me), GetUnitX(id), GetUnitY(id));
														newCombatMargin = CalculateModifiedCloseCombatRating(me, id, id, 0, 0) - CalculateModifiedCloseCombatRating(id, me, id, 0, 0);
														combatMargin = Min(newCombatMargin, combatMargin);
														Log("Unit, new combat margin, worst combat margin", id, newCombatMargin, combatMargin);
														UnitDeployIfPositionDifferent(id, potentialCharger_X, potentialCharger_Y, 1);
														
														// v1.5.8 addition - always evade if effective flank/rear charge from new unit is possible
                            if ((IsFlankRearAttack(id, me) == 1) && ((IsLightTroops(me) == 1) || (IsLightTroops(id) == 0)))
                            	{
                            		definitely_evade = 1;                            		
                            	}													
														// End v1.5.8 addition														
													}

												// Restore previous Attrib values
												SetAttrib(id, "AP", actualAP);
												SetAttrib(id, "MadeLargeTurn", largeTurn);
												SetAttrib(id, "MadeFreeTurn", freeTurn);
												SetAttrib(id, "Shots", shots);
											}
									}
							}

							// Move original charger back to its starting position
							UnitDeployIfPositionDifferent(enemy, current_X, current_Y, 1);

							// v1.5.8 addition
              if (definitely_evade == 0)
              	{
              // End v1.5.8 addition
									desiredMargin = 10 - Rand(0,10); // May need further tweaking
									
									Log("\nShall we evade?: target, charger", me, enemy);

									// Reduce desired margin if light foot being charged by light foot, or light horse by light horse
									if ((IsLightTroops(me) == 1) && (IsLightTroops(enemy) == 1))
										{
											if ((IsMounted(me) == 1) && (IsMounted(enemy) == 1))
												{
													desiredMargin -= 12; // May need tweaking
												}
											if ((IsFoot(me) == 1) && (IsFoot(enemy) == 1))
												{
													desiredMargin -= 12; // May need tweaking
												}
										}
									
									// Reduced desired margin if cavalry, camelry or light chariots	
									if ((IsUnitSquadType(me, "Cavalry") == 1) || (IsUnitSquadType(me, "Camelry") == 1) || (IsUnitSquadType(me, "Light_Chariots") == 1))
										{
				//							desiredMargin -= 12; // may need tweaking.

											Log("Before cavalry anti-evade adjustment: charger, target unit, desiredMargin, combatMargin", me, enemy, desiredMargin, combatMargin);
											
											// v1.3.5 change
											// Don't make anti-evade adjustment if unit has significantly higher shooting rating than charger.
											// (Takes into account current ammunition state, although not how many turns of full ammo left).
				              myShootingRating = CalculateModifiedShootingRating(me, enemy, -1);
				              enemyShootingRating = CalculateModifiedShootingRating(enemy, me, -1);
				              comparator = (myShootingRating * 67) / 100;
				              
				              Log("me (target unit), target shooting rating, charger shooting rating, comparator", me, myShootingRating, enemyShootingRating, comparator);
				              
				              if ((myShootingRating == 0) || (enemyShootingRating > comparator))
				              	{
				              		desiredMargin -= 12; // may need tweaking.
				              		Log("Cavalry Anti-evade boost applied");
				              	}
				              else
				              	{
				              		Log("Cavalry Anti-evade boost NOT applied");
				              	}
											
											// End v1.3.5 change
										}

									Log("charger, target unit, desiredMargin, combatMargin", me, enemy, desiredMargin, combatMargin);

									if (combatMargin >= desiredMargin) // Don't evade because we could win combat.
										{
											evade = 0;
										}
									else
										{
											// Consider not evading anyway if likely to be caught - however, the higher the risk of losing straight combat, the more likely to evade despite risk of being caught.
											// Note that to catch the evaders, the pursuers need to reach a square adjacent to the evaders AND have sufficient AP left to assault the evaders' square.
											safetyMargin =  evadeAP - pursuitAP;
											desiredMargin = 0;

											if (combatMargin > 0) // May need further tweaking
												{
													desiredMargin = 8; // Will get away unless evader goes down on VMD AND charger goes up on VMD (6% chance of being caught)
												}
											else
												{
													if (combatMargin > -20) // May need further tweaking
														{
															desiredMargin = 4; // Will get away unless evader goes down on VMD OR charger goes up on VMD (and other doesn't go other way) (37.5% chance of being caught)
														}
												}

				              Log("Unit, enemy, AP: safetyMargin, desiredMargin", me, enemy, safetyMargin, desiredMargin);
				//              DebugLogX("Unit, enemy, AP: safetyMargin, desiredMargin", me, enemy, safetyMargin, desiredMargin);
				              
											if (safetyMargin < desiredMargin)
												{
													evade = 0;
												}
										}
								} // v1.5.8 addition
						}
				}
		}
From a design point of view, we are happy that this leaves the question of whether a unit will evade or not sometimes somewhat unpredictable. In real life, such things would not be easily predictable for the charging unit or either side's C-in-C.
I knew it! I knew it was never that simple... lol

So, as a rough summary from the code and from your answer:
1. It's not a random chance check per se, but a raw combat margin check and it will result in an evasion "desire" score.
2. My cavalry unit may check with the most dangerous enemy units in vicinity instead of the original enemy charger.
3. The more the enemy units in charge range, the more "desirable" it will be for my cavalry to evade.
4. If my cavalry has ranged attack and is more potent than the enemies, it will affect their "desire" to evade.
5. If my cavalry is in danger of being caught, then evading will be less "desirable" , but will still evade if their combat margin in straight combat is too bad.
6. There's still a small random evade chance in form of starting "desire" from 10 to 0 points.

Thank you so much for your detailed answer, RBS! In regular games, I don't really want to dig this kind of stuff and treat it as "natural and slightly random" instead, but the upcoming 4th tournament round will most likely be a major dog fight between cavalries and I'd like to have sufficient knowledge for "control", even if it's just a little bit, instead of leaving it all (and me score) to random.
rbodleyscott
Field of Glory 2
Field of Glory 2
Posts: 28014
Joined: Sun Dec 04, 2005 6:25 pm

Re: How does a cavalry unit AI decide in evading charges?

Post by rbodleyscott »

Atherys wrote: Mon Apr 12, 2021 11:46 am So, as a rough summary from the code and from your answer:
1. It's not a random chance check per se, but a raw combat margin check and it will result in an evasion "desire" score.
2. My cavalry unit may check with the most dangerous enemy units in vicinity instead of the original enemy charger.
3. The more the enemy units in charge range, the more "desirable" it will be for my cavalry to evade.
4. If my cavalry has ranged attack and is more potent than the enemies, it will affect their "desire" to evade.
5. If my cavalry is in danger of being caught, then evading will be less "desirable" , but will still evade if their combat margin in straight combat is too bad.
6. There's still a small random evade chance in form of starting "desire" from 10 to 0 points.
Pretty much, although it is a random adjustment to the desired combat margin of up to 10 points. Whether this has any effect at all will depend on the combat margin. It is there specifically to make close call decisions less predictable, but should not have any effect on "no brainer" decisions.

So mostly, evades are not random, except in edge cases. It is just that the algorithm is too complex to allow the player to always predict what will happen. Experience, however, will allow the skilled player a fairly good idea of what it likely to happen in most circumstances.
Richard Bodley Scott

Image
kronenblatt
General - Elite King Tiger
General - Elite King Tiger
Posts: 4333
Joined: Mon Jun 03, 2019 4:17 pm
Location: Stockholm, SWEDEN

Re: How does a cavalry unit AI decide in evading charges?

Post by kronenblatt »

rbodleyscott wrote: Mon Apr 12, 2021 6:38 am It is pretty much impossible to distill the decision making code to the simple rule of thumb you would like. It isn't calculated from the % chances anyway, but from raw combat ratings.

Also the chance of evading is also modified by the chance of being caught if they evade.

Also, the unit also tests against other enemy units that might charge them later this turn if they don't evade, so the decision to evade may not be based on the unit that is currently charging them. (A gamey trick in FOG1 was to charge them enemy with a weak unit to trick them into standing, and then charge them with a strong one).

Also, troops who have a better shooting rating than the enemy are more likely to evade, because in the long run it may be better to keep shooting rather than get stuck in melee. (Even if they have a reasonable chance of winning this particular melee eventually - the longer it will take, the more chance of other enemy units intervening).

Also there is a small random element around the cut off point for evading or not.

Here is the actual code: (As you can see, it is not a couple of simple lines)

Code: Select all

	if (evade == 1)
		{
			evadeAP = GetBaseAttrib(me,"AP");
			pursuitAP = GetAttrib(enemy,"AP");

			// See whether unit decides to stand rather than evade      
      
      if ((IsUnitSquadType(me, "Light_Foot") != 1) || (IsLightTroops(enemy) == 1) || (IsRoughOrDifficult(GetUnitX(me),GetUnitY(me)) == 1) || (IsTileEdgeDefendibleObstacle(GetUnitX(me), GetUnitY(me), adjacent_X, adjacent_Y) > -1)) 
      	// v1.5.0 addition. Light foot always evade non-lights unless in rough or difficult or defending obstacle. 
      	// Note that currently they don't automatically evade if alternative chargers could charge them not across an obstacle.
      	{        	
		      if ((IsFlankRearAttack(enemy, me) == 0) || ((IsLightTroops(me) == 0) && (IsLightTroops(enemy) == 1))) // Always evade if charged in flank/rear, unless non-lights being charged by lights
		      // V 1.2.2 change. Note that this does not take into account the guaranteed net +50 POA for a flank charge by lights on non-lights, but as the non-lights are likely to win the melee even
		      // if they disrupt on impact, I don't think this matters.
						{
							// Move enemy to fighting position, calculate combat margin, then move it back to starting position.
							// Note: in the unlikely event that there is a unit there already it will get swapped out then swapped back again.
							current_X = GetUnitX(enemy);
							current_Y = GetUnitY(enemy);
							UnitDeployIfPositionDifferent(enemy, adjacent_X, adjacent_Y, 1);

							Log("Combat margin calc in pos: Chargee x,y, charger x,y", GetUnitX(me), GetUnitY(me), GetUnitX(enemy), GetUnitY(enemy));
							combatMargin = CalculateModifiedCloseCombatRating(me, enemy, enemy, 0, 0) - CalculateModifiedCloseCombatRating(enemy, me, enemy, 0, 0);
							Log("Main charger, combat margin", enemy, combatMargin);
							Log("Pursuit AP", pursuitAP);
							// Don't move the unit back to its start position until after testing other potential chargers, because its combat position may block them from charging

							// Also check if any other nasty enemy could charge them this turn or next (from current position) - if so, and it is nastier than the charger, substitute the combat margin pertaining to it.
							// This is intended to stop players pinning enemy light troops by charging them with something weak enough that they won't evade, and then immediately charging them with something nasty
							// when they are in close combat and can therefore no longer evade.
							// Note, however, does not currently take into account other pursuers that might contact it.
							enemySide = GetEnemySide(me);
							total = GetUnitCount(enemySide);
							for (i = 0; i < total; i++)
							{
								id = GetUnitID(enemySide, i);
								if ((id != -1) && (id != enemy))
									{
										if (GetDistanceBetween(id, me) <= GetBaseAttrib(id, "AP") / 4)
											{
												// Store current values of relevant attributes and set the attribs to start turn condition
												actualAP = GetAttrib(id, "AP");
												SetAttrib(id, "AP", GetBaseAttrib(id, "AP"));
												largeTurn = GetAttrib(id, "MadeLargeTurn");
												SetAttrib(id, "MadeLargeTurn", 0);
												freeTurn = GetAttrib(id, "MadeFreeTurn");
												SetAttrib(id, "MadeFreeTurn", 0);
												shots = GetAttrib(id, "Shots");
												SetAttrib(id, "Shots", GetBaseAttrib(id, "Shots"));

												// Determine whether unit could charge this turn or next turn from current position, and if so whether it is nastier than the original charger.
												if (CallUnitFunctionDirect(id, "CHECK_UNIT_ASSAULT", id, me) >= 0)
													{
														// Move enemy unit to fighting position, calculate combat margin, then move it back to starting position.
														// Note: in the unlikely event that there is a unit there already it will get swapped out then swapped back again.
														unit_X = GetUnitX(me);
														unit_Y = GetUnitY(me);
														potentialCharger_X = GetUnitX(id);
														potentialCharger_Y = GetUnitY(id);
														if (AreTilesAdjacent(potentialCharger_X, potentialCharger_Y, unit_X, unit_Y) == 1)
															{
																adjacent_X = potentialCharger_X;
																adjacent_Y = potentialCharger_Y;
															}
														else
															{
																route = GetRouteCost(id, unit_X, unit_Y, 0, 1);
																length = GetCheckRouteLength();
																adjacent_X = GetCheckRouteX(length-2);
																adjacent_Y = GetCheckRouteY(length-2);
															}

														UnitDeployIfPositionDifferent(id, adjacent_X, adjacent_Y, 1);

														Log("Combat margin calc in pos: Chargee x,y, charger x,y", GetUnitX(me), GetUnitY(me), GetUnitX(id), GetUnitY(id));
														newCombatMargin = CalculateModifiedCloseCombatRating(me, id, id, 0, 0) - CalculateModifiedCloseCombatRating(id, me, id, 0, 0);
														combatMargin = Min(newCombatMargin, combatMargin);
														Log("Unit, new combat margin, worst combat margin", id, newCombatMargin, combatMargin);
														UnitDeployIfPositionDifferent(id, potentialCharger_X, potentialCharger_Y, 1);
														
														// v1.5.8 addition - always evade if effective flank/rear charge from new unit is possible
                            if ((IsFlankRearAttack(id, me) == 1) && ((IsLightTroops(me) == 1) || (IsLightTroops(id) == 0)))
                            	{
                            		definitely_evade = 1;                            		
                            	}													
														// End v1.5.8 addition														
													}

												// Restore previous Attrib values
												SetAttrib(id, "AP", actualAP);
												SetAttrib(id, "MadeLargeTurn", largeTurn);
												SetAttrib(id, "MadeFreeTurn", freeTurn);
												SetAttrib(id, "Shots", shots);
											}
									}
							}

							// Move original charger back to its starting position
							UnitDeployIfPositionDifferent(enemy, current_X, current_Y, 1);

							// v1.5.8 addition
              if (definitely_evade == 0)
              	{
              // End v1.5.8 addition
									desiredMargin = 10 - Rand(0,10); // May need further tweaking
									
									Log("\nShall we evade?: target, charger", me, enemy);

									// Reduce desired margin if light foot being charged by light foot, or light horse by light horse
									if ((IsLightTroops(me) == 1) && (IsLightTroops(enemy) == 1))
										{
											if ((IsMounted(me) == 1) && (IsMounted(enemy) == 1))
												{
													desiredMargin -= 12; // May need tweaking
												}
											if ((IsFoot(me) == 1) && (IsFoot(enemy) == 1))
												{
													desiredMargin -= 12; // May need tweaking
												}
										}
									
									// Reduced desired margin if cavalry, camelry or light chariots	
									if ((IsUnitSquadType(me, "Cavalry") == 1) || (IsUnitSquadType(me, "Camelry") == 1) || (IsUnitSquadType(me, "Light_Chariots") == 1))
										{
				//							desiredMargin -= 12; // may need tweaking.

											Log("Before cavalry anti-evade adjustment: charger, target unit, desiredMargin, combatMargin", me, enemy, desiredMargin, combatMargin);
											
											// v1.3.5 change
											// Don't make anti-evade adjustment if unit has significantly higher shooting rating than charger.
											// (Takes into account current ammunition state, although not how many turns of full ammo left).
				              myShootingRating = CalculateModifiedShootingRating(me, enemy, -1);
				              enemyShootingRating = CalculateModifiedShootingRating(enemy, me, -1);
				              comparator = (myShootingRating * 67) / 100;
				              
				              Log("me (target unit), target shooting rating, charger shooting rating, comparator", me, myShootingRating, enemyShootingRating, comparator);
				              
				              if ((myShootingRating == 0) || (enemyShootingRating > comparator))
				              	{
				              		desiredMargin -= 12; // may need tweaking.
				              		Log("Cavalry Anti-evade boost applied");
				              	}
				              else
				              	{
				              		Log("Cavalry Anti-evade boost NOT applied");
				              	}
											
											// End v1.3.5 change
										}

									Log("charger, target unit, desiredMargin, combatMargin", me, enemy, desiredMargin, combatMargin);

									if (combatMargin >= desiredMargin) // Don't evade because we could win combat.
										{
											evade = 0;
										}
									else
										{
											// Consider not evading anyway if likely to be caught - however, the higher the risk of losing straight combat, the more likely to evade despite risk of being caught.
											// Note that to catch the evaders, the pursuers need to reach a square adjacent to the evaders AND have sufficient AP left to assault the evaders' square.
											safetyMargin =  evadeAP - pursuitAP;
											desiredMargin = 0;

											if (combatMargin > 0) // May need further tweaking
												{
													desiredMargin = 8; // Will get away unless evader goes down on VMD AND charger goes up on VMD (6% chance of being caught)
												}
											else
												{
													if (combatMargin > -20) // May need further tweaking
														{
															desiredMargin = 4; // Will get away unless evader goes down on VMD OR charger goes up on VMD (and other doesn't go other way) (37.5% chance of being caught)
														}
												}

				              Log("Unit, enemy, AP: safetyMargin, desiredMargin", me, enemy, safetyMargin, desiredMargin);
				//              DebugLogX("Unit, enemy, AP: safetyMargin, desiredMargin", me, enemy, safetyMargin, desiredMargin);
				              
											if (safetyMargin < desiredMargin)
												{
													evade = 0;
												}
										}
								} // v1.5.8 addition
						}
				}
		}
From a design point of view, we are happy that this leaves the question of whether a unit will evade or not sometimes somewhat unpredictable. In real life, such things would not be easily predictable for the charging unit or either side's C-in-C.
rbodleyscott wrote: Mon Apr 12, 2021 2:33 pm
Atherys wrote: Mon Apr 12, 2021 11:46 am So, as a rough summary from the code and from your answer:
1. It's not a random chance check per se, but a raw combat margin check and it will result in an evasion "desire" score.
2. My cavalry unit may check with the most dangerous enemy units in vicinity instead of the original enemy charger.
3. The more the enemy units in charge range, the more "desirable" it will be for my cavalry to evade.
4. If my cavalry has ranged attack and is more potent than the enemies, it will affect their "desire" to evade.
5. If my cavalry is in danger of being caught, then evading will be less "desirable" , but will still evade if their combat margin in straight combat is too bad.
6. There's still a small random evade chance in form of starting "desire" from 10 to 0 points.
Pretty much, although it is a random adjustment to the desired combat margin of up to 10 points. Whether this has any effect at all will depend on the combat margin. It is there specifically to make close call decisions less predictable, but should not have any effect on "no brainer" decisions.

So mostly, evades are not random, except in edge cases. It is just that the algorithm is too complex to allow the player to always predict what will happen. Experience, however, will allow the skilled player a fairly good idea of what it likely to happen in most circumstances.
Does this apply to Ancients as well?
kronenblatt's campaign and tournament thread hub:

https://www.slitherine.com/forum/viewtopic.php?t=108643
rbodleyscott
Field of Glory 2
Field of Glory 2
Posts: 28014
Joined: Sun Dec 04, 2005 6:25 pm

Re: How does a cavalry unit AI decide in evading charges?

Post by rbodleyscott »

kronenblatt wrote: Sun Apr 25, 2021 7:08 am Does this apply to Ancients as well?
Yes
Richard Bodley Scott

Image
Post Reply

Return to “Field of Glory II: Medieval”