2525#include " gameTypes/GameTypesOutput.h"
2626#include " gameData/MilitaryConsts.h"
2727#include " gameData/SettingTypeConv.h"
28+ #include " rttr/test/random.hpp"
2829#include < rttr/test/testHelpers.hpp>
2930#include < boost/test/data/monomorphic.hpp>
3031#include < boost/test/data/test_case.hpp>
3132#include < boost/test/unit_test.hpp>
3233#include < array>
3334#include < iostream>
35+ #include < numeric>
3436
3537using SoldierState = nofActiveSoldier::SoldierState;
3638
@@ -45,12 +47,20 @@ BOOST_AUTO_TEST_SUITE(AttackSuite)
4547
4648namespace {
4749
50+ namespace dataset = boost::unit_test::data;
51+
4852struct AttackDefaults
4953{
5054 static constexpr unsigned width = 20 ;
5155 static constexpr unsigned height = 12 ;
5256};
5357
58+ template <typename T>
59+ auto calcSum (const T& collection)
60+ {
61+ return std::accumulate (std::begin (collection), std::end (collection), 0u );
62+ }
63+
5464// / Reschedule the walk event of the obj to be executed in numGFs GFs
5565void rescheduleWalkEvent (TestEventManager& em, noMovable& obj, unsigned numGFs)
5666{
@@ -95,13 +105,11 @@ struct AttackFixtureBase : public WorldWithGCExecution<T_numPlayers, T_width, T_
95105
96106 AttackFixtureBase ()
97107 {
98- const auto soldiers = PeopleCounts::make (Job::General, 3 );
99108 for (unsigned i = 0 ; i < T_numPlayers; i++)
100109 {
101110 curPlayer = i;
102111 hqPos[i] = world.GetPlayer (i).GetHQPos ();
103112 MakeVisible (hqPos[i]);
104- world.template GetSpecObj <nobBaseWarehouse>(hqPos[i])->AddToInventory (soldiers, true );
105113 this ->ChangeMilitary (MILITARY_SETTINGS_SCALE);
106114 }
107115 curPlayer = 0 ;
@@ -145,6 +153,29 @@ struct AttackFixtureBase : public WorldWithGCExecution<T_numPlayers, T_width, T_
145153 AddSoldiers (bldPos, numWeak, Job::Private);
146154 AddSoldiers (bldPos, numStrong, Job::General);
147155 }
156+
157+ // Named constants for indices to make access to the returned array easier to read
158+ static constexpr unsigned real = 0 ;
159+ static constexpr unsigned visual = 1 ;
160+ static auto getNumSoldiers (const nobBaseWarehouse& wh)
161+ {
162+ std::array<std::array<unsigned , NUM_SOLDIER_RANKS>, 2 > soldiers{};
163+ for (const auto job : SOLDIER_JOBS)
164+ {
165+ soldiers[real][getSoldierRank (job)] = wh.GetNumRealFigures (job);
166+ soldiers[visual][getSoldierRank (job)] = wh.GetNumVisualFigures (job);
167+ }
168+ return soldiers;
169+ }
170+ // Get counts of soldiers
171+ auto getTotalSoldiers (unsigned player)
172+ {
173+ const auto & inv = world.GetPlayer (player).GetInventory ();
174+ std::array<unsigned , NUM_SOLDIER_RANKS> soldiers{};
175+ for (const auto job : SOLDIER_JOBS)
176+ soldiers[getSoldierRank (job)] = inv[job];
177+ return soldiers;
178+ }
148179};
149180
150181// Size is chosen based on current maximum attacking distances!
@@ -158,6 +189,12 @@ struct NumSoldierTestFixture : public AttackFixtureBase<3, 56, 38>
158189
159190 NumSoldierTestFixture () : gwv(curPlayer, world)
160191 {
192+ // Add some soldiers to the HQs
193+ for (const auto pos : hqPos)
194+ {
195+ world.GetSpecObj <nobBaseWarehouse>(pos)->AddToInventory (
196+ PeopleCounts::make (Job::General, rttr::test::randomValue (3 , 10 )), true );
197+ }
161198 // Assert player positions: 0: Top-Left, 1: Top-Right, 2: Bottom-Middle
162199 BOOST_TEST_REQUIRE (hqPos[0 ].x < hqPos[1 ].x );
163200 BOOST_TEST_REQUIRE (hqPos[0 ].y < hqPos[2 ].y );
@@ -557,8 +594,8 @@ enum Case
557594};
558595
559596BOOST_DATA_TEST_CASE_F (AttackFixture<>, ConquerBldArmorAddon,
560- boost::unit_test::data:: make (std::array{Case::EnableAfterDisable, Case::DisableAfterEnable,
561- Case::KeepEnabled, Case::KeepDisabled}))
597+ dataset:: make (std::array{Case::EnableAfterDisable, Case::DisableAfterEnable, Case::KeepEnabled ,
598+ Case::KeepDisabled}))
562599{
563600 if (sample == Case::EnableAfterDisable)
564601 this ->ggs .setSelection (AddonId::ARMOR_CAPTURED_BLD, 1 );
@@ -972,61 +1009,178 @@ BOOST_FIXTURE_TEST_CASE(FlagBecomesUnreachableForWaitingAttacker, AttackFixture<
9721009 BOOST_TEST (attacker3.GetState () == SoldierState::AttackingFightingVsDefender);
9731010}
9741011
975- BOOST_FIXTURE_TEST_CASE (CancelAgressiveDefenders, AttackFixture<2 >)
1012+ enum AttackedBldType
1013+ {
1014+ Hq,
1015+ MillitaryBld
1016+ };
1017+
1018+ #ifndef __INTELLISENSE__
1019+ BOOST_DATA_TEST_CASE_F (AttackFixture<2 >, HandleLeavingAggDefendersOnAttack,
1020+ dataset::make (std::array{AttackedBldType::Hq, AttackedBldType::MillitaryBld})
1021+ * dataset::make(std::array{false , true }),
1022+ test_case, useAllDefenders)
1023+ #else
1024+ struct HandleLeavingAggDefendersOnAttack : AttackFixture<2 >
1025+ {
1026+ void test (AttackedBldType test_case, bool useAllDefenders);
1027+ };
1028+ void HandleLeavingAggDefendersOnAttack::test (AttackedBldType test_case, bool useAllDefenders)
1029+ #endif
9761030{
1031+ // When defender is called, leaving aggressive defenders should be canceled.
1032+ // When there is no other soldier left to send as defender, an agressive defenders should be used as defender
1033+ // instead.
1034+ //
9771035 // Reproduce issue #1907:
9781036 // Accounting of soldiers that were about to leave but got canceled was wrong
979- // Situation:
1037+ // Situation (happens with a single attacker already) :
9801038 // - Attack on "military storehouse", i.e. HQ.
9811039 // - HQ sends aggressive defender about to leave.
9821040 // - Attacker reaches flag and requests defender which cancels any agressive defenders
983- AddSoldiers (milBld0Pos, 4 , Job::General);
1041+ // - Readding that defender cause error in accounting
1042+ //
1043+ // Handling for "real" military buildings and attackable warehouses (HQ) is different, so test all combinations
1044+ std::array<nofAttacker*, 5 > attackers{};
1045+ AddSoldiers (milBld0Pos, attackers.size () + 1 , Job::General);
9841046 auto & attackedHq = ensureNonNull (world.GetSpecObj <nobBaseWarehouse>(hqPos[1 ]));
985- const auto getSoldiers = [&attackedHq]() {
986- std::array<std::array<unsigned , NUM_SOLDIER_RANKS>, 2 > soldiers{};
987- for (const auto job : SOLDIER_JOBS)
1047+ auto & attackedBld = (test_case == AttackedBldType::Hq) ? attackedHq : static_cast <nobBaseMilitary&>(*milBld1);
1048+ const auto attackedPlayer = attackedBld.GetPlayer ();
1049+ const auto numGenerals = rttr::test::randomValue<unsigned >(1 , attackers.size () / 2 );
1050+ const auto numPrivates = rttr::test::randomValue<unsigned >(1 , attackers.size () / 2 );
1051+ if (test_case == AttackedBldType::Hq)
1052+ {
1053+ // Make sure they can be sent out (aggressive defenders are not taken from the reserve)
1054+ attackedHq.SetRealReserve (getSoldierRank (Job::General), 0 );
1055+ attackedHq.SetRealReserve (getSoldierRank (Job::Private), 0 );
1056+ auto startSoldiers = PeopleCounts::make (Job::General, numGenerals);
1057+ startSoldiers[Job::Private] = numPrivates;
1058+ attackedHq.AddToInventory (startSoldiers, true );
1059+ } else
1060+ {
1061+ AddSoldiers (attackedBld.GetPos (), numGenerals, Job::General);
1062+ AddSoldiers (attackedBld.GetPos (), numPrivates, Job::Private);
1063+ }
1064+ // Total soldiers should not change because none die in this test case
1065+ // This ensures they are not double-accounted during conversion from agressive defender to defender
1066+ // and not missed when removed from the leave queue
1067+ const auto totalSoldiersBefore = getTotalSoldiers (attackedPlayer);
1068+ const auto soldiersBefore = getNumSoldiers (attackedHq);
1069+ const auto numAttackers = useAllDefenders ? attackers.size () : 1u ;
1070+ this ->Attack (attackedBld.GetPos (), numAttackers, true );
1071+ BOOST_TEST_REQUIRE (milBld0->GetLeavingFigures ().size () == numAttackers);
1072+ {
1073+ int i = 0 ;
1074+ for (auto & fig : milBld0->GetLeavingFigures ())
1075+ attackers[i++] = &dynamic_cast <nofAttacker&>(fig);
1076+ }
1077+ // Let one out then call SendAggressiveDefender for all attackers.
1078+ // This is technically wrong but we want to ensure they come from the target bld only and are still in the leave
1079+ // queue when the first attacker calls the defender
1080+ RTTR_EXEC_TILL (50 , milBld0->GetLeavingFigures ().size () < numAttackers);
1081+ for (unsigned i = 0 ; i < numAttackers; i++)
1082+ {
1083+ auto * attacker = attackers[i];
1084+ if (attacker && !attacker->GetHuntingDefender ())
9881085 {
989- soldiers[0 ][getSoldierRank (job)] = attackedHq.GetNumRealFigures (job);
990- soldiers[1 ][getSoldierRank (job)] = attackedHq.GetNumVisualFigures (job);
1086+ auto * aggDefender = attackedBld.SendAggressiveDefender (*attacker);
1087+ if (aggDefender)
1088+ attacker->LetsFight (*aggDefender);
9911089 }
992- return soldiers;
993- };
994- auto soldiersBefore = getSoldiers ();
995-
996- this ->Attack (attackedHq.GetPos (), 1 , true );
997- BOOST_TEST_REQUIRE (milBld0->GetLeavingFigures ().size () == 1u );
998- // multiple soldiers and last is aggressive
999- auto & attacker = dynamic_cast <nofAttacker&>(milBld0->GetLeavingFigures ().front ());
1000- RTTR_EXEC_TILL (70 , milBld0->GetLeavingFigures ().empty ()); // -V807
1001- if (!attacker.GetHuntingDefender ())
1090+ }
1091+ if (useAllDefenders)
10021092 {
1003- auto * aggDefender = attackedHq.SendAggressiveDefender (attacker);
1004- BOOST_TEST_REQUIRE (aggDefender);
1005- attacker.LetsFight (*aggDefender);
1093+ if (test_case == AttackedBldType::Hq) // HQs will be empty as all are leaving
1094+ BOOST_TEST_REQUIRE (attackedBld.GetLeavingFigures ().size () == numGenerals + numPrivates);
1095+ else // Military buildings keep at least one
1096+ BOOST_TEST_REQUIRE (attackedBld.GetLeavingFigures ().size () == numGenerals + numPrivates - 1u );
1097+ } else
1098+ BOOST_TEST_REQUIRE (!attackedBld.GetLeavingFigures ().empty ());
1099+ if (test_case == AttackedBldType::Hq)
1100+ {
1101+ auto curSoldiers = getNumSoldiers (attackedHq);
1102+ // Still there visually
1103+ BOOST_TEST (curSoldiers[visual] == soldiersBefore[visual]);
1104+ // but not real
1105+ BOOST_TEST (curSoldiers[real] != soldiersBefore[real]);
1106+ if (useAllDefenders)
1107+ BOOST_TEST (calcSum (curSoldiers[real]) == 0u );
1108+ else
1109+ BOOST_TEST (calcSum (curSoldiers[real]) == calcSum (soldiersBefore[real]) - numAttackers);
10061110 }
1007- BOOST_TEST_REQUIRE (!attackedHq.GetLeavingFigures ().empty ());
1008- // Still there visually, but not real
1009- auto soldiersNow = getSoldiers ();
1010- BOOST_TEST (soldiersNow[0 ] != soldiersBefore[0 ]);
1011- BOOST_TEST (soldiersNow[1 ] == soldiersBefore[1 ]);
1012- moveObjTo (world, attacker, attackedHq.GetFlagPos ());
1111+ BOOST_TEST (getTotalSoldiers (attackedPlayer) == totalSoldiersBefore);
1112+ // Move attacker to flag to trigger defender request
1113+ auto & attacker = *attackers.front ();
1114+ moveObjTo (world, attacker, attackedBld.GetFlagPos ());
10131115 rescheduleWalkEvent (em, attacker, 1 );
10141116 RTTR_SKIP_GFS (1 );
10151117 BOOST_TEST_REQUIRE (attacker.GetState () == SoldierState::AttackingWaitingForDefender);
10161118 // All agressive defenders are cancelled
1017- for (const auto & fig : attackedHq .GetLeavingFigures ())
1119+ for (const auto & fig : attackedBld .GetLeavingFigures ())
10181120 BOOST_TEST (fig.GetGOT () != GO_Type::NofAggressivedefender);
1019- BOOST_TEST_REQUIRE (!attackedHq .GetLeavingFigures ().empty ());
1121+ BOOST_TEST_REQUIRE (!attackedBld .GetLeavingFigures ().empty ());
10201122 // Defender is coming
1021- BOOST_TEST_REQUIRE (attackedHq.GetLeavingFigures ().front ().GetGOT () == GO_Type::NofDefender);
1022- const auto defenderRank = getSoldierRank (attackedHq.GetLeavingFigures ().front ().GetJobType ());
1023- // Counts unchanged until defender is out
1024- BOOST_TEST (getSoldiers () == soldiersNow);
1123+ BOOST_TEST_REQUIRE (attackedBld.GetLeavingFigures ().front ().GetGOT () == GO_Type::NofDefender);
1124+ const auto defenderRank = getSoldierRank (attackedBld.GetLeavingFigures ().front ().GetJobType ());
1125+ if (test_case == AttackedBldType::Hq)
1126+ {
1127+ const auto curSoldiers = getNumSoldiers (attackedHq);
1128+ // Only defender is missing, only from real count (until actually left)
1129+ BOOST_TEST (calcSum (curSoldiers[real]) == calcSum (soldiersBefore[real]) - 1 );
1130+ BOOST_TEST (curSoldiers[visual] == soldiersBefore[visual]);
1131+ }
1132+ BOOST_TEST (getTotalSoldiers (attackedPlayer) == totalSoldiersBefore);
1133+ RTTR_EXEC_TILL (70 , attackedBld.GetLeavingFigures ().empty ());
1134+ if (test_case == AttackedBldType::Hq)
1135+ {
1136+ auto soldiersExpected = soldiersBefore;
1137+ soldiersExpected[visual][defenderRank] = --soldiersExpected[real][defenderRank]; // Defender is out
1138+ BOOST_TEST (getNumSoldiers (attackedHq) == soldiersExpected);
1139+ }
1140+ BOOST_TEST (getTotalSoldiers (attackedPlayer) == totalSoldiersBefore);
1141+ }
1142+
1143+ BOOST_FIXTURE_TEST_CASE (LeavingSoldierUsedAsDefender, AttackFixture<2 >)
1144+ {
1145+ // When the only soldier in a HQ is currently leaving (to a building) it is used as defender
1146+ // Counts need to be correct at all times
1147+ AddSoldiers (milBld0Pos, 2 , Job::General);
1148+ auto & attackedHq = ensureNonNull (world.GetSpecObj <nobBaseWarehouse>(hqPos[1 ]));
1149+ const auto soldierJob = rttr::test::randomElement (SOLDIER_JOBS);
1150+ attackedHq.SetRealReserve (getSoldierRank (soldierJob), 0 );
1151+ attackedHq.AddToInventory (PeopleCounts::make (soldierJob, 1 ), true );
1152+ const auto attackedPlayer = attackedHq.GetPlayer ();
1153+ const auto totalSoldiersBefore = getTotalSoldiers (attackedPlayer);
1154+ const auto soldiersBefore = getNumSoldiers (attackedHq);
1155+
1156+ this ->Attack (attackedHq.GetPos (), 1 , true );
1157+ BOOST_TEST_REQUIRE (milBld0->GetLeavingFigures ().size () == 1 );
1158+ auto & attacker = dynamic_cast <nofAttacker&>(milBld0->GetLeavingFigures ().front ());
1159+ RTTR_EXEC_TILL (50 , milBld0->GetLeavingFigures ().empty ());
1160+ curPlayer = attackedPlayer;
1161+ BuildRoadForBlds (milBld1Pos, hqPos[1 ]);
1162+ curPlayer = 0 ;
1163+ BOOST_TEST_REQUIRE (!attackedHq.GetLeavingFigures ().empty ());
1164+ {
1165+ const auto & leavingSoldier = dynamic_cast <nofSoldier&>(attackedHq.GetLeavingFigures ().front ());
1166+ BOOST_TEST (leavingSoldier.GetGoal () == milBld1);
1167+ }
1168+ BOOST_TEST (getTotalSoldiers (attackedPlayer) == totalSoldiersBefore);
1169+ const auto soldiersNow = getNumSoldiers (attackedHq);
1170+ BOOST_TEST (soldiersNow[visual] == soldiersBefore[visual]);
1171+ BOOST_TEST (calcSum (soldiersNow[real]) == 0u );
1172+ moveObjTo (world, attacker, attackedHq.GetFlagPos ());
1173+ rescheduleWalkEvent (em, attacker, 1 );
1174+ RTTR_SKIP_GFS (1 );
1175+ BOOST_TEST_REQUIRE (attackedHq.GetLeavingFigures ().size () == 1 );
1176+ BOOST_TEST (attackedHq.GetLeavingFigures ().front ().GetGOT () == GO_Type::NofDefender);
1177+ BOOST_TEST (getTotalSoldiers (attackedPlayer) == totalSoldiersBefore);
1178+ BOOST_TEST (getNumSoldiers (attackedHq) == soldiersNow);
10251179 RTTR_EXEC_TILL (70 , attackedHq.GetLeavingFigures ().empty ());
1026- soldiersNow = getSoldiers ( );
1027- auto soldiersExpected = soldiersBefore ;
1028- soldiersExpected[ 1 ][defenderRank] = --soldiersExpected[ 0 ][defenderRank]; // Defender is out
1029- BOOST_TEST (soldiersNow == soldiersExpected );
1180+ BOOST_TEST ( getTotalSoldiers (attackedPlayer) == totalSoldiersBefore );
1181+ const auto soldiersAfter = getNumSoldiers (attackedHq) ;
1182+ BOOST_TEST ( calcSum (soldiersAfter[visual]) == 0u );
1183+ BOOST_TEST (calcSum (soldiersAfter[real]) == 0u );
10301184}
10311185
10321186using DestroyRoadsOnConquerFixture = AttackFixture<2 , 24 >;
0 commit comments