unit KM_AIArmyEvaluation; {$I KaM_Remake.inc} interface uses KM_Defaults, KM_CommonClasses, KM_Units, KM_UnitGroup; type TKMGroupEval = record Count: Integer; HitPoints, Attack, AttackHorse, Defence, DefenceProjectiles: Single; end; TKMFoodState = record Full, Middle, Low: Integer; end; TKMArmyEval = record FoodState: TKMFoodState; Groups: array[GROUP_TYPE_MIN..GROUP_TYPE_MAX] of TKMGroupEval; end; TKMGameEval = array[0 .. MAX_HANDS - 1] of TKMArmyEval; TKMGroupStrengthArray = array[GROUP_TYPE_MIN..GROUP_TYPE_MAX] of Single; //This class evaluate self army relatively enemy armies TKMArmyEvaluation = class private fEvals: TKMGameEval; //Results of evaluation function CompareStrength(A, E: TKMArmyEval): Single; function GetAllianceStrength(const aAlliance: TKMHandIDArray): TKMArmyEval; procedure EvaluatePower(aPlayer: TKMHandID; aConsiderHitChance: Boolean = False); public constructor Create(); destructor Destroy; override; procedure Save(SaveStream: TKMemoryStream); procedure Load(LoadStream: TKMemoryStream); function UnitEvaluation(aUT: TKMUnitType; aConsiderHitChance: Boolean = False): TKMGroupEval; function GroupEvaluation(aGroup: TKMUnitGroup; aConsiderHitChance: Boolean): TKMGroupEval; function PlayerEvaluation(aPlayer: TKMHandID): TKMArmyEval; function AllianceEvaluation(aPlayer: TKMHandID; aAlliance: TKMAllianceType): TKMArmyEval; function CompareAllianceStrength(const aAlly, aEnemy: TKMHandIDArray): Single; function CheckFoodProblems(const aAlly: TKMHandIDArray): Boolean; procedure AfterMissionInit(); procedure UpdateState(aTick: Cardinal); end; const HIT_CHANCE_MODIFIER = 0.5; implementation uses Classes, Math, KM_Hand, KM_HandsCollection, KM_HandStats, KM_UnitsCollection, KM_Resource, KM_ResUnits, KM_ResTypes; { TKMArmyEvaluation } constructor TKMArmyEvaluation.Create(); begin inherited Create; FillChar(fEvals, SizeOf(fEvals), #0); end; destructor TKMArmyEvaluation.Destroy; begin inherited; end; procedure TKMArmyEvaluation.Save(SaveStream: TKMemoryStream); begin SaveStream.PlaceMarker('ArmyEvaluation'); SaveStream.Write(fEvals, SizeOf(fEvals)); end; procedure TKMArmyEvaluation.Load(LoadStream: TKMemoryStream); begin LoadStream.CheckMarker('ArmyEvaluation'); LoadStream.Read(fEvals, SizeOf(fEvals)); end; function TKMArmyEvaluation.UnitEvaluation(aUT: TKMUnitType; aConsiderHitChance: Boolean = False): TKMGroupEval; var unitSpec: TKMUnitSpec; begin // Fill array with reference values unitSpec := gRes.Units[aUT]; Result.Count := 1; Result.HitPoints := unitSpec.HitPoints; Result.Attack := unitSpec.Attack; Result.AttackHorse := unitSpec.AttackHorse; Result.Defence := unitSpec.Defence; Result.DefenceProjectiles := unitSpec.GetDefenceVsProjectiles(False); if aConsiderHitChance and (UNIT_TO_GROUP_TYPE[aUT] = gtRanged) then Result.Attack := Result.Attack * HIT_CHANCE_MODIFIER; end; function TKMArmyEvaluation.GroupEvaluation(aGroup: TKMUnitGroup; aConsiderHitChance: Boolean): TKMGroupEval; var K: Integer; GE: TKMGroupEval; begin FillChar(Result, SizeOf(Result), #0); Result.Count := aGroup.Count; for K := 0 to aGroup.Count-1 do begin GE := UnitEvaluation(TKMUnit(aGroup.Members[K]).UnitType, aConsiderHitChance); with Result do begin HitPoints := HitPoints + GE.HitPoints; Attack := Attack + GE.Attack; AttackHorse := AttackHorse + GE.AttackHorse; Defence := Defence + GE.Defence; DefenceProjectiles := DefenceProjectiles + GE.DefenceProjectiles; end; end; end; function TKMArmyEvaluation.PlayerEvaluation(aPlayer: TKMHandID): TKMArmyEval; begin Move(fEvals[aPlayer], Result, SizeOf(fEvals[aPlayer])); end; function TKMArmyEvaluation.AllianceEvaluation(aPlayer: TKMHandID; aAlliance: TKMAllianceType): TKMArmyEval; var Cnt: Integer; PL: TKMHandID; Alliance: TKMHandIDArray; begin Cnt := 0; SetLength(Alliance, gHands.Count); for PL := 0 to gHands.Count - 1 do begin Alliance[Cnt] := PL; Inc(Cnt, Byte( gHands[PL].Enabled AND (gHands[aPlayer].Alliances[PL] = aAlliance) )); end; SetLength(Alliance,Cnt); Result := GetAllianceStrength(Alliance); end; function TKMArmyEvaluation.CompareStrength(A, E: TKMArmyEval): Single; // Sum power of all types of groups procedure SumPower(var aEval: TKMGroupEval; var Army: TKMArmyEval); var GT: TKMGroupType; begin FillChar(aEval, SizeOf(aEval), #0); for GT := GROUP_TYPE_MIN to GROUP_TYPE_MAX do begin aEval.Count := aEval.Count + Army.Groups[GT].Count ; aEval.HitPoints := aEval.HitPoints + Army.Groups[GT].HitPoints ; aEval.Attack := aEval.Attack + Army.Groups[GT].Attack ; aEval.AttackHorse := aEval.AttackHorse + Army.Groups[GT].AttackHorse ; aEval.Defence := aEval.Defence + Army.Groups[GT].Defence ; aEval.DefenceProjectiles := aEval.DefenceProjectiles + Army.Groups[GT].DefenceProjectiles; end; end; var AEval, EEval: TKMGroupEval; begin // Sum power of groups in alliance SumPower(AEval,A); SumPower(EEval,E); // Get weighted power with consideration of hostile groups AEval.AttackHorse := AEval.AttackHorse * E.Groups[gtMounted].Count / Max(1, EEval.Count); EEval.AttackHorse := EEval.AttackHorse * A.Groups[gtMounted].Count / Max(1, AEval.Count); AEval.DefenceProjectiles := AEval.DefenceProjectiles * E.Groups[gtRanged].Count / Max(1, EEval.Count); EEval.DefenceProjectiles := EEval.DefenceProjectiles * A.Groups[gtRanged].Count / Max(1, AEval.Count); // Get balance of power Result := ( + 1 - Min(2,(EEval.Attack + EEval.AttackHorse ) / Max(1,(AEval.Attack + AEval.AttackHorse ))) + 1 - Min(2,(EEval.HitPoints ) / Max(1,(AEval.HitPoints ))) + 1 - Min(2,(EEval.Defence + EEval.DefenceProjectiles) / Max(1,(AEval.Defence + AEval.DefenceProjectiles))) ) / 3; end; function TKMArmyEvaluation.GetAllianceStrength(const aAlliance: TKMHandIDArray): TKMArmyEval; var PL: Integer; GT: TKMGroupType; begin FillChar(Result, SizeOf(Result), #0); for PL in aAlliance do begin for GT := GROUP_TYPE_MIN to GROUP_TYPE_MAX do with Result.Groups[GT] do begin Count := Count + fEvals[PL].Groups[GT].Count; HitPoints := HitPoints + fEvals[PL].Groups[GT].HitPoints; Attack := Attack + fEvals[PL].Groups[GT].Attack; AttackHorse := AttackHorse + fEvals[PL].Groups[GT].AttackHorse; Defence := Defence + fEvals[PL].Groups[GT].Defence; DefenceProjectiles := DefenceProjectiles + fEvals[PL].Groups[GT].DefenceProjectiles; end; with Result.FoodState do begin Inc(Full,fEvals[PL].FoodState.Full); Inc(Middle,fEvals[PL].FoodState.Middle); Inc(Low,fEvals[PL].FoodState.Low); end; end; end; // Approximate way how to compute strength of 2 alliances function TKMArmyEvaluation.CompareAllianceStrength(const aAlly, aEnemy: TKMHandIDArray): Single; var AllyEval, EnemyEval: TKMArmyEval; begin AllyEval := GetAllianceStrength(aAlly); EnemyEval := GetAllianceStrength(aEnemy); Result := CompareStrength(AllyEval, EnemyEval); // => number in <-1,1> ... positive = we have advantage and vice versa end; function TKMArmyEvaluation.CheckFoodProblems(const aAlly: TKMHandIDArray): Boolean; const FOOD_THRESHOLD = 0.75; var Full, Middle, Low, Food: Integer; PL: TKMHandID; begin Result := False; Full := 0; Middle := 0; Low := 0; for PL in aAlly do begin Food := 0; Inc(Food ,gHands[PL].Stats.GetWareBalance(wtBread)); Inc(Food ,gHands[PL].Stats.GetWareBalance(wtWine)); Inc(Food ,gHands[PL].Stats.GetWareBalance(wtSausage)); Inc(Food ,gHands[PL].Stats.GetWareBalance(wtFish)); Inc(Full ,fEvals[PL].FoodState.Full); Inc(Middle,fEvals[PL].FoodState.Middle); Inc(Low ,max(0, fEvals[PL].FoodState.Low - Food)); end; if ((Full + Middle + Low) > 0) then Result := ((Full + Middle) / Max(1, (Full + Middle + Low))) < FOOD_THRESHOLD; end; // Actualize power of specific player // // Equation of combat: // // Close combat // Attack (+ AttackHorse) (+ DirModifier) -> DirModifier is not considered in ArmyEvaluation // Probability = --------------------------------------- // Defence // // Ranged units // HitProbability = 1 - Distance; Distance in <0,1> -> Hit probability is considered as a decreasing of attack by HIT_CHANCE_MODIFIER // Attack // Probability = -------------------- // DefenceProjectiles // // Probability > random number => decrease hitpoint; 0 hitpoints = unit is dead procedure TKMArmyEvaluation.EvaluatePower(aPlayer: TKMHandID; aConsiderHitChance: Boolean = False); procedure EvaluateFoodLevel(); const FULL_LIMIT = Round(UNIT_MAX_CONDITION * 0.75); LOW_LIMIT = Round(UNIT_MAX_CONDITION * 0.3); var K, Condition, LowCnt, FullCnt: Integer; begin LowCnt := 0; FullCnt := 0; for K := 0 to gHands[aPlayer].Units.Count - 1 do begin Condition := gHands[aPlayer].Units[K].Condition; Inc(LowCnt, Byte(Condition < LOW_LIMIT)); Inc(FullCnt,Byte(Condition > FULL_LIMIT)); end; fEvals[aPlayer].FoodState.Low := LowCnt; fEvals[aPlayer].FoodState.Middle := gHands[aPlayer].Units.Count - LowCnt - FullCnt; fEvals[aPlayer].FoodState.Full := FullCnt; end; var Stats: TKMHandStats; Qty: Integer; US: TKMUnitSpec; UT: TKMUnitType; GT: TKMGroupType; begin Stats := gHands[aPlayer].Stats; // Clear array FillChar(fEvals[aPlayer], SizeOf(fEvals[aPlayer]), #0); // Fill array with reference values for UT := WARRIOR_MIN to WARRIOR_MAX do begin Qty := Stats.GetUnitQty(UT); US := gRes.Units[UT]; GT := UNIT_TO_GROUP_TYPE[UT]; with fEvals[aPlayer].Groups[GT] do begin Count := Count + Qty; HitPoints := HitPoints + Qty * US.HitPoints; Attack := Attack + Qty * US.Attack; AttackHorse := AttackHorse + Qty * US.AttackHorse; Defence := Defence + Qty * US.Defence; DefenceProjectiles := DefenceProjectiles + Qty * US.GetDefenceVsProjectiles(False); // True = IsBolt -> calculation without bolts end; end; if aConsiderHitChance then with fEvals[aPlayer].Groups[gtRanged] do Attack := Attack * HIT_CHANCE_MODIFIER; // Check if army is hungry EvaluateFoodLevel(); end; procedure TKMArmyEvaluation.AfterMissionInit(); var PL: TKMHandID; begin for PL := 0 to gHands.Count - 1 do if gHands[PL].Enabled then EvaluatePower(PL, True); end; procedure TKMArmyEvaluation.UpdateState(aTick: Cardinal); const PERF_SUM = MAX_HANDS * 10; var PL: Integer; begin PL := aTick mod PERF_SUM; if (PL < gHands.Count) AND gHands[PL].Enabled then EvaluatePower(PL, True); end; end.