The trade-raid block is 16 of a measured turn's 18-22 generator words and it is the first thing the standalone can model as a COUNT rather than as a formula: two chance rolls per entry of the player vector, neither site inside a back edge, both probabilities strictly inside (0,1) so neither early-out fires. src/app/trade_raid implements it with the word cost reported rather than assumed, so a tuning table that pushes an odds value to 0 or 1 removes the draw and the ledger says so. Against lane Z's two calibrated oracle pairs the standalone now consumes 16 words and lands 4 and 2 short, which is exactly the per-call-site ledger's split for those two turns. It does NOT match the oracle's state, and the report says which sites are missing instead of netting them off the total. The tail's last phase -- the per-player turn record -- is modelled for the six fields that are recoverable from the wire, and self-checked every run against the record the input save already carries for its own turn: 480 fields over 80 player-records across the corpus, 0 mismatches. It is not committed. Under --commit-blocked it closes 24 container-shaped divergences on the reference pair and opens 17 leaf-shaped ones, all of them in the five fields the model does not hold, so the block is a measurement now rather than an argument. phases 14/44 of the two turn drivers (8 committed), 3/37 of the tail. divergence unchanged: 209->204 and 108->103, 5 closed / 0 regressed on both.
142 lines
5.3 KiB
C++
142 lines
5.3 KiB
C++
// 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 <cstdio>
|
|
#include <vector>
|
|
|
|
#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<float> 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<float> 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;
|
|
}
|