コード例 #1
0
        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);
        }
コード例 #2
0
        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);
        }