The design record's section array is [mission, command, engine]; the wire is [command, mission, engine]. Hull size is an assignment inside the original's per-section loop, and that loop runs over the array, so the last resolved section in MEMORY order wins -- engine, else command, else mission. The first cut of this walked the wire order, which gives a different answer for a design with an empty engine slot and both other slots filled. Rule A3 makes that shape invalid, so no design in the corpus can tell the two apart and the 480/480 census result is unchanged either way -- which is exactly why the order is now a named constant with the reasoning attached instead of whichever loop was already to hand. derive_stats takes hull_size after its loop for the same reason; the defence-platform flag is an OR and stays in the loop. 52 design unit tests (the mixed-class case now pins memory order and adds an engine-slot case), census still 480/480 on 11 saves, realdata still 127/127 and 197/197. host ctest 43/43, clean-room OK.
160 lines
5.6 KiB
C++
160 lines
5.6 KiB
C++
// Hull size, the defence-platform class flag, and the six-counter census.
|
|
#include "game/design/hull.h"
|
|
|
|
#include "game/design/stats.h"
|
|
#include "mini_catalog.h"
|
|
#include "test_main.h"
|
|
|
|
using namespace game::data;
|
|
using namespace game::design;
|
|
|
|
namespace {
|
|
|
|
Design platform() {
|
|
Design d;
|
|
d.name = "Light Defense Platform";
|
|
d.race = Species::Human;
|
|
d.mission() = use("DEDefencePlatform", {"bal_gauss", "bal_gauss", "bal_gauss", "bal_gauss", "mis"});
|
|
return d;
|
|
}
|
|
|
|
Design cruiser() {
|
|
Design d;
|
|
d.name = "Cruiser";
|
|
d.race = Species::Human;
|
|
d.command() = use("CRCommand", {});
|
|
d.mission() = use("CRArmor", {});
|
|
d.engine() = use("CRFission", {});
|
|
return d;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST(hull_size_from_section_class) {
|
|
CHECK_EQ(hull_size(SectionClass::Destroyer), 0);
|
|
CHECK_EQ(hull_size(SectionClass::Cruiser), 1);
|
|
CHECK_EQ(hull_size(SectionClass::Dreadnought), 2);
|
|
// An absent or unrecognised class is a destroyer, not an error: the
|
|
// original's name lookup fails and leaves the field at zero.
|
|
CHECK_EQ(hull_size(SectionClass::None), 0);
|
|
CHECK_EQ(hull_size(SectionClass::Other), 0);
|
|
}
|
|
|
|
TEST(hull_class_of_a_stock_destroyer) {
|
|
HullClass h = classify_design(mini_catalog(), armor());
|
|
CHECK(h.ok());
|
|
CHECK_EQ(h.hull_size, 0);
|
|
CHECK(!h.defence_platform);
|
|
CHECK_EQ(h.resolved_sections, 3);
|
|
CHECK_EQ(h.unresolved_sections, 0);
|
|
}
|
|
|
|
TEST(hull_class_of_a_cruiser) {
|
|
HullClass h = classify_design(mini_catalog(), cruiser());
|
|
CHECK(h.ok());
|
|
CHECK_EQ(h.hull_size, 1);
|
|
CHECK(!h.defence_platform);
|
|
}
|
|
|
|
TEST(defence_platform_flag_is_read_from_the_section) {
|
|
const ShipSectionDef* s = mini_catalog().section("Human", "DEDefencePlatform");
|
|
CHECK(s != nullptr);
|
|
if (s) CHECK(s->defence_platform.value_or(false));
|
|
const ShipSectionDef* armorsec = mini_catalog().section("Human", "DEArmor");
|
|
CHECK(armorsec != nullptr);
|
|
if (armorsec) CHECK(!armorsec->defence_platform.value_or(false));
|
|
|
|
HullClass h = classify_design(mini_catalog(), platform());
|
|
CHECK(h.ok());
|
|
CHECK(h.defence_platform);
|
|
CHECK_EQ(h.hull_size, 0);
|
|
CHECK_EQ(h.resolved_sections, 1);
|
|
}
|
|
|
|
TEST(one_flagged_section_makes_the_whole_design_a_platform) {
|
|
// The flag word is OR-ed across sections, so a design that mixes a
|
|
// flagged section with unflagged ones is still a platform. No shipped
|
|
// design does this -- the platforms are all standalone mission sections --
|
|
// so this is the rule, tested where the data cannot show it.
|
|
Design d = armor();
|
|
d.mission() = use("DEDefencePlatform", {"bal_gauss", "bal_gauss", "bal_gauss", "bal_gauss", "mis"});
|
|
HullClass h = classify_design(mini_catalog(), d);
|
|
CHECK(h.defence_platform);
|
|
CHECK_EQ(h.resolved_sections, 3);
|
|
}
|
|
|
|
TEST(hull_size_takes_the_last_resolved_section_in_memory_order) {
|
|
// Assignment, not first-wins and not max, and the order is the record's
|
|
// in-memory array order -- mission, command, engine -- not its wire order.
|
|
// Class-mixing is a rule A6 error and an engine-less two-slot design breaks
|
|
// rule A3, so this can only be shown on designs the validator rejects,
|
|
// which is exactly why it is worth pinning.
|
|
CHECK(kAggregationOrder[0] == Slot::Mission);
|
|
CHECK(kAggregationOrder[1] == Slot::Command);
|
|
CHECK(kAggregationOrder[2] == Slot::Engine);
|
|
|
|
Design d;
|
|
d.race = Species::Human;
|
|
d.command() = use("DECommand", {"bal_gauss"});
|
|
d.mission() = use("CRArmor", {});
|
|
HullClass h = classify_design(mini_catalog(), d);
|
|
CHECK_EQ(h.resolved_sections, 2);
|
|
CHECK_EQ(h.hull_size, 0); // the destroyer COMMAND section, visited last
|
|
|
|
Design r;
|
|
r.race = Species::Human;
|
|
r.command() = use("CRCommand", {});
|
|
r.mission() = use("DEArmor", {"bal_gauss", "bal_gauss", "mis"});
|
|
CHECK_EQ(classify_design(mini_catalog(), r).hull_size, 1); // the cruiser command section
|
|
|
|
// With the engine slot filled, the engine section wins whatever the others say.
|
|
Design e;
|
|
e.race = Species::Human;
|
|
e.command() = use("DECommand", {"bal_gauss"});
|
|
e.mission() = use("DEArmor", {"bal_gauss", "bal_gauss", "mis"});
|
|
e.engine() = use("CRFission", {});
|
|
CHECK_EQ(classify_design(mini_catalog(), e).hull_size, 1);
|
|
}
|
|
|
|
TEST(an_unresolvable_design_is_reported_not_counted_as_a_destroyer) {
|
|
Design d;
|
|
d.race = Species::Human;
|
|
d.mission() = use("NoSuchSection", {});
|
|
HullClass h = classify_design(mini_catalog(), d);
|
|
CHECK(!h.ok());
|
|
CHECK_EQ(h.resolved_sections, 0);
|
|
CHECK_EQ(h.unresolved_sections, 1);
|
|
}
|
|
|
|
TEST(derive_stats_carries_the_two_census_words) {
|
|
DesignStats st = derive_stats(mini_rules(), armor());
|
|
CHECK_EQ(st.hull_size, 0);
|
|
CHECK(!st.defence_platform);
|
|
|
|
DesignStats p = derive_stats(mini_rules(), platform());
|
|
CHECK_EQ(p.hull_size, 0);
|
|
CHECK(p.defence_platform);
|
|
|
|
DesignStats c = derive_stats(mini_rules(), cruiser());
|
|
CHECK_EQ(c.hull_size, 1);
|
|
CHECK_EQ(c.hull_class, "cruiser");
|
|
}
|
|
|
|
TEST(census_counts_ships_and_platforms_separately) {
|
|
ShipCensus c;
|
|
HullClass de; // destroyer ship
|
|
HullClass cr; cr.hull_size = 1; cr.resolved_sections = 1;
|
|
HullClass dn; dn.hull_size = 2; dn.resolved_sections = 1;
|
|
HullClass pl; pl.defence_platform = true; pl.resolved_sections = 1;
|
|
de.resolved_sections = 1;
|
|
|
|
c.add(de); c.add(de); c.add(cr); c.add(dn); c.add(pl); c.add(pl); c.add(pl);
|
|
CHECK_EQ(c.ships[0], 2);
|
|
CHECK_EQ(c.ships[1], 1);
|
|
CHECK_EQ(c.ships[2], 1);
|
|
CHECK_EQ(c.platforms[0], 3);
|
|
CHECK_EQ(c.platforms[1], 0);
|
|
CHECK_EQ(c.platforms[2], 0);
|
|
CHECK_EQ(c.ship_total(), 4);
|
|
CHECK_EQ(c.platform_total(), 3);
|
|
}
|