250 lines
8.9 KiB
C++
250 lines
8.9 KiB
C++
#include "catch.hpp"
|
|
#include "FactoryQueries.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#include "Building.h"
|
|
#include "BuildingConfig.h"
|
|
#include "BuildingSystem.h"
|
|
#include "BuildingType.h"
|
|
#include "ConfigLoader.h"
|
|
#include "GameConfig.h"
|
|
#include "Rotation.h"
|
|
#include "ShipLayout.h"
|
|
#include "ShipsConfig.h"
|
|
#include "Simulation.h"
|
|
#include "SimulationTestAccess.h"
|
|
#include "TestConfig.h"
|
|
|
|
// readBuildingConfig underpins blueprint capture (REQ-UI-BLUEPRINT-STORAGE): it
|
|
// extracts a building's recipe / schematic / layout / splitter filters so they can be
|
|
// stored in the blueprint and reapplied on placement. It reads operational buildings
|
|
// and construction sites alike.
|
|
|
|
namespace
|
|
{
|
|
const BuildingDef* findDef(const GameConfig& cfg, BuildingType type)
|
|
{
|
|
for (const BuildingDef& def : cfg.buildings.buildings)
|
|
{
|
|
if (def.type == type) { return &def; }
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
BuildingId placeOperational(Simulation& sim, const GameConfig& cfg,
|
|
BuildingType type, QPoint anchor)
|
|
{
|
|
const BuildingDef* def = findDef(cfg, type);
|
|
REQUIRE(def != nullptr);
|
|
return SimulationTestAccess::buildings(sim).placeImmediate(SimulationTestAccess::state(sim), type, def->surfaceMask, anchor, Rotation::East);
|
|
}
|
|
|
|
const ShipDef* findAvailableSchematic(const GameConfig& cfg)
|
|
{
|
|
for (const ShipDef& def : cfg.ships.ships)
|
|
{
|
|
// A ship starts unlocked iff no unlock group grants it (REQ-LOCK-EXPLICIT).
|
|
bool granted = false;
|
|
for (const UnlockGroupDef& group : cfg.unlocks.groups)
|
|
{
|
|
if (std::find(group.ships.begin(), group.ships.end(), def.id) != group.ships.end())
|
|
{
|
|
granted = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!granted) { return &def; }
|
|
}
|
|
return nullptr;
|
|
}
|
|
} // namespace
|
|
|
|
TEST_CASE("readBuildingConfig returns a miner's selected recipe", "[blueprint]")
|
|
{
|
|
const GameConfig cfg = loadTestConfig();
|
|
Simulation sim(loadTestConfig(), 7);
|
|
|
|
const BuildingId id = placeOperational(sim, cfg, BuildingType::Miner, QPoint(0, 0));
|
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore");
|
|
|
|
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
|
|
REQUIRE(config.has_value());
|
|
CHECK(config->type == BuildingType::Miner);
|
|
REQUIRE(config->recipeId.has_value());
|
|
CHECK(*config->recipeId == "mine_iron_ore");
|
|
CHECK_FALSE(config->isSplitter);
|
|
CHECK_FALSE(config->shipLayout.has_value());
|
|
}
|
|
|
|
TEST_CASE("readBuildingConfig leaves recipe unset when nothing is selected",
|
|
"[blueprint]")
|
|
{
|
|
const GameConfig cfg = loadTestConfig();
|
|
Simulation sim(loadTestConfig(), 7);
|
|
|
|
const BuildingId id = placeOperational(sim, cfg, BuildingType::Assembler, QPoint(0, 0));
|
|
|
|
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
|
|
REQUIRE(config.has_value());
|
|
CHECK(config->type == BuildingType::Assembler);
|
|
CHECK_FALSE(config->recipeId.has_value()); // nothing to copy
|
|
CHECK_FALSE(config->isSplitter);
|
|
}
|
|
|
|
TEST_CASE("readBuildingConfig returns a shipyard's schematic and layout",
|
|
"[blueprint]")
|
|
{
|
|
const GameConfig cfg = loadTestConfig();
|
|
Simulation sim(loadTestConfig(), 7);
|
|
|
|
const ShipDef* schematic = findAvailableSchematic(cfg);
|
|
REQUIRE(schematic != nullptr);
|
|
|
|
const BuildingId id = placeOperational(sim, cfg, BuildingType::Shipyard, QPoint(0, 0));
|
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, schematic->id);
|
|
|
|
// A real layout, not an empty one: applying an empty layout to a shipyard that has
|
|
// none is a no-op (REQ-MAT-INPUT-BUFFER), so it would leave nothing to read back.
|
|
ShipLayoutConfig layout;
|
|
PlacedModule placed;
|
|
placed.moduleId = "armor_plate";
|
|
placed.position = QPoint(0, 0);
|
|
placed.rotation = Rotation::East;
|
|
layout.placedModules.push_back(placed);
|
|
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), id, layout);
|
|
|
|
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
|
|
REQUIRE(config.has_value());
|
|
CHECK(config->type == BuildingType::Shipyard);
|
|
REQUIRE(config->recipeId.has_value());
|
|
CHECK(*config->recipeId == schematic->id);
|
|
REQUIRE(config->shipLayout.has_value());
|
|
REQUIRE(config->shipLayout->placedModules.size() == 1);
|
|
CHECK(config->shipLayout->placedModules[0].moduleId == "armor_plate");
|
|
}
|
|
|
|
TEST_CASE("readBuildingConfig reads a queued construction site", "[blueprint]")
|
|
{
|
|
const GameConfig cfg = loadTestConfig();
|
|
Simulation sim(loadTestConfig(), 7);
|
|
|
|
// A placed miner enters the construction queue as a site (not yet operational).
|
|
const BuildingId id =
|
|
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
|
REQUIRE(id != kInvalidBuildingId);
|
|
REQUIRE(findBuilding(sim.getFactoryState(), id) == nullptr);
|
|
REQUIRE(findSite(sim.getFactoryState(), id) != nullptr);
|
|
|
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore");
|
|
|
|
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
|
|
REQUIRE(config.has_value());
|
|
CHECK(config->type == BuildingType::Miner);
|
|
REQUIRE(config->recipeId.has_value());
|
|
CHECK(*config->recipeId == "mine_iron_ore");
|
|
}
|
|
|
|
TEST_CASE("readBuildingConfig returns nullopt for an unknown id", "[blueprint]")
|
|
{
|
|
Simulation sim(loadTestConfig(), 7);
|
|
CHECK_FALSE(readBuildingConfig(sim, kInvalidBuildingId).has_value());
|
|
}
|
|
|
|
// The derived facts a blueprint card shows (REQ-UI-BLUEPRINT-CARD). They live here
|
|
// rather than in the dialog so they can be tested: src/ui is off the test include path.
|
|
|
|
namespace
|
|
{
|
|
Blueprint makeBlueprint(const std::vector<BuildingType>& types)
|
|
{
|
|
Blueprint blueprint;
|
|
for (const BuildingType type : types)
|
|
{
|
|
BlueprintBuilding building;
|
|
building.type = type;
|
|
building.rotation = Rotation::East;
|
|
blueprint.buildings.push_back(building);
|
|
}
|
|
return blueprint;
|
|
}
|
|
} // namespace
|
|
|
|
TEST_CASE("computeBlueprintCost sums the constituent placement costs", "[blueprint]")
|
|
{
|
|
const GameConfig cfg = loadTestConfig();
|
|
|
|
// belt 2 + belt 2 + miner 15.
|
|
const Blueprint blueprint = makeBlueprint(
|
|
{BuildingType::Belt, BuildingType::Belt, BuildingType::Miner});
|
|
CHECK(computeBlueprintCost(blueprint, cfg.buildings) == 19);
|
|
}
|
|
|
|
TEST_CASE("computeBlueprintCost ignores types absent from buildings.toml", "[blueprint]")
|
|
{
|
|
const GameConfig cfg = loadTestConfig();
|
|
REQUIRE(cfg.buildings.findBuildingDef(BuildingType::Hq) == nullptr);
|
|
|
|
const Blueprint blueprint = makeBlueprint({BuildingType::Belt, BuildingType::Hq});
|
|
CHECK(computeBlueprintCost(blueprint, cfg.buildings) == 2);
|
|
}
|
|
|
|
TEST_CASE("computeBlueprintCost is zero for an empty blueprint", "[blueprint]")
|
|
{
|
|
const GameConfig cfg = loadTestConfig();
|
|
CHECK(computeBlueprintCost(Blueprint{}, cfg.buildings) == 0);
|
|
}
|
|
|
|
TEST_CASE("summarizeBlueprintContents orders by descending count", "[blueprint]")
|
|
{
|
|
const GameConfig cfg = loadTestConfig();
|
|
|
|
const Blueprint blueprint = makeBlueprint(
|
|
{BuildingType::Belt, BuildingType::Smelter, BuildingType::Miner,
|
|
BuildingType::Miner, BuildingType::Smelter, BuildingType::Miner});
|
|
|
|
const std::vector<BlueprintContentEntry> summary =
|
|
summarizeBlueprintContents(blueprint, cfg.buildings);
|
|
REQUIRE(summary.size() == 3);
|
|
CHECK(summary[0].buildingName == "Miner");
|
|
CHECK(summary[0].count == 3);
|
|
CHECK(summary[1].buildingName == "Smelter");
|
|
CHECK(summary[1].count == 2);
|
|
CHECK(summary[2].buildingName == "Belt");
|
|
CHECK(summary[2].count == 1);
|
|
}
|
|
|
|
TEST_CASE("summarizeBlueprintContents breaks ties in buildings.toml order", "[blueprint]")
|
|
{
|
|
const GameConfig cfg = loadTestConfig();
|
|
|
|
// Miner is declared after Belt in buildings.toml, which is the order the build
|
|
// button bar lays its buttons out, so Belt wins the tie despite the equal counts
|
|
// and despite sorting later by enum value.
|
|
const Blueprint blueprint = makeBlueprint(
|
|
{BuildingType::Miner, BuildingType::Belt, BuildingType::Miner, BuildingType::Belt});
|
|
|
|
const std::vector<BlueprintContentEntry> summary =
|
|
summarizeBlueprintContents(blueprint, cfg.buildings);
|
|
REQUIRE(summary.size() == 2);
|
|
CHECK(summary[0].buildingName == "Belt");
|
|
CHECK(summary[1].buildingName == "Miner");
|
|
}
|
|
|
|
TEST_CASE("summarizeBlueprintContents spells multi-word ids as display names", "[blueprint]")
|
|
{
|
|
const GameConfig cfg = loadTestConfig();
|
|
|
|
const Blueprint blueprint = makeBlueprint({BuildingType::TunnelEntry});
|
|
const std::vector<BlueprintContentEntry> summary =
|
|
summarizeBlueprintContents(blueprint, cfg.buildings);
|
|
REQUIRE(summary.size() == 1);
|
|
CHECK(summary[0].buildingName == "Tunnel Entry");
|
|
}
|
|
|
|
TEST_CASE("summarizeBlueprintContents is empty for an empty blueprint", "[blueprint]")
|
|
{
|
|
const GameConfig cfg = loadTestConfig();
|
|
CHECK(summarizeBlueprintContents(Blueprint{}, cfg.buildings).empty());
|
|
}
|