// The trade-raid block is the strategic turn's dominant generator cost, so its WORD COUNT is // load-bearing in a way its decisions are not. These tests pin the count, the two no-draw // early-outs of the chance helper, and the roll order -- all against a scripted generator, so // nothing here depends on the real MT19937. #include #include #include "app/trade_raid.h" #include "mars/rng/mt19937.h" static int failures = 0; #define CHECK(c) \ do { \ if (!(c)) { \ std::printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #c); \ ++failures; \ } \ } while (0) namespace { // A generator that hands out a scripted sequence and counts what it handed out. class Scripted final : public sots::sim::IRandom { public: explicit Scripted(std::vector f) : f_(std::move(f)) {} float NextFloat() override { ++draws; return f_.empty() ? 0.f : f_[(cursor_++) % f_.size()]; } std::uint32_t NextIntInclusive(std::uint32_t) override { ++draws; return 0; } std::uint32_t NextUInt32() override { ++draws; return 0; } int draws = 0; private: std::vector f_; std::size_t cursor_ = 0; }; } // namespace int main() { using namespace sots::app; // --- the chance helper's two no-draw early-outs ------------------------------------ { Scripted g({0.5f}); CHECK(Chance(g, 0.f) == false); CHECK(Chance(g, -1.f) == false); CHECK(Chance(g, 1.f) == true); CHECK(Chance(g, 2.f) == true); CHECK(g.draws == 0); // four calls, zero words: both early-outs return before drawing } // --- the comparison is `p > r`, so equality FAILS and still costs a word ----------- { Scripted g({0.25f, 0.25f}); CHECK(Chance(g, 0.25f) == false); // p == r -> false CHECK(g.draws == 1); CHECK(Chance(g, 0.5f) == true); // p > r -> true CHECK(g.draws == 2); } // --- the word count is two per player when the NPC gate is open ------------------- { Scripted g({0.9f}); // every roll fails; the count must not depend on that const TradeRaidResult r = RollTradeRaids(g, 8, TradeRaidOdds{}, TradeRaidGates{}); CHECK(r.players == 8); CHECK(r.words == 16); CHECK(g.draws == 16); CHECK(r.playerRaidHits == 0); CHECK(r.npcRaidHits == 0); CHECK(r.refugeeRaidHits == 0); } // --- ... and the count is unchanged when every roll succeeds ----------------------- { Scripted g({0.0f}); // every roll succeeds (p > 0) const TradeRaidResult r = RollTradeRaids(g, 8, TradeRaidOdds{}, TradeRaidGates{}); CHECK(r.words == 16); CHECK(r.playerRaidHits == 8); CHECK(r.npcRaidHits == 8); } // --- one per player when the NPC gate is shut, three when the refugee one is open -- { Scripted g({0.9f}); TradeRaidGates gates; gates.npcRaids = false; CHECK(RollTradeRaids(g, 7, TradeRaidOdds{}, gates).words == 7); gates.npcRaids = true; gates.refugeeRaids = true; Scripted g2({0.9f}); CHECK(RollTradeRaids(g2, 7, TradeRaidOdds{}, gates).words == 21); } // --- a degenerate player vector costs nothing -------------------------------------- { Scripted g({0.9f}); CHECK(RollTradeRaids(g, 0, TradeRaidOdds{}, TradeRaidGates{}).words == 0); CHECK(RollTradeRaids(g, -3, TradeRaidOdds{}, TradeRaidGates{}).words == 0); CHECK(g.draws == 0); } // --- odds pinned at 0 or 1 cost NOTHING, and the counter says so ------------------- // This is the case that would silently break a count-the-calls implementation: a tuning // table that pushes an odds value out of (0, 1) removes the draw, not just the outcome. { Scripted g({0.9f}); TradeRaidOdds odds; odds.player = 0.f; odds.npc = 1.f; const TradeRaidResult r = RollTradeRaids(g, 8, odds, TradeRaidGates{}); CHECK(r.words == 0); CHECK(g.draws == 0); CHECK(r.playerRaidHits == 0); CHECK(r.npcRaidHits == 8); } // --- against the real generator: 16 words is 16 words ----------------------------- { mars::rng::MT19937 gen(12345u); class Adapt final : public sots::sim::IRandom { public: explicit Adapt(mars::rng::MT19937& g) : g_(g) {} float NextFloat() override { return g_.next_float(); } std::uint32_t NextIntInclusive(std::uint32_t n) override { return g_.next_int_inclusive(n); } std::uint32_t NextUInt32() override { return g_.next_u32(); } private: mars::rng::MT19937& g_; } a(gen); const int before = gen.index(); const TradeRaidResult r = RollTradeRaids(a, 8, TradeRaidOdds{}, TradeRaidGates{}); CHECK(r.words == 16); CHECK(gen.index() - before == 16); } std::printf(failures ? "trade_raid: FAILED (%d)\n" : "trade_raid: ok (%d failures)\n", failures); return failures ? 1 : 0; }