public static BattleResult SimulateBattle(BattleConfig battleConfig) { Log.Debug("Starting Battle Simulation"); //Stopwatch stopwatch = new Stopwatch(); //stopwatch.Restart(); // Based on: Tribal Wars 2 - Tutorial: Basic Battle System - https://www.youtube.com/watch?v=SG_qI1-go88 // Based on: Battle Simulator - http://www.ds-pro.de/2/simulator.php BattleResult result = new BattleResult(battleConfig); BattleMeta battleMeta = battleConfig.BattleMeta; WeaponSet paladinAtkWeapon = battleConfig.GetAtkWeapon(); WeaponSet paladinDefWeapon = battleConfig.GetDefWeapon(); decimal atkModifier = GetAtkBattleModifier(battleMeta); result.AtkBattleModifier = (int)(atkModifier * 100m); decimal defModifier = GetDefBattleModifier(battleMeta); result.DefModifierBeforeBattle = (int)(defModifier * 100m); // Stop here if there are no units given if (!battleConfig.HasUnits) { return(result); } // Re-calculate the defense modifier when the wall has been damaged int wallLevelAfterPreRound = PreRound(ref result, paladinAtkWeapon); battleMeta.WallLevel = wallLevelAfterPreRound; defModifier = GetDefBattleModifier(battleMeta); result.DefModifierDuringBattle = (int)(defModifier * 100m); List <BattleResult> BattleHistory = new List <BattleResult> { result.Copy() }; BattleResult currentRound = BattleHistory.Last(); // Set the atkUnits - minus the lost siege units and set that as the attacking group currentRound.AtkUnits -= result.AtkUnitsLost; currentRound.AtkUnitsLost.Clear(); // Simulate for 3 rounds (infantry, cavalry and archers) bool battleDetermined = false; int wallDefense = result.WallDefenseAfter; while (!battleDetermined) { // Safety Break if (BattleHistory.Count > 5) { break; } currentRound = BattleHistory.Last(); UnitSet atkUnits = currentRound.AtkUnits; UnitSet atkUnitsLost = currentRound.AtkUnitsLost; UnitSet defUnits = currentRound.DefUnits; int atkInfantryProvisions = atkUnits.GetTotalInfantryProvisions(); int atkCavalryProvisions = atkUnits.GetTotalCavalryProvisions(); int atkArchersProvisions = atkUnits.GetTotalArcherProvisions(); int atkSpecialProvisions = atkUnits.GetTotalSpecialProvisions(); int totalAtkProvisions = atkInfantryProvisions + atkCavalryProvisions + atkArchersProvisions + atkSpecialProvisions; int totalDefProvisions = defUnits.GetTotalProvisions(); if (totalAtkProvisions == 0) { break; } // Get the number of units for each group, this is used to determine which group the special group joins int atkInfantryUnits = atkUnits.GetTotalInfantryUnits(); int atkCavalryUnits = atkUnits.GetTotalCavalryUnits(); int atkArchersUnits = atkUnits.GetTotalArcherUnits(); int atkSpecialUnits = atkUnits.GetTotalSpecialUnits(); int totalAtkUnits = atkInfantryUnits + atkCavalryUnits + atkArchersUnits + atkSpecialUnits; // Determine if the defense is superior, if so, then double the berserker attack strength. bool defSuperior = (totalAtkProvisions * 2 <= totalDefProvisions); int atkInfantry = atkUnits.GetTotalInfantryAttack(paladinAtkWeapon, defSuperior); int atkCavalry = atkUnits.GetTotalCavalryAttack(paladinAtkWeapon); int atkArchers = atkUnits.GetTotalArcherAttack(paladinAtkWeapon); int atkSpecial = atkUnits.GetTotalSpecialAtk(); int totalAtk = atkInfantry + atkCavalry + atkArchers + atkSpecial; int strongestGroupIndex; // Determine which group the special units join during battle, the group with the highest number of units gets them. if (atkInfantryUnits > atkCavalryUnits && atkInfantryUnits > atkArchersUnits) { //AtkInfantry is the strongest group atkInfantry += atkSpecial; atkInfantryProvisions += atkSpecialProvisions; strongestGroupIndex = 1; } else if (atkCavalryUnits > atkInfantryUnits && atkCavalryUnits > atkArchersUnits) { //atkCavalry is the strongest group atkCavalry += atkSpecial; atkCavalryProvisions += atkSpecialProvisions; strongestGroupIndex = 2; } else { //AtkArchers is the strongest group atkArchers += atkSpecial; atkArchersProvisions += atkSpecialProvisions; strongestGroupIndex = 3; } // Add Atk modifier atkInfantry = AddAtkModifier(atkInfantry, atkModifier); atkCavalry = AddAtkModifier(atkCavalry, atkModifier); atkArchers = AddAtkModifier(atkArchers, atkModifier); decimal atkInfantryRatio = GetUnitProvisionRatio(atkInfantryProvisions, totalAtkProvisions); decimal atkCavalryRatio = GetUnitProvisionRatio(atkCavalryProvisions, totalAtkProvisions); decimal atkArchersRatio = GetUnitProvisionRatio(atkArchersProvisions, totalAtkProvisions); decimal totalRatio = atkInfantryRatio + atkCavalryRatio + atkArchersRatio; // These units sets contains the defensive units proportionate to the atkInfantry ratio UnitSet infantryGroupDefUnitSet = defUnits.GetUnitsByRatio(atkInfantryRatio); UnitSet cavalryGroupDefUnitSet = defUnits.GetUnitsByRatio(atkCavalryRatio); UnitSet archerGroupDefUnitSet = defUnits.GetUnitsByRatio(atkArchersRatio); //Check if the defUnits were divided correctly, if not, then use the difference value later to counteract the rounding error UnitSet defUnitSetSum = infantryGroupDefUnitSet + cavalryGroupDefUnitSet + archerGroupDefUnitSet; UnitSet difference = defUnits - defUnitSetSum; int totalDefFromInfantry = infantryGroupDefUnitSet.GetTotalDefFromInfantry(paladinDefWeapon); int totalDefFromCavalry = cavalryGroupDefUnitSet.GetTotalDefFromCavalry(paladinDefWeapon); int totalDefFromArchers = archerGroupDefUnitSet.GetTotalDefFromArchers(paladinDefWeapon); // Add defense modifier totalDefFromInfantry = AddDefModifier(totalDefFromInfantry, defModifier) + wallDefense; totalDefFromCavalry = AddDefModifier(totalDefFromCavalry, defModifier) + wallDefense; totalDefFromArchers = AddDefModifier(totalDefFromArchers, defModifier) + wallDefense; int totalDef = totalDefFromInfantry + totalDefFromCavalry + totalDefFromArchers; // Used to keep track of how many units are lost this round UnitSet infantryGroupDefUnitSetLost = new UnitSet(), cavalryGroupDefUnitSetLost = new UnitSet(), archerGroupDefUnitSetLost = new UnitSet(); // Determine the result of each mini-round by comparing the attack vs defense strength. // If both sides have 0 forces then ignore the result and leave the result undefined (null) bool? atkWonRound1 = null, atkWonRound2 = null, atkWonRound3 = null; List <bool> miniBattleResult = new List <bool>(); if (atkInfantry > 0 && totalDefFromInfantry > 0) { atkWonRound1 = atkInfantry >= totalDefFromInfantry; miniBattleResult.Add((bool)atkWonRound1); } if (atkCavalry > 0 && totalDefFromCavalry > 0) { atkWonRound2 = atkCavalry >= totalDefFromCavalry; miniBattleResult.Add((bool)atkWonRound2); } if (atkArchers > 0 && totalDefFromArchers > 0) { atkWonRound3 = atkArchers >= totalDefFromArchers; miniBattleResult.Add((bool)atkWonRound3); } // For every round that Defense won, kill the attacking party and vice versa // General / Infantry round decimal killRate; if (atkWonRound1 != null) { if ((bool)atkWonRound1) { killRate = GetAtkKillRate(atkInfantry, totalDefFromInfantry); atkUnitsLost += atkUnits.ApplyKillRateAtkInfantry(killRate); infantryGroupDefUnitSetLost = infantryGroupDefUnitSet.ApplyKillRate(1); if (strongestGroupIndex == 1) { atkUnitsLost += atkUnits.ApplyKillRateAtkSpecial(killRate); } } else { killRate = GetDefKillRate(atkInfantry, totalDefFromInfantry); atkUnitsLost += atkUnits.ApplyKillRateAtkInfantry(1); atkUnitsLost += atkUnits.ApplyKillRateAtkSpecial(1); infantryGroupDefUnitSetLost = infantryGroupDefUnitSet.ApplyKillRate(killRate); } } // Cavalry round if (atkWonRound2 != null) { if ((bool)atkWonRound2) { killRate = GetAtkKillRate(atkCavalry, totalDefFromCavalry); atkUnitsLost += atkUnits.ApplyKillRateAtkCavalry(killRate); cavalryGroupDefUnitSetLost = cavalryGroupDefUnitSet.ApplyKillRate(1); if (strongestGroupIndex == 2) { atkUnitsLost += atkUnits.ApplyKillRateAtkSpecial(killRate); } } else { killRate = GetDefKillRate(atkCavalry, totalDefFromCavalry); atkUnitsLost += atkUnits.ApplyKillRateAtkCavalry(1); atkUnitsLost += atkUnits.ApplyKillRateAtkSpecial(1); cavalryGroupDefUnitSetLost = cavalryGroupDefUnitSet.ApplyKillRate(killRate); } } // Archer round if (atkWonRound3 != null) { if ((bool)atkWonRound3) { killRate = GetAtkKillRate(atkArchers, totalDefFromArchers); atkUnitsLost += atkUnits.ApplyKillRateAtkArchers(killRate); archerGroupDefUnitSetLost = archerGroupDefUnitSet.ApplyKillRate(1); if (strongestGroupIndex == 3) { atkUnitsLost += atkUnits.ApplyKillRateAtkSpecial(killRate); } } else { killRate = GetDefKillRate(atkArchers, totalDefFromArchers); atkUnitsLost += atkUnits.ApplyKillRateAtkArchers(1); atkUnitsLost += atkUnits.ApplyKillRateAtkSpecial(1); archerGroupDefUnitSetLost = archerGroupDefUnitSet.ApplyKillRate(killRate); } } UnitSet survivingDefUnits = infantryGroupDefUnitSet + cavalryGroupDefUnitSet + archerGroupDefUnitSet; // Compensate for the rounding error by adding the difference to the unitsLost UnitSet defUnitsLost = infantryGroupDefUnitSetLost + cavalryGroupDefUnitSetLost + archerGroupDefUnitSetLost + difference; currentRound.AtkUnits = atkUnits; currentRound.AtkUnitsLost = atkUnitsLost; currentRound.DefUnits = survivingDefUnits; currentRound.DefUnitsLost = defUnitsLost; // Check if during the 3 mini-battles either attack of defense won all mini-battles battleDetermined = !(miniBattleResult.Contains(false) && miniBattleResult.Contains(true)); if (!battleDetermined) { // WallDefense is only added the first round wallDefense = 0; BattleHistory.Add(currentRound.Copy()); //Reset the UnitLost for subsequent rounds BattleHistory.Last().AtkUnitsLost = new UnitSet(); BattleHistory.Last().DefUnitsLost = new UnitSet(); } } BattleResult finalResult = result.Copy(); foreach (BattleResult battleResult in BattleHistory) { finalResult.AtkUnitsLost += battleResult.AtkUnitsLost; finalResult.DefUnitsLost += battleResult.DefUnitsLost; } // Simulate the post battle calculations PostBattle(ref finalResult, paladinAtkWeapon); // stopwatch.Stop(); Log.Debug($"Ending Battle Simlation"); return(finalResult); }
public static int PreRound(ref BattleResult result, WeaponSet atkWeapon) { // Abort when there are no attacking rams or catapults, assumes nothing else damages the wall if (result.AtkUnits.Ram == 0 && result.AtkUnits.Catapult == 0) { result.WallLevelAfter = result.WallLevelBefore; return(result.WallLevelBefore); } // Based on https://en.forum.tribalwars2.com/index.php?threads/unraveling-some-myths-regarding-the-battle-engine.3959/ int wallLevel = Math.Clamp(result.WallLevelBefore, 0, 20); decimal atkModifier = result.AtkBattleModifier / 100m; int ironWall = 0; //TODO Need to make an battleCalculatorInput that takes the Tribe skill Iron wall into account decimal paladinModifier = (atkWeapon.BelongsToUnitType == UnitType.Ram ? atkWeapon.AtkModifier : 0) + 1; UnitSet atkUnits = result.AtkUnits; UnitSet defUnits = result.DefUnits; UnitSet atkUnitsLost = result.AtkUnitsLost; // Calculate how many rams were killed by the Trebuchet int ramsKilled = GetRamsKilled(atkUnits.Ram, atkUnits.Catapult, defUnits.Trebuchet); atkUnits.Ram -= ramsKilled; atkUnitsLost.Ram = ramsKilled; // Get the total attacking provisions without the ram provisions included. int totalProvisionsWithNoRams = atkUnits.GetTotalProvisions() - atkUnits.GetTotalRamProvisions(); // Get base wall defense int wallDefense = GetWallDefense(wallLevel); // This is meant to calculate the "active" rams that will do damage in relation to the attacking force. // More infantry = more damage with the rams int provisionDefense = (defUnits.GetTotalProvisions() + wallDefense); decimal ramRatio = 0; if (provisionDefense > 0) { ramRatio = Math.Clamp((decimal)totalProvisionsWithNoRams / provisionDefense, 0, 1); } int wallHitPoints = (Wall.GetHitPoints(wallLevel) * 2); // This is the net wall damage done by the rams decimal wallDamage = (atkUnits.Ram * ramRatio * atkModifier * paladinModifier); // If wallHitPoints is not zero then divide by if (wallHitPoints > 0) { wallDamage /= wallHitPoints; } // Calculate the new wall level after damage applied int resultingWallLevel; // If the wall is already below the Iron Wall threshold then don't change anything. if (wallLevel <= ironWall) { resultingWallLevel = wallLevel; } else { if (wallLevel - ironWall < -wallDamage) { resultingWallLevel = wallLevel < ironWall ? wallLevel : ironWall; } else { decimal rawLevel = wallLevel - wallDamage; resultingWallLevel = (int)Math.Round(rawLevel, GameRounding); } } result.WallLevelAfter = Math.Clamp(resultingWallLevel, 0, 20); result.AtkUnitsLost = atkUnitsLost; return(result.WallLevelAfter); }