add blueprint tests
This commit is contained in:
406
src/test/BlueprintTest.cpp
Normal file
406
src/test/BlueprintTest.cpp
Normal file
@@ -0,0 +1,406 @@
|
||||
#include "catch.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <vector>
|
||||
|
||||
#include <QPoint>
|
||||
|
||||
#include "Blueprint.h"
|
||||
#include "BuildingSystem.h"
|
||||
#include "BuildingType.h"
|
||||
#include "ConfigLoader.h"
|
||||
#include "EntityId.h"
|
||||
#include "Rotation.h"
|
||||
#include "Simulation.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers that mirror the production implementations under test.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Mirror of the CW / CCW offset transforms in GameWorldView::keyPressEvent.
|
||||
static QPoint rotateCW(QPoint p) { return QPoint(-p.y(), p.x()); }
|
||||
static QPoint rotateCCW(QPoint p) { return QPoint( p.y(), -p.x()); }
|
||||
|
||||
// Mirror of the Rotation cycling in GameWorldView (anonymous-namespace helpers).
|
||||
static Rotation rotCW(Rotation r)
|
||||
{
|
||||
switch (r)
|
||||
{
|
||||
case Rotation::North: return Rotation::East;
|
||||
case Rotation::East: return Rotation::South;
|
||||
case Rotation::South: return Rotation::West;
|
||||
case Rotation::West: return Rotation::North;
|
||||
}
|
||||
return Rotation::East;
|
||||
}
|
||||
|
||||
static Rotation rotCCW(Rotation r)
|
||||
{
|
||||
switch (r)
|
||||
{
|
||||
case Rotation::North: return Rotation::West;
|
||||
case Rotation::East: return Rotation::North;
|
||||
case Rotation::South: return Rotation::East;
|
||||
case Rotation::West: return Rotation::South;
|
||||
}
|
||||
return Rotation::East;
|
||||
}
|
||||
|
||||
// Mirror of BlueprintPanel::createBlueprintFromSelection: given per-building
|
||||
// (anchor, bodyCells, type, rotation), compute Blueprint with floor-division
|
||||
// bounding-box center and per-building tile offsets.
|
||||
struct BuildingSpec
|
||||
{
|
||||
QPoint anchor;
|
||||
std::vector<QPoint> bodyCells;
|
||||
BuildingType type;
|
||||
Rotation rotation;
|
||||
};
|
||||
|
||||
static Blueprint buildBlueprint(const std::vector<BuildingSpec>& specs)
|
||||
{
|
||||
int minX = INT_MAX, maxX = INT_MIN;
|
||||
int minY = INT_MAX, maxY = INT_MIN;
|
||||
for (const BuildingSpec& s : specs)
|
||||
{
|
||||
for (const QPoint& cell : s.bodyCells)
|
||||
{
|
||||
minX = std::min(minX, cell.x());
|
||||
maxX = std::max(maxX, cell.x());
|
||||
minY = std::min(minY, cell.y());
|
||||
maxY = std::max(maxY, cell.y());
|
||||
}
|
||||
}
|
||||
const QPoint center((minX + maxX) / 2, (minY + maxY) / 2);
|
||||
|
||||
Blueprint bp;
|
||||
for (const BuildingSpec& s : specs)
|
||||
{
|
||||
BlueprintBuilding bb;
|
||||
bb.type = s.type;
|
||||
bb.rotation = s.rotation;
|
||||
bb.offset = s.anchor - center;
|
||||
bp.buildings.push_back(bb);
|
||||
}
|
||||
return bp;
|
||||
}
|
||||
|
||||
// Apply one CW/CCW rotation to every building in the constellation, mirroring
|
||||
// the Q / E handling in GameWorldView::keyPressEvent.
|
||||
static void applyRotationCW(Blueprint& bp)
|
||||
{
|
||||
for (BlueprintBuilding& bb : bp.buildings)
|
||||
{
|
||||
bb.offset = rotateCW(bb.offset);
|
||||
bb.rotation = rotCW(bb.rotation);
|
||||
}
|
||||
}
|
||||
|
||||
static void applyRotationCCW(Blueprint& bp)
|
||||
{
|
||||
for (BlueprintBuilding& bb : bp.buildings)
|
||||
{
|
||||
bb.offset = rotateCCW(bb.offset);
|
||||
bb.rotation = rotCCW(bb.rotation);
|
||||
}
|
||||
}
|
||||
|
||||
static GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(DOTA_FACTORY_CONFIG_DIR);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Offset computation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("Blueprint: single 1x1 building gets zero offset", "[blueprint]")
|
||||
{
|
||||
// Body = one tile at (-5, 3). Center = (-5, 3). Offset = (0, 0).
|
||||
const BuildingSpec spec{ QPoint(-5, 3), {QPoint(-5, 3)}, BuildingType::Belt, Rotation::East };
|
||||
const Blueprint bp = buildBlueprint({ spec });
|
||||
|
||||
REQUIRE(bp.buildings.size() == 1);
|
||||
REQUIRE(bp.buildings[0].offset == QPoint(0, 0));
|
||||
}
|
||||
|
||||
TEST_CASE("Blueprint: two 1x1 buildings with odd span get symmetric offsets", "[blueprint]")
|
||||
{
|
||||
// Anchors at (-6, 0) and (-4, 0). bboxX = [-6, -4], center.x = -5.
|
||||
// Offsets: -6 - (-5) = -1, -4 - (-5) = +1.
|
||||
const BuildingSpec left { QPoint(-6, 0), {QPoint(-6, 0)}, BuildingType::Belt, Rotation::East };
|
||||
const BuildingSpec right{ QPoint(-4, 0), {QPoint(-4, 0)}, BuildingType::Belt, Rotation::East };
|
||||
const Blueprint bp = buildBlueprint({ left, right });
|
||||
|
||||
REQUIRE(bp.buildings[0].offset == QPoint(-1, 0));
|
||||
REQUIRE(bp.buildings[1].offset == QPoint( 1, 0));
|
||||
}
|
||||
|
||||
TEST_CASE("Blueprint: even span uses C++ integer truncation for center", "[blueprint]")
|
||||
{
|
||||
// Anchors at (-5, 0) and (-4, 0). bboxX = [-5, -4], sum = -9.
|
||||
// center.x = -9 / 2 = -4 (C++ truncates toward zero, i.e. rounds up for negatives).
|
||||
// Offsets: -5 - (-4) = -1, -4 - (-4) = 0.
|
||||
const BuildingSpec left { QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Belt, Rotation::East };
|
||||
const BuildingSpec right{ QPoint(-4, 0), {QPoint(-4, 0)}, BuildingType::Belt, Rotation::East };
|
||||
const Blueprint bp = buildBlueprint({ left, right });
|
||||
|
||||
REQUIRE(bp.buildings[0].offset == QPoint(-1, 0));
|
||||
REQUIRE(bp.buildings[1].offset == QPoint( 0, 0));
|
||||
}
|
||||
|
||||
TEST_CASE("Blueprint: bounding box is computed from body cells, not anchors alone", "[blueprint]")
|
||||
{
|
||||
// A 2x1 building: anchor (-6, 0), body cells (-6, 0) and (-5, 0).
|
||||
// A 1x1 building: anchor (-3, 0), body cell (-3, 0).
|
||||
// All body cells: {(-6,0),(-5,0),(-3,0)}. bboxX = [-6, -3], sum = -9.
|
||||
// center.x = -9 / 2 = -4 (C++ truncation).
|
||||
// Wide building anchor offset: -6 - (-4) = -2.
|
||||
// Small building anchor offset: -3 - (-4) = +1.
|
||||
const BuildingSpec wide{
|
||||
QPoint(-6, 0),
|
||||
{ QPoint(-6, 0), QPoint(-5, 0) },
|
||||
BuildingType::Belt,
|
||||
Rotation::East
|
||||
};
|
||||
const BuildingSpec small{
|
||||
QPoint(-3, 0),
|
||||
{ QPoint(-3, 0) },
|
||||
BuildingType::Belt,
|
||||
Rotation::East
|
||||
};
|
||||
const Blueprint bp = buildBlueprint({ wide, small });
|
||||
|
||||
REQUIRE(bp.buildings[0].offset == QPoint(-2, 0));
|
||||
REQUIRE(bp.buildings[1].offset == QPoint( 1, 0));
|
||||
}
|
||||
|
||||
TEST_CASE("Blueprint: 2-D bounding box with buildings on both axes", "[blueprint]")
|
||||
{
|
||||
// Four 1x1 buildings at corners of a 4x2 rectangle (-7,0),(-5,0),(-7,2),(-5,2).
|
||||
// bboxX = [-7, -5], center.x = -6. bboxY = [0, 2], center.y = 1.
|
||||
// Offsets: (-1,-1), (1,-1), (-1,1), (1,1).
|
||||
const auto belt = BuildingType::Belt;
|
||||
const auto east = Rotation::East;
|
||||
std::vector<BuildingSpec> specs = {
|
||||
{ QPoint(-7, 0), {QPoint(-7, 0)}, belt, east },
|
||||
{ QPoint(-5, 0), {QPoint(-5, 0)}, belt, east },
|
||||
{ QPoint(-7, 2), {QPoint(-7, 2)}, belt, east },
|
||||
{ QPoint(-5, 2), {QPoint(-5, 2)}, belt, east },
|
||||
};
|
||||
const Blueprint bp = buildBlueprint(specs);
|
||||
|
||||
REQUIRE(bp.buildings[0].offset == QPoint(-1, -1));
|
||||
REQUIRE(bp.buildings[1].offset == QPoint( 1, -1));
|
||||
REQUIRE(bp.buildings[2].offset == QPoint(-1, 1));
|
||||
REQUIRE(bp.buildings[3].offset == QPoint( 1, 1));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Offset rotation math
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("Blueprint: CW rotation in screen space maps right→down, down→left", "[blueprint]")
|
||||
{
|
||||
// Screen space has Y growing downward. CW means: right→down, down→left, left→up, up→right.
|
||||
REQUIRE(rotateCW(QPoint( 1, 0)) == QPoint( 0, 1));
|
||||
REQUIRE(rotateCW(QPoint( 0, 1)) == QPoint(-1, 0));
|
||||
REQUIRE(rotateCW(QPoint(-1, 0)) == QPoint( 0, -1));
|
||||
REQUIRE(rotateCW(QPoint( 0, -1)) == QPoint( 1, 0));
|
||||
}
|
||||
|
||||
TEST_CASE("Blueprint: CCW rotation is the inverse of CW rotation", "[blueprint]")
|
||||
{
|
||||
REQUIRE(rotateCCW(QPoint( 1, 0)) == QPoint( 0, -1));
|
||||
REQUIRE(rotateCCW(QPoint( 0, -1)) == QPoint(-1, 0));
|
||||
REQUIRE(rotateCCW(QPoint(-1, 0)) == QPoint( 0, 1));
|
||||
REQUIRE(rotateCCW(QPoint( 0, 1)) == QPoint( 1, 0));
|
||||
}
|
||||
|
||||
TEST_CASE("Blueprint: four CW rotations restore any offset to its original", "[blueprint]")
|
||||
{
|
||||
const QPoint original(-3, 5);
|
||||
QPoint p = original;
|
||||
for (int i = 0; i < 4; ++i) { p = rotateCW(p); }
|
||||
REQUIRE(p == original);
|
||||
}
|
||||
|
||||
TEST_CASE("Blueprint: CW followed by CCW is identity", "[blueprint]")
|
||||
{
|
||||
const QPoint original(2, -7);
|
||||
REQUIRE(rotateCCW(rotateCW(original)) == original);
|
||||
REQUIRE(rotateCW(rotateCCW(original)) == original);
|
||||
}
|
||||
|
||||
TEST_CASE("Blueprint: non-axis-aligned offset rotates correctly", "[blueprint]")
|
||||
{
|
||||
// (2, 3) → CW → (-3, 2) → CW → (-2, -3) → CW → (3, -2) → CW → (2, 3)
|
||||
QPoint p(2, 3);
|
||||
p = rotateCW(p); REQUIRE(p == QPoint(-3, 2));
|
||||
p = rotateCW(p); REQUIRE(p == QPoint(-2, -3));
|
||||
p = rotateCW(p); REQUIRE(p == QPoint( 3, -2));
|
||||
p = rotateCW(p); REQUIRE(p == QPoint( 2, 3));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constellation rotation: offsets AND building rotations both update
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("Blueprint: CW constellation rotation updates offset and building rotation", "[blueprint]")
|
||||
{
|
||||
// Building one tile to the right, facing East.
|
||||
Blueprint bp;
|
||||
bp.name = "test";
|
||||
BlueprintBuilding bb;
|
||||
bb.type = BuildingType::Belt;
|
||||
bb.rotation = Rotation::East;
|
||||
bb.offset = QPoint(1, 0);
|
||||
bp.buildings.push_back(bb);
|
||||
|
||||
applyRotationCW(bp);
|
||||
|
||||
// Offset: right → down.
|
||||
REQUIRE(bp.buildings[0].offset == QPoint(0, 1));
|
||||
// Building rotation: East → South.
|
||||
REQUIRE(bp.buildings[0].rotation == Rotation::South);
|
||||
}
|
||||
|
||||
TEST_CASE("Blueprint: CCW constellation rotation updates offset and building rotation", "[blueprint]")
|
||||
{
|
||||
Blueprint bp;
|
||||
bp.name = "test";
|
||||
BlueprintBuilding bb;
|
||||
bb.type = BuildingType::Belt;
|
||||
bb.rotation = Rotation::East;
|
||||
bb.offset = QPoint(1, 0);
|
||||
bp.buildings.push_back(bb);
|
||||
|
||||
applyRotationCCW(bp);
|
||||
|
||||
// Offset: right → up.
|
||||
REQUIRE(bp.buildings[0].offset == QPoint(0, -1));
|
||||
// Building rotation: East → North.
|
||||
REQUIRE(bp.buildings[0].rotation == Rotation::North);
|
||||
}
|
||||
|
||||
TEST_CASE("Blueprint: four CW rotations restore offset and building rotation", "[blueprint]")
|
||||
{
|
||||
Blueprint bp;
|
||||
bp.name = "test";
|
||||
BlueprintBuilding bb;
|
||||
bb.type = BuildingType::Belt;
|
||||
bb.rotation = Rotation::East;
|
||||
bb.offset = QPoint(2, -1);
|
||||
bp.buildings.push_back(bb);
|
||||
|
||||
const QPoint originalOffset = bb.offset;
|
||||
const Rotation originalRotation = bb.rotation;
|
||||
|
||||
for (int i = 0; i < 4; ++i) { applyRotationCW(bp); }
|
||||
|
||||
REQUIRE(bp.buildings[0].offset == originalOffset);
|
||||
REQUIRE(bp.buildings[0].rotation == originalRotation);
|
||||
}
|
||||
|
||||
TEST_CASE("Blueprint: multi-building constellation rotates symmetrically CW", "[blueprint]")
|
||||
{
|
||||
// Two buildings left and right of center; after CW they should be above and below.
|
||||
Blueprint bp;
|
||||
bp.name = "test";
|
||||
BlueprintBuilding left, right;
|
||||
left.type = right.type = BuildingType::Belt;
|
||||
left.rotation = right.rotation = Rotation::East;
|
||||
left.offset = QPoint(-1, 0);
|
||||
right.offset = QPoint( 1, 0);
|
||||
bp.buildings = { left, right };
|
||||
|
||||
applyRotationCW(bp);
|
||||
|
||||
// left (-1, 0) → CW → (0, -1) (above center)
|
||||
// right ( 1, 0) → CW → (0, 1) (below center)
|
||||
REQUIRE(bp.buildings[0].offset == QPoint(0, -1));
|
||||
REQUIRE(bp.buildings[1].offset == QPoint(0, 1));
|
||||
REQUIRE(bp.buildings[0].rotation == Rotation::South);
|
||||
REQUIRE(bp.buildings[1].rotation == Rotation::South);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Simulation-level: blueprint placement places buildings at correct tiles
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("Blueprint placement: buildings land at anchor + offset from cursor", "[blueprint]")
|
||||
{
|
||||
// Simulate placing a two-belt blueprint with offsets (-1, 0) and (+1, 0)
|
||||
// at cursor tile (-5, 0). Expected anchors: (-6, 0) and (-4, 0).
|
||||
// (Belt surface_mask ["A>"] — body at relative (0,0), port at (1,0).)
|
||||
Simulation sim(loadConfig());
|
||||
|
||||
const QPoint cursor(-5, 0);
|
||||
const QPoint offsetA(-1, 0);
|
||||
const QPoint offsetB( 1, 0);
|
||||
|
||||
const EntityId idA = sim.tryPlaceBuilding(
|
||||
BuildingType::Belt, cursor + offsetA, Rotation::East);
|
||||
const EntityId idB = sim.tryPlaceBuilding(
|
||||
BuildingType::Belt, cursor + offsetB, Rotation::East);
|
||||
|
||||
REQUIRE(idA != kInvalidEntityId);
|
||||
REQUIRE(idB != kInvalidEntityId);
|
||||
REQUIRE(sim.buildings().isTileOccupied(cursor + offsetA)); // (-6, 0)
|
||||
REQUIRE(sim.buildings().isTileOccupied(cursor + offsetB)); // (-4, 0)
|
||||
REQUIRE_FALSE(sim.buildings().isTileOccupied(cursor)); // center not occupied
|
||||
}
|
||||
|
||||
TEST_CASE("Blueprint placement: cost is deducted for each building in sequence", "[blueprint]")
|
||||
{
|
||||
Simulation sim(loadConfig());
|
||||
|
||||
// Find belt cost from config (belt cost = 2 in test config).
|
||||
int beltCost = 0;
|
||||
for (const BuildingDef& def : sim.config().buildings.buildings)
|
||||
{
|
||||
if (def.type == BuildingType::Belt) { beltCost = def.cost; break; }
|
||||
}
|
||||
REQUIRE(beltCost > 0);
|
||||
|
||||
const int startBlocks = sim.buildingBlocksStock();
|
||||
REQUIRE(startBlocks >= 2 * beltCost); // test config has enough starting blocks
|
||||
|
||||
sim.tryPlaceBuilding(BuildingType::Belt, QPoint(-6, 0), Rotation::East);
|
||||
REQUIRE(sim.buildingBlocksStock() == startBlocks - beltCost);
|
||||
|
||||
sim.tryPlaceBuilding(BuildingType::Belt, QPoint(-4, 0), Rotation::East);
|
||||
REQUIRE(sim.buildingBlocksStock() == startBlocks - 2 * beltCost);
|
||||
}
|
||||
|
||||
TEST_CASE("Blueprint placement: insufficient blocks returns kInvalidEntityId and deducts nothing",
|
||||
"[blueprint]")
|
||||
{
|
||||
Simulation sim(loadConfig());
|
||||
|
||||
// Find miner cost (15 in test config) — expensive enough to exhaust a small stock.
|
||||
int minerCost = 0;
|
||||
for (const BuildingDef& def : sim.config().buildings.buildings)
|
||||
{
|
||||
if (def.type == BuildingType::Miner) { minerCost = def.cost; break; }
|
||||
}
|
||||
REQUIRE(minerCost > 0);
|
||||
|
||||
// Drain the stock by placing miners until we no longer have enough.
|
||||
// Non-overlapping columns: miner body is 2 wide, so step by 2.
|
||||
int col = -2;
|
||||
while (sim.buildingBlocksStock() >= minerCost)
|
||||
{
|
||||
sim.tryPlaceBuilding(BuildingType::Miner, QPoint(col, 0), Rotation::East);
|
||||
col -= 2;
|
||||
}
|
||||
|
||||
const int blocksBeforeAttempt = sim.buildingBlocksStock();
|
||||
const EntityId id = sim.tryPlaceBuilding(
|
||||
BuildingType::Miner, QPoint(col - 2, 0), Rotation::East);
|
||||
|
||||
// Placement must fail and leave the stock unchanged.
|
||||
REQUIRE(id == kInvalidEntityId);
|
||||
REQUIRE(sim.buildingBlocksStock() == blocksBeforeAttempt);
|
||||
}
|
||||
@@ -15,4 +15,5 @@ add_files(
|
||||
WaveSystemTest.cpp
|
||||
CombatSystemTest.cpp
|
||||
ShipyardTest.cpp
|
||||
BlueprintTest.cpp
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user