Skip to content

Commit 2d82f08

Browse files
FlamefireFlow86
authored andcommitted
Extent test case to handle millitary buildings & HQ w/ full & empty troops
When a defender is requested then a) Leaving agg. defenders are stopped b) A defender is chosen from remaining troops This might "convert" and agg. defender to a defender. As this is kind of a sub-case of the existing one, merge them with a data-param. Similar the handling for warehouses and military buildings needs to be checked, so make that another param.
1 parent 8b2e540 commit 2d82f08

2 files changed

Lines changed: 203 additions & 42 deletions

File tree

tests/s25Main/integration/testAttacking.cpp

Lines changed: 195 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@
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

3537
using SoldierState = nofActiveSoldier::SoldierState;
3638

@@ -45,12 +47,20 @@ BOOST_AUTO_TEST_SUITE(AttackSuite)
4547

4648
namespace {
4749

50+
namespace dataset = boost::unit_test::data;
51+
4852
struct 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
5565
void 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

559596
BOOST_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

10321186
using DestroyRoadsOnConquerFixture = AttackFixture<2, 24>;

tests/testHelpers/rttr/test/random.hpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (C) 2005 - 2021 Settlers Freaks (sf-team at siedler25.org)
1+
// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org)
22
//
33
// SPDX-License-Identifier: GPL-2.0-or-later
44

@@ -35,4 +35,11 @@ auto randomPoint(typename T::ElementType min = std::numeric_limits<typename T::E
3535
return T{randomValue(min, max), randomValue(min, max)};
3636
}
3737
std::string randString(int len = -1);
38+
39+
template<typename ContainerT>
40+
auto randomElement(ContainerT& container)
41+
{
42+
return helpers::getRandomElement(getRandState(), container);
43+
}
44+
3845
} // namespace rttr::test

0 commit comments

Comments
 (0)