// Headless tests of milestone M7 (DESIGN_UE.md section 10): the validity rules of build mode (U1), the recipe costs (V8) and the arm's choice (U14). #include "Tests/DescentTestCommon.h" #include "Core/Tuning/DescentTune_Carrier.h" #include "Systems/DescentAutomation.h" #include "Systems/DescentBuild.h" #include "Systems/DescentCraft.h" #include "Vehicles/CarrierLayout.h" #if WITH_DEV_AUTOMATION_TESTS IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDescentBuildRulesTest, "Descent.Build.Rules", DESCENT_TEST_FLAGS) bool FDescentBuildRulesTest::RunTest(const FString& Parameters) { namespace DB = Descent::Build; namespace BT = Descent::Tune::Carrier; const DB::FKitDef* Table = DB::FindKit(TEXT("kit_table")); const DB::FKitDef* Light = DB::FindKit(TEXT("light")); const DB::FKitDef* Chair = DB::FindKit(TEXT("chair")); if (!TestNotNull(TEXT("kit_table"), Table) || !TestNotNull(TEXT("light"), Light) || !TestNotNull(TEXT("chair"), Chair)) { return false; } const double D3 = BT::DeckFloorZ3; const FVector Up = FVector::UpVector; DB::FPlacement P; FString Why; // the example that passes: a table on the habitation floor, snapped to the 0.25 m grid TestTrue(TEXT("a table on the habitation floor is valid"), DB::Validate(*Table, FVector(11.1, -5.07, D3), Up, 0.0, true, {}, 0, P, Why)); AddInfo(FString::Printf(TEXT("valid example: reason '%s', snapped to (%.2f, %.2f)"), *Why, P.PosM.X, P.PosM.Y)); TestEqual(TEXT("snapped to the grid"), P.PosM.X, 11.0, 1e-9); // 1 the mount TestFalse(TEXT("rule 1: a table on a wall"), DB::Validate(*Table, FVector(11, -5, D3 + 1.0), FVector(0, 1, 0), 0.0, true, {}, 0, P, Why)); TestEqual(TEXT("rule 1 reason"), Why, FString(TEXT("BAD_SURFACE"))); TestFalse(TEXT("rule 1: a work light on the floor"), DB::Validate(*Light, FVector(11, -5, D3), Up, 0.0, true, {}, 0, P, Why)); TestTrue(TEXT("a work light under the ceiling is valid"), DB::Validate(*Light, FVector(11, -5, D3 + BT::RoomHeightM), -Up, 0.0, true, {}, 0, P, Why)); // 2 one room TestFalse(TEXT("rule 2: a table through the wall between habitation and fabrication"), DB::Validate(*Table, FVector(6.0, -5.0, D3), Up, 0.0, true, {}, 0, P, Why)); TestEqual(TEXT("rule 2 reason"), Why, FString(TEXT("OUT_OF_ROOM"))); // 3 keep-out: the helm, and the floor in front of a door TestFalse(TEXT("rule 3: a chair at the helm"), DB::Validate(*Chair, FVector(44.5, 0.0, D3), Up, 0.0, true, {}, 0, P, Why)); TestEqual(TEXT("rule 3 reason (helm)"), Why, FString(TEXT("KEEP_OUT"))); int32 DoorsChecked = 0; for (const Descent::Carrier::FDoorDef& D : Descent::Carrier::FLayout::Get().Doors) { for (double Side : { -0.75, 0.75 }) { const FVector2D Mid = D.Mid(); const FVector At = D.WallAtConstX() ? FVector(Mid.X + Side, Mid.Y, D.FloorZ()) : FVector(Mid.X, Mid.Y + Side, D.FloorZ()); const DB::FPlacement Q = DB::MakePlacement(*Chair, At, Up, 0.0, false); if (!DB::RuleInRoom(Q)) { continue; } ++DoorsChecked; TestFalse(*FString::Printf(TEXT("rule 3: a chair 0.75 m inside a door at (%.1f, %.1f)"), At.X, At.Y), DB::RuleKeepOut(Q)); } } TestTrue(TEXT("rule 3 was checked at some doors"), DoorsChecked > 4); // 4 overlap const FBox Blocker(FVector(10.8, -5.2, D3), FVector(11.2, -4.8, D3 + 0.4)); TestFalse(TEXT("rule 4: a table on top of an ore box"), DB::Validate(*Table, FVector(11, -5, D3), Up, 0.0, true, MakeArrayView(&Blocker, 1), 0, P, Why)); TestEqual(TEXT("rule 4 reason"), Why, FString(TEXT("BLOCKED"))); // 5 the four corners on the floor TestFalse(TEXT("rule 5: a table over the moon pool"), DB::RuleFloorCorners(DB::MakePlacementAt(*Table, FVector(BT::PoolX0 + 0.3, BT::PoolY0 + 2.0, BT::DeckFloorZ1), 0.0, DB::Floor))); TestFalse(TEXT("rule 5: a table half a metre above the floor"), DB::RuleFloorCorners(DB::MakePlacementAt(*Table, FVector(11, -5, D3 + 0.5), 0.0, DB::Floor))); TestTrue(TEXT("rule 5: a table on the floor"), DB::RuleFloorCorners(DB::MakePlacementAt(*Table, FVector(11, -5, D3), 0.0, DB::Floor))); // 6 the limit TestFalse(TEXT("rule 6: the 201st object"), DB::Validate(*Table, FVector(11, -5, D3), Up, 0.0, true, {}, DB::MaxPlaced, P, Why)); TestEqual(TEXT("rule 6 reason"), Why, FString(TEXT("LIMIT_200"))); // the bay sweep const DB::FKitDef* Shelf = DB::FindKit(TEXT("shelf")); TestFalse(TEXT("rule 3: nothing 4.5 m tall under the carousel"), DB::RuleKeepOut(DB::MakePlacementAt(*DB::FindKit(TEXT("refinery")), FVector(-29.0, -6.0, BT::DeckFloorZ1 + 1.5), 0.0, DB::Floor))); TestTrue(TEXT("a shelf on the bay floor is under the sweep"), Shelf && DB::RuleKeepOut(DB::MakePlacementAt(*Shelf, FVector(-21.0, -7.5, BT::DeckFloorZ1), 0.0, DB::Floor))); return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDescentRecipesCostsTest, "Descent.Recipes.Costs", DESCENT_TEST_FLAGS) bool FDescentRecipesCostsTest::RunTest(const FString& Parameters) { namespace DC = Descent::Craft; TestTrue(TEXT("the recipe table has the 31 rows of section 6.7"), DC::Recipes().Num() == 31); for (const DC::FRecipe& R : DC::Recipes()) { int32 Have[DC::NumRes]; for (int32 I = 0; I < DC::NumRes; ++I) { Have[I] = R.Cost[I] + 2; } TestTrue(*FString::Printf(TEXT("%s: paid"), *R.Id.ToString()), DC::Pay(R, Have)); for (int32 I = 0; I < DC::NumRes; ++I) { TestEqual(*FString::Printf(TEXT("%s: exactly %d %s taken"), *R.Id.ToString(), R.Cost[I], *DC::ResName(I)), Have[I], 2); } for (int32 Short = 0; Short < DC::NumRes; ++Short) { if (R.Cost[Short] == 0) { continue; } for (int32 I = 0; I < DC::NumRes; ++I) { Have[I] = R.Cost[I]; } --Have[Short]; TestFalse(*FString::Printf(TEXT("%s: refused one %s short"), *R.Id.ToString(), *DC::ResName(Short)), DC::Pay(R, Have)); for (int32 I = 0; I < DC::NumRes; ++I) { TestEqual(TEXT("nothing taken when refused"), Have[I], R.Cost[I] - (I == Short ? 1 : 0)); } } } const DC::FRecipe* Depth = DC::Find(TEXT("depth1")); TestTrue(TEXT("Depth Module Mk I costs 3 ingots and 2 wafers"), Depth && Depth->Cost[DC::Ingot] == 3 && Depth->Cost[DC::Wafer] == 2 && Depth->Cost[DC::Lattice] == 0); const DC::FRecipe* Arm2 = DC::Find(TEXT("kit_arm2")); TestTrue(TEXT("Handling Arm Mk II needs one Mk I built"), Arm2 && Arm2->Needs == TEXT("kit_arm1")); const DC::FRecipe* Harpoon = DC::Find(TEXT("harpoon")); TestTrue(TEXT("the weapon row is locked"), Harpoon && Harpoon->bLocked); return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDescentArmsRanksTest, "Descent.Arms.Ranks", DESCENT_TEST_FLAGS) bool FDescentArmsRanksTest::RunTest(const FString& Parameters) { using Descent::Cargo::FSocketView; int32 From = -1, To = -1; // outfeed box, a free rack socket and the hopper: the box goes to the highest rank { const FSocketView S[] = { { 0, true, 6, 0 }, { 1, false, 0, 6 }, { 2, false, 0, 12 } }; TestTrue(TEXT("outfeed to hopper"), Descent::Cargo::ChooseMove(S, From, To) && From == 0 && To == 2); } // the hopper has room for 3 only: the box waits on the rack { const FSocketView S[] = { { 0, true, 6, 0 }, { 1, false, 0, 6 }, { 2, false, 0, 3 } }; TestTrue(TEXT("outfeed to rack when the hopper is short"), Descent::Cargo::ChooseMove(S, From, To) && From == 0 && To == 1); } // rack boxes and free rack sockets only: no move inside one rank { const FSocketView S[] = { { 1, true, 6, 6 }, { 1, false, 0, 6 }, { 1, false, 0, 6 } }; TestFalse(TEXT("never rack to rack"), Descent::Cargo::ChooseMove(S, From, To)); } // the lowest rank is served first { const FSocketView S[] = { { 1, true, 4, 6 }, { 0, true, 6, 0 }, { 2, false, 0, 12 } }; TestTrue(TEXT("the outfeed before the rack"), Descent::Cargo::ChooseMove(S, From, To) && From == 1 && To == 2); } // random sets: a chosen move always goes up in rank, from a real box to a free socket with room FRandomStream Rng(7); int32 Moves = 0; for (int32 Trial = 0; Trial < 2000; ++Trial) { TArray S; const int32 N = Rng.RandRange(0, 8); for (int32 I = 0; I < N; ++I) { FSocketView V; V.Rank = Rng.RandRange(0, 2); V.bOccupied = V.Rank < 2 && Rng.FRand() < 0.5; V.BoxCount = (V.bOccupied && Rng.FRand() < 0.8) ? Rng.RandRange(1, 6) : 0; V.Capacity = V.Rank == 2 ? Rng.RandRange(0, 12) : (V.Rank == 1 ? 6 : 0); S.Add(V); } if (!Descent::Cargo::ChooseMove(S, From, To)) { continue; } ++Moves; if (!(S[To].Rank > S[From].Rank && S[From].BoxCount > 0 && !S[To].bOccupied && S[To].Capacity >= S[From].BoxCount)) { AddError(FString::Printf(TEXT("trial %d: a move from rank %d to rank %d (box %d, capacity %d, occupied %d)"), Trial, S[From].Rank, S[To].Rank, S[From].BoxCount, S[To].Capacity, S[To].bOccupied)); break; } } TestTrue(TEXT("the random sets produced moves"), Moves > 200); // the joint angles reach the target double A = 0.0, B = 0.0; Descent::Cargo::TwoLinkIK(2.0, 0.5, 1.8, 1.8, A, B); const double Ar = FMath::DegreesToRadians(A), Br = FMath::DegreesToRadians(A + B); TestEqual(TEXT("IK reaches the distance"), 1.8 * FMath::Cos(Ar) + 1.8 * FMath::Cos(Br), 2.0, 1e-6); TestEqual(TEXT("IK reaches the height"), 1.8 * FMath::Sin(Ar) + 1.8 * FMath::Sin(Br), 0.5, 1e-6); return true; } #endif // WITH_DEV_AUTOMATION_TESTS