Skip to content

Commit 86fc349

Browse files
committed
refactor combat sim to match Kirilloids. Tests now don't pass anymore. Thanks Kirill.
1 parent 3be415e commit 86fc349

File tree

12 files changed

+495
-270
lines changed

12 files changed

+495
-270
lines changed

TbsCore/Helpers/HeroHelper.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ public static (TroopsEnum, int) ParseWeapon(HeroItemEnum item)
107107
return (TroopsEnum.None, 0);
108108
}
109109

110-
public static int GetArmorBaseStrength(string name)
110+
public static int GetArmorStrength(string name)
111111
{
112112
switch (name)
113113
{
@@ -117,6 +117,16 @@ public static int GetArmorBaseStrength(string name)
117117
}
118118
}
119119

120+
public static int GetArmorDmgReduce(string name, int tier)
121+
{
122+
switch (name)
123+
{
124+
case "Scale": return 2 + 2 * tier;
125+
case "Segmented": return 2 + tier;
126+
default: return 0;
127+
}
128+
}
129+
120130
private static int GetWeaponBoost(TroopsEnum troop, int tier)
121131
{
122132
var upkeep = TroopsData.GetTroopUpkeep(troop);

TbsCore/Models/CombatModels/Combat.cs

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
using TravBotSharp.Files.TravianData;
5+
using static TravBotSharp.Files.Helpers.Classificator;
6+
7+
namespace TbsCore.Models.CombatModels
8+
{
9+
public class Combat
10+
{
11+
public CombatAttacker Attacker { get; set; }
12+
public CombatDeffender Deffender { get; set; }
13+
14+
15+
private (long, long) finalState;
16+
private (long, long) baseState;
17+
18+
private readonly int baseVillDeff = 10;
19+
20+
public (double, double) Raid()
21+
{
22+
CalculateBaseState();
23+
CalculateTotalPoints();
24+
var ratio = CalculateRatio();
25+
// Calc losses
26+
return RaidRatio(ratio);
27+
}
28+
29+
public (double, double) Normal()
30+
{
31+
CalculateBaseState();
32+
CalculateTotalPoints();
33+
// CalculateRams()
34+
var ratio = CalculateRatio();
35+
return NormalRatio(ratio);
36+
// CalculateCatapults()
37+
}
38+
39+
private (double, double) RaidRatio(double x) => (1 / (1 + x), x / (1 + x));
40+
private (double, double) NormalRatio(double x) => (Math.Min(1 / x, 1), Math.Min(x, 1));
41+
42+
private double CalculateBaseRatio() => finalState.Item1 / (double)finalState.Item2;
43+
private double CalculateRatio() => Math.Pow(CalculateBaseRatio(), GetImmensityFactor());
44+
45+
/// <summary>
46+
/// Get Morale/Population bonus
47+
/// </summary>
48+
private double GetMoraleBonus(long off, long deff)
49+
{
50+
if (Deffender.DeffTribe == TribeEnum.Nature)
51+
{
52+
// Note: Nature, the 'account' for unoccupied oases, has its own population (500).
53+
Deffender.Population = 500;
54+
}
55+
56+
if (Attacker.Population < Deffender.Population) return 1.0F;
57+
58+
// So we don't divide by 0
59+
if (Deffender.Population == 0) return 1.5F;
60+
if (Attacker.Population == 0) return 1.0F;
61+
62+
double bonus;
63+
64+
if (deff < off)
65+
{
66+
// M^0.2, where M is attacker's population / defender's population
67+
bonus = Math.Pow((Attacker.Population / (double)Deffender.Population), 0.2F);
68+
}
69+
else
70+
{
71+
// If the attacker has fewer points than defender:
72+
// M ^{ 0.2·(offense points / defense points)}
73+
double exp = 0.2F * (off / (double)deff);
74+
bonus = Math.Pow((Attacker.Population / (double)Deffender.Population), exp);
75+
}
76+
77+
// Moralebonus never goes higher than +50%
78+
if (1.5F < bonus) bonus = 1.5;
79+
80+
return bonus;
81+
}
82+
83+
private double GetWallBonus() // getDefBonus
84+
{
85+
var (bonus, _) = BuildingsData.GetWallBonus(Deffender.DeffTribe, Deffender.WallLevel);
86+
return bonus;
87+
}
88+
89+
private double GetCataMorale()
90+
{
91+
// limit(0.3333, 1)((offPop / defPop) ** -0.3);
92+
if (Deffender.Population == 0) return 1;
93+
var morale = Math.Pow(Attacker.Population / Deffender.Population, -0.3F);
94+
if (morale < 0.3333F) return 0.3333F;
95+
if (1.0F < morale) return 1.0F;
96+
return morale;
97+
}
98+
99+
private int GetVillDeff() // getDefAbsolute
100+
{
101+
var (_, wallDeff) = BuildingsData.GetWallBonus(Deffender.DeffTribe, Deffender.WallLevel);
102+
var palaceDeff = 2 * Math.Pow(Deffender.PalaceLevel, 2);
103+
return wallDeff + baseVillDeff + (int)palaceDeff;
104+
}
105+
106+
private (long, long) CalculateBaseState() // calcBasePoints
107+
{
108+
var offPts = this.Attacker.Army.GetOffense();
109+
Console.WriteLine($"off = ${offPts.i}/${offPts.c}");
110+
var defPts = Deffender.GetDeffense();
111+
Console.WriteLine($"off = ${defPts.i}/${defPts.c}");
112+
var (off, def) = GetAducedDef(offPts, defPts);
113+
Console.WriteLine($"adduced = ${off}/${def}");
114+
baseState = ( off, def );
115+
return baseState;
116+
}
117+
118+
private (long, long) CalculateTotalPoints()
119+
{
120+
var (baseOff, baseDeff) = CalculateBaseState();
121+
var finalDef = RoundLong((baseDeff + GetVillDeff()) * GetWallBonus());
122+
var morale = GetMoraleBonus(baseOff, finalDef);
123+
var finalOff = RoundLong(baseOff * morale);
124+
finalState = (finalOff, finalDef);
125+
return finalState;
126+
}
127+
128+
// K depends on how much soldiers were involved in combat. Really, large, immense battles
129+
// should differ from small battles between hundreds of soldiers.
130+
private double GetImmensityFactor()
131+
{
132+
// N is total amount of units taking part in battle (unit count, not their wheat upkeep)
133+
long N = 0;
134+
N += Attacker.Army.GetTotal();
135+
Deffender.Armies.ForEach(army => N += army.GetTotal());
136+
137+
// K = 2 · (1.8592 – N^0.015)
138+
double K = 2 * (1.8592 - Math.Pow(N, 0.015));
139+
if (1.5 < K) return 1.5;
140+
if (K < 1.2578) return 1.2578;
141+
return K;
142+
}
143+
144+
private (long, long) GetAducedDef(CombatPoints off, CombatPoints deff)
145+
{
146+
var totalOff = off.i + off.c;
147+
var infantryPart = RoundPercent(off.i / (double)totalOff);
148+
var cavalryPart = RoundPercent(off.c / (double)totalOff);
149+
var totalDef = deff.i * infantryPart + deff.c * cavalryPart;
150+
return (totalOff, RoundLong(totalDef));
151+
}
152+
153+
private double RoundPercent(double val) =>
154+
Math.Round(val, 4, MidpointRounding.AwayFromZero);
155+
private long RoundLong(double val) =>
156+
(long)Math.Round(val, MidpointRounding.AwayFromZero);
157+
}
158+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using TbsCore.TravianData;
6+
using TravBotSharp.Files.Helpers;
7+
8+
namespace TbsCore.Models.CombatModels
9+
{
10+
public class CombatArmy : CombatArmyBase
11+
{
12+
13+
public new void ApplyLosses(double losses)
14+
{
15+
base.ApplyLosses(losses);
16+
if ((Hero?.Info?.Health ?? 0) != 0)
17+
{
18+
var armBonus = Hero.GetArmorDmgReduction();
19+
Hero.Info.Health = (int)Math.Max(Hero.Info.Health - losses * 100.0F + armBonus, 0.0F);
20+
}
21+
}
22+
23+
public new CombatPoints GetOffense()
24+
{
25+
var unitsOff = base.GetOffense();
26+
if ((Hero?.Info?.Health ?? 0) == 0) return unitsOff;
27+
28+
var heroOff = Hero.GetOff();
29+
30+
var (troop, boost) = Hero.GetWeaponBoost();
31+
var weaponBoost = CombatPoints.both(base.Troops[(int)troop % 10] * boost);
32+
33+
return CombatPoints.sum(new CombatPoints[]{
34+
unitsOff, heroOff, weaponBoost
35+
}).Mul(Hero.GetOffBonus());
36+
}
37+
38+
public new CombatPoints GetDeffense()
39+
{
40+
var unitsDeff = base.GetDeffense();
41+
if ((Hero?.Info?.Health ?? 0) == 0) return unitsDeff;
42+
43+
var heroDeff = Hero.GetDeff();
44+
45+
var (troop, boost) = Hero.GetWeaponBoost();
46+
var weaponBoost = CombatPoints.both(base.Troops[(int)troop % 10] * boost);
47+
48+
return CombatPoints.sum(new CombatPoints[]{
49+
unitsDeff, heroDeff, weaponBoost
50+
}).Mul(Hero.GetDeffBonus());
51+
}
52+
}
53+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using TbsCore.TravianData;
6+
using TravBotSharp.Files.Helpers;
7+
8+
namespace TbsCore.Models.CombatModels
9+
{
10+
public class CombatArmyBase
11+
{
12+
public Classificator.TribeEnum Tribe { get; set; }
13+
public int[] Troops { get; set; }
14+
public int[] Improvements { get; set; }
15+
public CombatHero Hero { get; set; }
16+
17+
18+
public void ApplyLosses(double losses)
19+
{
20+
for (int i = 0; i < this.Troops.Length; i++)
21+
{
22+
this.Troops[i] = (int)Math.Round(this.Troops[i] * (1 - losses), MidpointRounding.AwayFromZero);
23+
}
24+
}
25+
public int GetTotal() => Troops.Sum();
26+
27+
public CombatPoints GetOffense()
28+
{
29+
double inf = 0, cav = 0;
30+
for (int i = 0; i < 10; i++)
31+
{
32+
if (Troops[i] == 0) continue;
33+
var troop = TroopsHelper.TroopFromInt(Tribe, i);
34+
35+
var lvl = Improvements == null ? 1 : Improvements[i];
36+
var off = TroopsData.GetTroopOff(troop, lvl);
37+
38+
if (TroopsData.IsInfantry(troop)) inf += off * Troops[i];
39+
else cav += off * Troops[i];
40+
}
41+
return new CombatPoints(inf, cav);
42+
}
43+
44+
public CombatPoints GetDeffense()
45+
{
46+
double inf = 0, cav = 0;
47+
for (int i = 0; i < 10; i++)
48+
{
49+
if (Troops[i] == 0) continue;
50+
var troop = TroopsHelper.TroopFromInt(Tribe, i);
51+
var lvl = Improvements == null ? 1 : Improvements[i];
52+
var (deffInf, deffCav) = TroopsData.GetTroopDeff(troop, lvl);
53+
54+
inf += deffInf * Troops[i];
55+
cav += deffCav * Troops[i];
56+
}
57+
return new CombatPoints(inf, cav);
58+
}
59+
60+
/// <summary>
61+
/// Gets rams number and their upgrade level
62+
/// </summary>
63+
public (int, int) GetRams() => (Troops[7], Improvements[7]);
64+
65+
/// <summary>
66+
/// Gets rams number and their upgrade level
67+
/// </summary>
68+
public (int, int) GetCatapults() => (Troops[8], Improvements[8]);
69+
70+
}
71+
}

TbsCore/Models/CombatModels/CombatAttacker.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public class CombatAttacker
99
/// <summary>
1010
/// Troops, hero, tribe
1111
/// </summary>
12-
public CombatBase Army { get; set; }
12+
public CombatArmy Army { get; set; }
1313

1414
/// <summary>
1515
/// Population of the attacker, morale bonus depends on it
@@ -20,5 +20,8 @@ public class CombatAttacker
2020
/// Ally metalurgy percentage (2% => 2, 4% => 4)
2121
/// </summary>
2222
public int Metalurgy { get; set; }
23+
24+
25+
// TODO: add catapult targets, add brewery support
2326
}
2427
}

TbsCore/Models/CombatModels/CombatBase.cs

Lines changed: 0 additions & 15 deletions
This file was deleted.

TbsCore/Models/CombatModels/CombatDeffender.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public class CombatDeffender
1010
/// <summary>
1111
/// List of (Troops, hero, tribe), since deffender can have armies from multiple accounts / villages
1212
/// </summary>
13-
public List<CombatBase> Armies { get; set; }
13+
public List<CombatArmy> Armies { get; set; }
1414

1515
/// <summary>
1616
/// Population of the deffender, morale bonus depends on it
@@ -21,6 +21,11 @@ public class CombatDeffender
2121
/// Level of the wall inside the village
2222
/// </summary>
2323
public int WallLevel { get; set; }
24+
25+
/// <summary>
26+
/// Level of Palace / Residence
27+
/// </summary>
28+
public int PalaceLevel { get; set; }
2429

2530
/// <summary>
2631
/// Which tribe is the deffender. Wall bonus depends on the tribe
@@ -31,5 +36,16 @@ public class CombatDeffender
3136
/// Ally metalurgy percentage (2% => 2, 4% => 4)
3237
/// </summary>
3338
public int Metalurgy { get; set; }
39+
40+
public CombatPoints GetDeffense()
41+
{
42+
CombatPoints ret = new CombatPoints();
43+
foreach (var army in Armies)
44+
{
45+
ret.Add(army.GetDeffense());
46+
}
47+
return ret;
48+
}
49+
3450
}
3551
}

0 commit comments

Comments
 (0)