#include "game/sim/observed.h" #include #include "check.h" using namespace sots::sim; static ObservedDesign rec(std::int32_t first, std::int32_t last, std::int32_t design, std::int32_t owner) { ObservedDesign o; o.turnFirst = first; o.turnLast = last; o.designId = design; o.ownerId = owner; return o; } static std::vector ids(const std::vector& v) { std::vector out; for (const auto& o : v) out.push_back(o.designId); return out; } static void test_create() { std::vector l; const auto r = RecordObservedDesign(l, /*design*/ 18, /*owner*/ 32, /*turn*/ 3); CHECK(r.created); CHECK(!r.moved); CHECK_EQ(r.evicted, 0); CHECK_EQ(l.size(), std::size_t{1}); CHECK_EQ(l[0].turnFirst, 3); CHECK_EQ(l[0].turnLast, 3); CHECK_EQ(l[0].designId, 18); CHECK_EQ(l[0].ownerId, 32); } // The whole point of the record: a second sighting stamps `turnLast` and leaves `turnFirst` // where it was. If this ever regresses, every leaf in the list moves. static void test_refresh_keeps_first_seen() { std::vector l{rec(2, 2, 18, 32)}; const auto r = RecordObservedDesign(l, 18, 32, 3); CHECK(!r.created); CHECK(r.moved); CHECK_EQ(l.size(), std::size_t{1}); CHECK_EQ(l[0].turnFirst, 2); CHECK_EQ(l[0].turnLast, 3); } // Re-observation is erase + push_back, NOT an in-place update, so the list ends up in // last-observation order. A model that updated in place would leave A B C here. static void test_reobservation_moves_to_the_back() { std::vector l{rec(1, 1, 10, 32), rec(1, 1, 20, 32), rec(1, 1, 30, 32)}; RecordObservedDesign(l, 10, 32, 5); CHECK(ids(l) == (std::vector{20, 30, 10})); RecordObservedDesign(l, 20, 32, 5); CHECK(ids(l) == (std::vector{30, 10, 20})); // and the moved records kept their first-seen turn CHECK_EQ(l[1].turnFirst, 1); CHECK_EQ(l[2].turnFirst, 1); } // The lookup key is the design id ALONE. Two owners cannot normally share one, but the // original compares a single word and this pins that reading. static void test_key_is_the_design_id_alone() { std::vector l{rec(1, 1, 10, 32)}; const auto r = RecordObservedDesign(l, 10, /*a different owner*/ 16, 5); CHECK(r.moved); CHECK_EQ(l.size(), std::size_t{1}); CHECK_EQ(l[0].ownerId, 32); // the carried record keeps its own owner } // Twenty per owner, counted from the most recent end, so the eviction comes off the FRONT. static void test_cap_is_twenty_per_owner() { std::vector l; for (int i = 0; i < kObservedDesignsPerOwner; ++i) RecordObservedDesign(l, 100 + i, 32, 1); CHECK_EQ(l.size(), std::size_t{kObservedDesignsPerOwner}); const auto r = RecordObservedDesign(l, 999, 32, 2); CHECK(r.created); CHECK_EQ(r.evicted, 1); CHECK_EQ(r.evictedIds.size(), std::size_t{1}); CHECK_EQ(r.evictedIds[0], 100); // the oldest went CHECK_EQ(l.size(), std::size_t{kObservedDesignsPerOwner}); CHECK_EQ(l.front().designId, 101); CHECK_EQ(l.back().designId, 999); } // The bucket is per design OWNER: a full list for owner 32 does not evict owner 16's record, // and owner 16's record is stepped over rather than counted. static void test_cap_buckets_by_owner() { std::vector l{rec(1, 1, 7, 16)}; for (int i = 0; i < kObservedDesignsPerOwner; ++i) RecordObservedDesign(l, 100 + i, 32, 1); CHECK_EQ(l.size(), std::size_t{kObservedDesignsPerOwner + 1}); CHECK_EQ(l.front().designId, 7); RecordObservedDesign(l, 999, 32, 2); CHECK_EQ(l.size(), std::size_t{kObservedDesignsPerOwner + 1}); CHECK_EQ(l.front().designId, 7); // still there CHECK_EQ(l[1].designId, 101); // 100 was the one that went // and owner 16 can still reach twenty of its own for (int i = 0; i < kObservedDesignsPerOwner - 1; ++i) RecordObservedDesign(l, 200 + i, 16, 3); int sixteens = 0; for (const auto& o : l) if (o.ownerId == 16) ++sixteens; CHECK_EQ(sixteens, kObservedDesignsPerOwner); } // The cap runs after EVERY call, not once at the end. Twenty-one designs seen in turn, each // once, leaves the LAST twenty -- and if the evicted one is then seen again it comes back as // a NEW record with this turn as its first-seen. That reset is what the reference save shows // on an empire with twenty-eight designs in service. static void test_thrash_resets_first_seen() { std::vector l; for (int i = 0; i <= kObservedDesignsPerOwner; ++i) RecordObservedDesign(l, 100 + i, 32, /*turn*/ 5); CHECK_EQ(l.size(), std::size_t{kObservedDesignsPerOwner}); CHECK_EQ(l.front().designId, 101); const auto again = RecordObservedDesign(l, 100, 32, 6); CHECK(again.created); // it was evicted, so this is a CREATE CHECK_EQ(l.back().turnFirst, 6); // ... and the first-seen turn is reset CHECK_EQ(again.evicted, 1); CHECK_EQ(again.evictedIds[0], 101); } static void test_slot_limit() { CHECK(ObserverSlotVisited(0)); CHECK(ObserverSlotVisited(14)); CHECK(!ObserverSlotVisited(15)); CHECK(!ObserverSlotVisited(-1)); } // The guard is on the DESIGN'S OWNER: an NPC's designs are invisible to everyone, unless the // NPC is a rebel AI. static void test_owner_guard() { CHECK(DesignOwnerIsObservable(/*npc*/ false, /*rebel*/ false)); CHECK(DesignOwnerIsObservable(false, true)); CHECK(!DesignOwnerIsObservable(true, false)); CHECK(DesignOwnerIsObservable(true, true)); } int main() { test_create(); test_refresh_keeps_first_seen(); test_reobservation_moves_to_the_back(); test_key_is_the_design_id_alone(); test_cap_is_twenty_per_owner(); test_cap_buckets_by_owner(); test_thrash_resets_first_seen(); test_slot_limit(); test_owner_guard(); return simtest::finish("observed"); }