// The no-research event, pinned as a rule rather than as a table of observations. // // The corpus agrees with this rule wherever it can be seen: the human posts the event on // every turn it holds no research target, and does NOT post it on the turn a tech of its own // completed. That comparison lives against the owner's saves. What is pinned HERE is what a // save cannot separate: // // * the "nothing researched on this turn or later" test. Only one corpus save exercises it // (a turn on which a tech landed), and a build that dropped the test would still look // right on both reference pairs, where nothing is researched at all. // * the available-tech test reads state 2 specifically. A build that accepted "not all // researched" would agree on every corpus player except the monster factions. // * the position sentinel is FLT_MAX, not infinity, and the action is stored as 1 rather // than falling into the `act == 0` rule. // * the bucket is created at the posting turn and the id sequence starts at 1 from a fresh // `EvNxID` of 0, which the wire cannot distinguish from "never posted" after the fact. // * the wire round trip is lossless, so a phase that posts nothing writes nothing. #include #include #include #include "app/event_phase.h" static int failures = 0; #define CHECK_EQ(a, b) \ do { \ const long long va = (long long)(a), vb = (long long)(b); \ if (va != vb) { \ std::printf("FAIL %s:%d %s == %s (%lld != %lld)\n", __FILE__, \ __LINE__, #a, #b, va, vb); \ ++failures; \ } \ } while (0) #define CHECK_STR(a, b) \ do { \ const std::string va = (a), vb = (b); \ if (va != vb) { \ std::printf("FAIL %s:%d %s == %s (\"%s\" != \"%s\")\n", __FILE__, \ __LINE__, #a, #b, va.c_str(), vb.c_str()); \ ++failures; \ } \ } while (0) using mars::stream::shapes::Player; using mars::stream::shapes::TechState; using sots::app::DecideNoResearchEvent; using sots::app::LoadEventStorage; using sots::app::PostNoResearchEvent; using sots::app::StoreEventStorage; namespace { // The two keys the phase resolves, with stand-in text. No shipped prose in this repo: the // point of the lookup is that the text is the caller's, so the test supplies its own. std::string_view TestLookup(void*, std::string_view key) { if (key == "EVENTSUM_NO_RESEARCH") return "SUM"; if (key == "EVENTMSG_NO_RESEARCH") return "MSG"; return {}; } sots::events::EventText TestText() { return sots::events::EventText{&TestLookup, nullptr}; } TechState Row(const char* name, int state, int turnAcquired) { TechState s; s.tNm = name; s.st = state; s.tAcq = turnAcquired; return s; } } // namespace int main() { // --- the decision ------------------------------------------------------------------- { Player p; p.techTree.state.push_back(Row("A", 4, 1)); // researched, on an earlier turn p.techTree.state.push_back(Row("B", 2, 0)); // available const auto d = DecideNoResearchEvent(p, 2, /*isAI=*/false); CHECK_EQ(d.noTarget, true); CHECK_EQ(d.noneResearched, true); CHECK_EQ(d.anyAvailable, true); CHECK_EQ(d.fires(), true); } { // A tech that landed THIS turn suppresses the event. This is the test the reference // pairs cannot exercise. Player p; p.techTree.state.push_back(Row("A", 4, 2)); p.techTree.state.push_back(Row("B", 2, 0)); const auto d = DecideNoResearchEvent(p, 2, false); CHECK_EQ(d.noneResearched, false); CHECK_EQ(d.fires(), false); // ... and a tech researched on a LATER turn than the one being posted suppresses it // too: the original's range runs to INT_MAX, not to the turn. p.techTree.state[0].tAcq = 9; CHECK_EQ(DecideNoResearchEvent(p, 2, false).noneResearched, false); } { // The monster factions: every row researched, none available. Player p; p.techTree.state.push_back(Row("A", 4, 1)); p.techTree.state.push_back(Row("B", 4, 1)); const auto d = DecideNoResearchEvent(p, 2, false); CHECK_EQ(d.anyAvailable, false); CHECK_EQ(d.fires(), false); } { // A row in state 1 (parent researched) or 3 (selected) is not "available". Player p; p.techTree.state.push_back(Row("A", 1, 0)); p.techTree.state.push_back(Row("B", 3, 0)); CHECK_EQ(DecideNoResearchEvent(p, 2, false).anyAvailable, false); } { Player p; p.resTNm = "IND_Waldo"; p.techTree.state.push_back(Row("B", 2, 0)); CHECK_EQ(DecideNoResearchEvent(p, 2, false).noTarget, false); CHECK_EQ(DecideNoResearchEvent(p, 2, false).fires(), false); } { // The AI stand-in suppresses a player that would otherwise post. Player p; p.techTree.state.push_back(Row("B", 2, 0)); const auto d = DecideNoResearchEvent(p, 2, /*isAI=*/true); CHECK_EQ(d.noTarget, true); CHECK_EQ(d.anyAvailable, true); CHECK_EQ(d.aiWouldPick, true); CHECK_EQ(d.fires(), false); } // --- the post ----------------------------------------------------------------------- { Player p; const auto post = PostNoResearchEvent(p, 2, TestText()); CHECK_EQ(post.eventId, 1); CHECK_EQ(post.leafWrites, 4); // EvNxID, the collection count, the bucket, the record CHECK_EQ(p.events.evNxID, 2); // 0 -> 1 on the first post, then id = nextId++ CHECK_EQ(p.events.turns.size(), 1u); CHECK_EQ(p.events.turns[0].evTurn, 2); CHECK_EQ(p.events.turns[0].events.size(), 1u); const auto& r = p.events.turns[0].events[0]; CHECK_EQ(r.evEID, 1); CHECK_STR(r.evDsc, "SUM"); CHECK_STR(r.evMsg, "MSG"); CHECK_STR(r.evImg, "EVENT_NO_RESEARCH"); CHECK_EQ(r.evLoc, 0); CHECK_EQ(r.evAct, 1); // NOT 2: the `act == 0` rule must not fire here CHECK_EQ(r.evCID, 0); CHECK_EQ(r.evPos.x == FLT_MAX, true); CHECK_EQ(r.evPos.y == FLT_MAX, true); CHECK_EQ(r.evPos.z == FLT_MAX, true); // A second turn appends a bucket and continues the id sequence. const auto next = PostNoResearchEvent(p, 3, TestText()); CHECK_EQ(next.eventId, 2); CHECK_EQ(next.leafWrites, 4); CHECK_EQ(p.events.evNxID, 3); CHECK_EQ(p.events.turns.size(), 2u); CHECK_EQ(p.events.turns[1].evTurn, 3); // Posting the same event twice into the same bucket is a dedup: the id comes back // and neither the list nor EvNxID moves. const auto dup = PostNoResearchEvent(p, 3, TestText()); CHECK_EQ(dup.eventId, 2); CHECK_EQ(dup.leafWrites, 0); CHECK_EQ(p.events.evNxID, 3); CHECK_EQ(p.events.turns.size(), 2u); } // --- the wire round trip ------------------------------------------------------------ { Player p; PostNoResearchEvent(p, 2, TestText()); const auto before = p.events; const int moved = StoreEventStorage(LoadEventStorage(p.events), p.events); CHECK_EQ(moved, 0); CHECK_EQ(p.events.evNxID, before.evNxID); CHECK_EQ(p.events.turns.size(), before.turns.size()); CHECK_STR(p.events.turns[0].events[0].evImg, before.turns[0].events[0].evImg); } if (failures == 0) std::printf("app_event_phase: OK\n"); return failures ? 1 : 0; }